Introducción: La detección que no estaba ahí
Pensé que tenía cubierto el reconocimiento de Active Directory. Después de cuatro partes de esta serie, había construido detecciones para:
- Parte 1: Transformación de OID: cómo los filtros LDAP de Impacket se convierten en operaciones bit a bit en los registros
- Parte 2: La pesadilla de los espacios en blanco: construcción de reglas Sigma que manejan el caos de formato
- Parte 3: SDFlags: el parámetro LDAP oculto que revela BloodHound
- Parte 4: El patrón (!FALSE): cómo se transforman las consultas de SOAPHound y cómo detectarlas
Esta parte es diferente. No hay código de atacante que analizar. El actor de amenazas usó PowerShell nativo. Esta es la historia de por qué eso bastó para evadirlo todo.
Las reglas Sigma estaban analizando los filtros del evento 1644. Estaba seguro de mí mismo.
Luego revisé los registros de una intrusión real.
El comando que lo cambió todo
El actor de amenazas había enumerado con éxito todo un entorno de Active Directory. Cada objeto de computadora. Cada propiedad. Descriptores de seguridad. Membresías de grupos. Nombres principales de servicio (SPN). Todo.
El comando estaba ahí mismo en los registros de PowerShell:
geT-aDcompUTER -Filter {eNaBLed -eq $TruE} -PROPErTieS *
Ninguna de mis detecciones se activó.
El atacante extrajo más de 500 objetos de computadora con propiedades completas y descriptores de seguridad, y se fue sin dejar rastro. Mis detecciones de las partes 1 a 4 fueron completamente ciegas a esto.
Ofuscación de mayúsculas y minúsculas. ¡Eso era todo! "¡No puede ser en serio!" Eso fue lo que evadió meses de trabajo de ingeniería de detección.
Pero esto fue lo que realmente me molestó: la ofuscación de mayúsculas y minúsculas ni siquiera era la razón por la que funcionó.
Incluso si hubieran escrito un Get-ADComputer perfectamente normal sin ninguna ofuscación, mis detecciones tampoco se habrían activado. La mezcla de mayúsculas y minúsculas era irrelevante, solo una evasión adicional sobre un punto ciego cuya existencia yo desconocía.
Esa comprensión me llevó a semanas de investigación: ¿por qué la enumeración de AD mediante PowerShell, ofuscada o no, era completamente invisible para todo lo que había construido?
Por qué falló la detección: el punto ciego de ADWS
Para corregir mi brecha de detección, necesitaba entender exactamente cómo PowerShell se comunica con Active Directory y por qué es invisible para la detección LDAP tradicional. Esto es lo que aprendí por las malas:
PowerShell no usa LDAP a través de la red.
Cuando ejecutas Get-ADComputer desde una estación de trabajo, PowerShell no establece una conexión LDAP al puerto 389 o 636. En cambio, usa Active Directory Web Services (ADWS), un servicio web basado en protocolo SOAP/XML que funciona en el puerto 9389 con cifrado TLS.
El tráfico es completamente invisible para la detección LDAP basada en red:
Figura 1: El punto ciego de ADWS. PowerShell nunca toca el puerto 389; usa protocolo SOAP cifrado en el puerto 9389, y para cuando se convierte en LDAP, la IP del cliente es localhost.
El detalle arquitectónico crítico
Cuando ADWS consulta Active Directory en nombre de una sesión remota de PowerShell, realiza esas consultas como localhost (::1 o 127.0.0.1) en el controlador de dominio. La IP original del cliente se registra en eventos específicos de ADWS (1138/1139), pero la consulta LDAP real (evento 1644) muestra localhost.
Esto genera tres problemas de detección:
- Los sensores LDAP de red son ciegos - La consulta nunca toca el puerto 389/636
- El evento 1644 muestra localhost - Parece actividad interna del sistema, no enumeración remota
- La IP real del atacante está oculta - Enterrada en los registros LDAP como [::1]
El evento 1644 muestra:
Client: [::1]:57132 ← ¡Localhost, no la IP del atacante!
User: DOMAIN\attacker
Filter: ( & ( ! (userAccountControl&2) ) (objectClass=computer) ... )
Para mi lógica de detección, esto parecía completamente normal: localhost hablando consigo mismo con una cuenta de dominio válida.
La pista falsa de la ofuscación de mayúsculas y minúsculas
El atacante usó ofuscación de mayúsculas y minúsculas (geT-aDcompUTER), tratando de evadir el registro de línea de comandos de PowerShell, los patrones de registro de bloques de script y las herramientas de seguridad basadas en coincidencia de cadenas.
Pero era irrelevante. PowerShell no distingue entre mayúsculas y minúsculas, y para cuando la consulta llega al controlador de dominio, se traduce a LDAP estándar:
PowerShell: geT-aDcompUTER -Filter {eNaBLed -eq $TruE}
↓
Capa de traducción de ADWS
↓
LDAP: ( & ( ! (userAccountControl&2) ) (objectClass=computer) )
Sin rastro de la ofuscación de mayúsculas y minúsculas. Sin rastro de la sintaxis de PowerShell. Solo LDAP limpio y optimizado.
La ofuscación logró evadir las detecciones de endpoint, pero ese no era el verdadero problema. El verdadero problema era que yo no tenía ninguna estrategia de detección para ADWS en absoluto.
La investigación que llevó hasta aquí
Después de este incidente, pasé semanas en el laboratorio:
- Habilitando el registro de ingeniería de campo en los controladores de dominio
- Capturando tráfico con Wireshark en un puerto espejo observando el puerto 9389
- Ejecutando cada comando de enumeración que se me ocurrió
- Correlacionando eventos entre los ID de evento 1138, 1139, 1166, 1167 y 1644
- Entendiendo la arquitectura de ADWS - Cómo PowerShell realmente se comunica con AD
Esta publicación de blog es el resultado de esa investigación. No es teórica; son lecciones aprendidas de una falla de detección real, análisis de registros real y pruebas reales en mi entorno de laboratorio MARVEL.LOCAL.
Esta es la parte 5A: cómo aprendí a detectar lo que no podía ver.
Qué hace diferente la detección de ADWS
ADWS requiere un enfoque de detección fundamentalmente distinto al de las partes 1 a 4. La siguiente tabla compara ambos flujos de datos lado a lado:
|
Partes 1–4: Detección de LDAP en la red |
Parte 5A: Correlación de múltiples eventos basada en endpoint | |
|---|---|---|
|
Flujo |
[Script de Python del atacante] ↓ Puerto 389/636 (LDAP/LDAPS) ↓ Captura de paquetes de red ↓ Analizar el filtro LDAP y evaluarlo ↓ Alerta de detección |
[PowerShell Get-ADComputer] ↓ Puerto 9389 (ADWS, cifrado) ↓ [DC: Servicio ADWS] ↓ Eventos 1138/1139: Conexión + autenticación ↓ [LDAP interno vía ::1] ↓ Evento 1644: Detalles de la consulta Eventos 1166/1167: Estadísticas e índices ↓ Correlacionar por ID de operación ↓ Alerta de detección |
|
Resultado |
❌ No detecta PowerShell • PowerShell usa el puerto 9389, no el 389/636 • El evento 1644 muestra ::1, no la IP del atacante • No hay firmas específicas de la herramienta • Parece actividad normal del controlador de dominio |
✅ Detecta PowerShell • El controlador de dominio registra cada paso del proceso ADWS • El evento 1644 muestra el filtro LDAP real • Los eventos 1138/1139 revelan quién hizo la consulta • La correlación de múltiples eventos ofrece una detección de alta fidelidad |
El dato clave: el evento 1644 por sí solo muestra localhost, lo que oculta al atacante. Pero cuando lo correlacionas con los eventos de conexión de ADWS, obtienes el panorama completo, incluyendo quién está realmente detrás de la consulta. Esa es la brecha de detección que cierra esta publicación.
Análisis del comando del actor de amenazas
Analicemos qué hizo realmente el atacante y por qué evadió la detección:
El comando
geT-aDcompUTER -Filter {eNaBLed -eq $TruE} -PROPErTieS *
Qué pretendía el atacante
Ofuscación de mayúsculas y minúsculas: geT-aDcompUTER en lugar de Get-ADComputer
- Objetivo: evadir las detecciones de registro de línea de comandos de PowerShell
- Objetivo (target): soluciones EDR que buscan coincidencias exactas de cadenas
- Resultado: evadió con éxito la detección de endpoints
Lógica del filtro: {eNaBLed -eq $TruE}
- Objetivo: obtener solo cuentas de computadora habilitadas (objetivos activos)
- Traducción: PowerShell convierte esto en la verificación bit a bit !(userAccountControl&2)
- Resultado: consulta eficiente usando índices de AD
Volcado completo de propiedades: -PROPErTieS *
- Objetivo: recopilar la máxima información para su explotación posterior
- Qué obtiene:
- versiones del sistema operativo (niveles de parche)
- nombres principales de servicio (ataques de Kerberos)
- marcas de tiempo del último inicio de sesión (activas frente a obsoletas)
- nombres DNS (mapeo de red)
- membresías de grupos (identificación de privilegios)
- descriptores de seguridad (enumeración de permisos)
- más de 50 atributos por objeto de computadora
Qué sucedió realmente en el controlador de dominio
Cuando se ejecuta el comando, esta es la cadena de eventos de la recreación del laboratorio en MARVEL.LOCAL:
Lado del cliente de PowerShell (WAKANDA-WRKSTN):
geT-aDcompUTER -Filter {eNaBLed -eq $TruE} -PROPErTieS *
# La ofuscación de mayúsculas y minúsculas no importa: PowerShell no distingue mayúsculas de minúsculas
Controlador de dominio - servicio ADWS (Earth-DC):
- recibe una solicitud cifrada de protocolo SOAP/XML en el puerto 9389
- descifra el payload TLS
- analiza la consulta de PowerShell desde el sobre de protocolo SOAP
- la traduce a la sintaxis de filtro LDAP
- hace una conexión LDAP LOCAL a NTDS mediante [::1]
- devuelve resultados envueltos en protocolo SOAP/XML
Registros de eventos generados:
Evento 1644 (lo que detectó mi detección de la Parte 5):
Cliente: [::1]:57132 ← ¡Localhost! ¡No es la IP del atacante!
Usuario: MARVEL\loki
Filtro: ( & ( ! (userAccountControl&2) ) (objectClass=computer)
(objectCategory=CN=Computer,CN=Schema,CN=Configuration,DC=marvel,DC=local) )
Selección de atributos: [all_with_list]name,objectClass,objectGUID,...[más de 50 atributos]
Controles del servidor: SDflags:0x7
Entradas visitadas: 3
Entradas devueltas: 3
Tiempo de búsqueda: 16 ms
Por qué mis detecciones no lo captaron:
- la IP del cliente era [::1]: filtré el localhost como "actividad del sistema"
- el usuario era una cuenta de dominio válida: no había patrón de credential stuffing
- la consulta era eficiente (relación de eficiencia 3/3): parecía una consulta de administrador optimizada
- bajo recuento de objetos (solo 3 computadoras en el dominio de prueba): por debajo del umbral volumétrico
- tiempo de consulta rápido (16 ms): no lo bastante lento para activar las alertas de búsquedas costosas
Mis líneas base decían: "Esto se ve normal". No lo era.
Lo que me perdí: el contexto de ADWS
El evento 1644 por sí solo no cuenta toda la historia. Necesitaba ver:
Evento 1138 (conexión ADWS):
User: MARVEL\loki
Client: [::1]:57132 ← ¡Esto se correlaciona con el evento 1644!
Operation: ldap_search
Operation ID: 5
Evento 1139 (autenticación ADWS):
Operation ID: 5 ← ¡La misma operación!
Status: 0 (Success)
User: MARVEL\loki
Eventos 1166 (estadísticas por objeto):
Operation ID: 5
Object: CN=WAKANDA-WRKSTN,CN=Computers,DC=marvel,DC=local
Object: CN=Earth-DC,CN=Computers,DC=marvel,DC=local
Object: CN=Asgard-WS,CN=Computers,DC=marvel,DC=local
Evento 1167 (uso de índices):
Operation ID: 5
Index: idx_objectCategory:3:N
El patrón que debí haber visto:
Todos estos eventos comparten el Operation ID 5 y ocurren dentro de una ventana de 10 segundos. Cuando los correlacionas:
Evento 1138 (inicio) + evento 1644 (detalles LDAP) + evento 1166 (resultados) +
Evento 1167 (índices) + evento 1139 (completo)
= sesión de enumeración de AD con PowerShell
Pero yo no estaba correlacionando eventos. Estaba mirando el evento 1644 de forma aislada.
El indicador [all_with_list]
El campo de selección de atributos mostraba:
[all_with_list]name,objectClass,objectGUID,sAMAccountName,dNSHostName,...
Ese prefijo [all_with_list] es exclusivo del parámetro -Properties * de PowerShell.
No deberías ver esto en:
- consultas LDAP en Python (especifican atributos explícitamente)
- DirectorySearcher de .NET (especifica PropertiesToLoad)
- herramientas LDAP nativas (listan atributos exactos)
- consultas nativas de Windows (solo atributos mínimos)
Este prefijo SOLO aparece cuando alguien ejecuta:
Get-ADUser -Properties *
Get-ADComputer -Properties *
Get-ADGroup -Properties *
Debí haber alertado sobre [all_with_list] combinado con objectClass=computer u objectClass=user, pero no lo estaba buscando.
La señal de alerta SDflags:0x7
Los controles del servidor mostraban SDflags:0x7. En binario: 0x7 = 0b111, lo que significa:
- Bit 1 (0x1): OWNER_SECURITY_INFORMATION
- Bit 2 (0x2): GROUP_SECURITY_INFORMATION
- Bit 3 (0x4): DACL_SECURITY_INFORMATION
El atacante solicitó descriptores de seguridad completos: quién es el propietario de cada objeto de computadora y quién tiene permisos sobre ellos. Este es un reconocimiento clásico de escalada de privilegios.
Las consultas administrativas normales no solicitan descriptores de seguridad. BloodHound sí lo hace. PowerView también. Los atacantes también.
Pero mi detección no estaba analizando los controles del servidor en absoluto.
Lecciones aprendidas
De este único incidente:
- El evento 1644 por sí solo es insuficiente: necesitas los eventos de ADWS circundantes para tener contexto
- La IP localhost significa ADWS; no la filtres, correlaciónala con los eventos 1138/1139
- [all_with_list] es una señal clara de volcados masivos de propiedades por PowerShell
- Los SDflags en los controles del servidor indican enumeración de descriptores de seguridad
- Los ID de operación conectan toda la cadena de ataque a través de los cinco tipos de eventos
- La ofuscación de mayúsculas y minúsculas es irrelevante en la capa LDAP: concéntrate en lo que el DC realmente registra, no en cómo se veía la sintaxis de PowerShell
El resto de esta publicación de blog trata sobre cómo construí detecciones que realmente funcionan.
Arquitectura de ADWS: cómo PowerShell realmente se comunica con AD
La pila de ADWS
Active Directory Web Services no es un simple envoltorio alrededor de LDAP. Es una pila completa de servicios web basada en protocolo SOAP que ofrece una interfaz completamente diferente a Active Directory:
Módulo PowerShell ActiveDirectory
↓
.NET System.DirectoryServices
↓
API cliente de ADWS
↓
WS-Management (WinRM)
↓
Servicio ADWS (puerto 9389)
↓
Base de datos de Active Directory
Cuando ejecutas Get-ADUser -Filter *, esto es lo que realmente ocurre:
- PowerShell llama al módulo ActiveDirectory
- El módulo usa clases de .NET DirectoryServices
- Estas clases se conectan a ADWS mediante WS-Management
- ADWS traduce la solicitud en consultas LDAP internas
- ADWS consulta la base de datos de AD directamente (sin LDAP de red)
- Los resultados se devuelven a través de la misma pila
Por eso la detección de LDAP basada en red pasa por alto por completo la enumeración con PowerShell. Las consultas LDAP ocurren dentro del controlador de dominio, nunca llegan a la red.
El problema del localhost
Este es el detalle arquitectónico crítico que hace posible la detección de ADWS:
Cuando ADWS consulta Active Directory en nombre de una sesión remota de PowerShell, hace esas consultas como localhost en el controlador de dominio. Esto crea un patrón distintivo en los registros:
Evento 1644: consulta LDAP desde ::1 (o 127.0.0.1)
Evento 1138: conexión ADWS desde [IP del atacante]
Evento 1139: conexión ADWS autenticada
Evento 1166: operación Enumerate de ADWS
Evento 1167: sesión ADWS finalizada
Los servidores Windows modernos usan IPv6 de forma predeterminada, así que normalmente verás ::1 (localhost de IPv6) en los registros del Evento 1644, aunque algunos sistemas pueden usar 127.0.0.1 (localhost de IPv4). Tus reglas de detección deben contemplar ambos patrones.
La combinación de consultas LDAP de localhost (Evento 1644) con conexiones ADWS remotas (Eventos 1138/1139) es la señal reveladora. Las aplicaciones legítimas no generan este patrón. Los atacantes que hacen enumeración con PowerShell sí.
El patrón de correlación de cinco eventos
Cada evento aporta una pieza del rompecabezas:
|
Event ID |
Origen |
Qué te indica |
Campos clave |
|
1138 |
ActiveDirectory_WebServices |
Conexión iniciada |
ClientAddress, InstanceId |
|
1139 |
ActiveDirectory_WebServices |
Qué cuenta se autenticó |
PrincipalName, InstanceId |
|
1644 |
ActiveDirectory_DomainService |
La consulta LDAP real |
Filter, Attributes, ClientIP, EntriesReturned |
|
1166 |
ActiveDirectory_WebServices |
Estadísticas por objeto |
InstanceId, TimeElapsed |
|
1167 |
ActiveDirectory_WebServices |
Sesión completa |
InstanceId |
La clave de correlación: Todos los eventos comparten el mismo Operation ID o InstanceId dentro de una ventana de tiempo corta. Así es como los vinculas en una sola sesión de enumeración.
Nota técnica: Windows Server 2016+ usa IPv6 de forma predeterminada, así que normalmente verás ::1 en los registros del Evento 1644. Algunos sistemas usan 127.0.0.1. Tu detección debe tener en cuenta ambos casos.
Detección en acción: pruebas reales de laboratorio
Revisemos un escenario real de detección del laboratorio MARVEL.LOCAL con registros de eventos reales de Windows.
Entorno de laboratorio:
- Cliente: WAKANDA-WRKSTN (10.1.1.14) - Windows 10/11, cuenta de usuario MARVEL\loki
- Controlador de dominio: Earth-DC.marvel.local (10.1.1.11) - Windows Server 2022, con registro de Field Engineering habilitado
Desde la estación de trabajo cliente, ejecuté:
PS C:\> Get-ADComputer -Filter * -Properties *
Lo que vio la red
1.113 paquetes de protocolo SOAP/XML cifrado con TLS en el puerto 9389. Ningún filtro LDAP visible. Ningún atributo visible. Detección de red: ciega.
Lo que vio el Evento 1644
Log Name: Directory Service
Event ID: 1644
User: MARVEL\loki
Client: [::1]:57132
Filter:
( & (objectClass=*) (objectClass=computer) (objectCategory=CN=Computer,CN=Schema,CN=Configuration,DC=marvel,DC=local) )
Attribute selection:
[all_with_list]name,objectClass,objectGUID,sAMAccountName,
nTSecurityDescriptor,servicePrincipalName,memberOf...
Server controls: SDflags:0x7
Visited entries: 3
Returned entries: 3
Search time (ms): 16
Indicadores clave
|
Indicador |
Qué significa |
|
Cliente: [::1]:57132 |
Traducción de ADWS: la IP real queda oculta |
|
Prefijo [all_with_list] |
Se usó PowerShell -Properties * |
|
SDflags:0x7 |
Enumeración del descriptor de seguridad (Owner+Group+DACL, excluye SACL) |
|
Usuario: MARVEL\loki |
Se conserva el contexto de la cuenta de usuario autenticada |
Uniendo las piezas
Operation ID 5 (Get-ADComputer de loki):
1138 START: MARVEL\loki, [::1]:57132, ldap_search
1644 DETAILS: Filter, attributes, 3 entries, 16ms
1166 STATS: Per-object timing
1167 INDEXES: idx_objectCategory:3:N
1139 COMPLETE: Status 0
Los cinco eventos están vinculados por el Operation ID. Esa es tu clave de correlación.
Curiosamente, también observé consultas LDAP simultáneas con ANONYMOUS LOGON desde la misma estación de trabajo que mostraban la IP real, un recordatorio de que no toda la enumeración usa ADWS, y la detección LDAP directa (Partes 1-4) sigue siendo importante.
Conclusiones clave de las pruebas reales
- El patrón de localhost es consistente: Cada consulta de PowerShell a AD mediante ADWS muestra el cliente como [::1] o 127.0.0.1, nunca la IP real del cliente. Esto es arquitectónico, no un error.
- La correlación de múltiples eventos es esencial: Un solo Evento 1644 por sí solo es ruidoso. Pero correlacionar 1138+1644+1166+1167+1139 por Operation ID da una detección de alta fidelidad.
- El [all_with_list] prefijo solo aparece con -Properties *. Es un indicador claro de enumeración masiva, no de una consulta de administrador específica.
- La detección de red pasa por alto a PowerShell por completo: La captura de Wireshark muestra protocolo SOAP/XML cifrado en el puerto 9389. No puedes ver el filtro LDAP, los atributos solicitados ni los objetos devueltos. La detección basada en el equipo mediante eventos de Windows es la ÚNICA forma de detectar esto.
Conclusión
La enumeración de AD con PowerShell está en todas partes. Es el método predeterminado para la administración legítima, y es el método preferido para los atacantes que quieren pasar inadvertidos. La detección de un solo evento no funciona: el Evento 1644 por sí solo es demasiado ruidoso, y los eventos de ADWS por sí solos son demasiado comunes.
Pero cuando correlacionas varios eventos entre sí, cuando ves consultas LDAP de localhost provenientes de conexiones ADWS, cuando rastreas sesiones de enumeración completas mediante InstanceId, cuando aplicas análisis estadístico para detectar anomalías, desbloqueas una detección de alta fidelidad del reconocimiento basado en PowerShell.
Cada parte se construyó sobre la anterior. Cada técnica de detección llenó un vacío. Y ahora tenemos una cobertura integral del reconocimiento de Active Directory: desde herramientas de Python que atacan LDAP a través de la red, hasta comandos nativos de PowerShell que usan ADWS internamente.
Todavía queda un problema: el Evento 1644 muestra localhost, no la IP real del atacante. En la Parte 5B, mostraré cómo resolver esto con la correlación del Evento 5156 y la ventana de tiempo de aproximadamente 60-80 ms que hace posible la atribución de la IP de origen.
Gracias a Jonathan Johnson, Anton Ovrutsky, Matt Anderson, Tyler Bohlmann, Michael Haag, Nasreddine Bencherchali, y Adam Bienvenu por su ayuda en la revisión de esta publicación.