La herramienta dice que no genera registros de auditoría de Windows. Lo comprobamos.
La parte 5A demostró que el punto ciego de ADWS existe. La parte 5B demostró que tiene solución. El evento 5156 te da la IP real del atacante detrás de localhost. Los datos siempre estuvieron ahí. Nadie los estaba correlacionando. La parte 6 presenta un punto ciego diferente: uno que vive en un protocolo que tu stack de detección nunca ha examinado, y una herramienta que es más detectable de lo que afirma su README.
Entonces, por supuesto, me acordé de ldapnomnom. Aquí vamos de nuevo ¯\_(ツ)_/¯
El README es bastante directo:
Fuerza bruta silenciosa y anónima de nombres de usuario de Active Directory a velocidades demenciales desde los controladores de dominio, (ab)usando solicitudes LDAP Ping (cLDAP) ...
No se generan registros de auditoría de Windows.
Sin registros de auditoría de Windows. Es una afirmación contundente. En esta serie, no tomo las afirmaciones contundentes al pie de la letra. Leo el código fuente. Lo ejecuto. Reviso los registros.
Figura 1: ldapnomnom en acción: anónimo, sin autenticación y rápido, extrayendo nombres de usuario válidos de Active Directory directamente de un controlador de dominio. El README afirma que no genera registros de auditoría de Windows. El resto de esta publicación lo comprueba, una fuente de registros a la vez.
Lo que encontré: el README está equivocado. No del todo. La técnica en la que se basa tiene una brecha de detección real. Pero la herramienta en sí, tal como se distribuye, es más detectable de lo que se anuncia. Para entender por qué, tienes que bajar desde el README hasta main.go, entrar en la biblioteca LDAP subyacente y llegar hasta la Windows Filtering Platform. Esa es la metodología From Code to Coverage, y esta publicación recorre cada capa.
Dónde encaja LDAP Ping
En cuanto un atacante tiene aunque sea una sola cuenta de dominio con pocos privilegios, la superficie de ataque se amplía drásticamente. El resto de esta serie cubre el lado autenticado: cómo BloodHound, PowerView y SOAPHound consultan el directorio, qué ve el evento 1644, cuándo no se activa y cómo detectar la enumeración lenta y sigilosa que se mantiene por debajo de cualquier umbral.
LDAP Ping es una herramienta previa a las credenciales. Te dice quién existe. Todo lo que viene después (a qué tienen acceso, en qué grupos están, qué cuentas son vulnerables a Kerberoasting) requiere autenticación. Las dos fases tienen historias de detección completamente distintas.
Tres caminos hacia Active Directory
Antes de entrar en la herramienta, la historia de detección exige entender qué stack de procesamiento gestiona la consulta en el DC. Hay tres caminos distintos, y cada uno tiene un comportamiento de registro completamente diferente:
LDAP normal | ADWS (PowerShell) | cLDAP (UDP real) | |
Identidad | |||
Protocolo | TCP | TCP | UDP |
Puerto | 389 / 636 (TLS) | 9389 | 389 |
Autenticación | Obligatoria (bind) | Obligatoria (Kerberos/NTLM) | Ninguna, anónima |
Quién lo usa | BloodHound, Impacket, ADFind | Módulo de AD de PowerShell, SOAPHound, Soapy | DC Locator, ldapnomnom (según afirma) |
Procesamiento | |||
Procesado en el DC por | LSASS / ntdsa.dll | Servicio ADWS → LSASS / ntdsa.dll | LSASS / netlogon.dll |
¿Llega al motor LDAP? | Sí | Sí (a través del proxy ADWS) | No, despacho independiente |
Detección | |||
Evento 1644 | Si es lo bastante costosa | Muestra localhost [::1] | Estructuralmente imposible |
Evento 5156 | Con auditoría FPC | Con auditoría FPC | UDP, WFP se comporta de forma distinta |
IP de origen visible | Sí, directa | No, localhost (resuelto en 5B) | Ausente en los registros de Windows; requiere pcap, NDR o MDI |
La división crítica está en la fila "Procesado en el DC por". Tanto LDAP normal como ADWS terminan llegando al motor LDAP ntdsa.dll dentro de LSASS. El cLDAP sobre UDP real va al controlador de netlogon.dll, también dentro de LSASS, pero por una ruta de código completamente distinta. El evento 1644 lo genera el motor LDAP. Por eso el evento 1644 no puede registrar cLDAP real. Las dos rutas de código no se superponen en absoluto en el procesamiento de consultas.
LDAP Ping no es una búsqueda LDAP
Vale la pena ser preciso en esto, porque es el malentendido más común sobre esta técnica. LDAP Ping y la búsqueda LDAP normal comparten el formato de transmisión. Ambas son SearchRequests LDAP codificadas en BER enviadas al puerto 389. Pero son operaciones completamente diferentes, procesadas por código completamente diferente en el DC.
Búsqueda LDAP normal: lo que usan BloodHound y PowerView
Requiere autenticación. El cliente debe hacer bind antes de emitir consultas
Consulta el árbol de directorio de AD: usuarios, grupos, equipos, GPO, ACL, todo
La procesa ntdsa.dll, el stack del servidor LDAP dentro de LSASS.exe
Por eso el evento 1644 puede verla. La consulta pasa por la maquinaria de consultas costosas que monitorea el 1644
Por eso necesita credenciales. Los clientes anónimos no pueden leer el árbol de directorio de forma predeterminada
LDAP Ping: lo que usan ldapnomnom y cldap_ping.py
No requiere ninguna autenticación; es anónimo por diseño
NO consulta el árbol de directorio en absoluto
El filtro (&(NtVer=...)(AAC=...)(User=...)) se despacha al controlador de netlogon.dll antes de tocar siquiera el motor LDAP
Netlogon hace una única búsqueda dirigida en el NC del directorio para ese sAMAccountName. Sin motor LDAP, sin análisis de filtros, sin recorrer un conjunto de candidatos
Devuelve un blob NETLOGON_SAM_LOGON_RESPONSE_EX de formato fijo. No es una entrada real del directorio ni el resultado de una búsqueda LDAP
Por eso el evento 1644 nunca se activa. La consulta nunca llega al motor LDAP
Piénsalo así:
Figura 2: Dentro de un controlador de dominio, una búsqueda LDAP normal y un LDAP Ping comparten el puerto 389, pero se ejecutan en rutas de código distintas; solo una de ellas es visible para la auditoría de la capa LDAP.
! Esto no es un problema de configuración de registros. No existe ninguna clave de registro, ningún ajuste de nivel de detalle ni ninguna regla Sigma que haga que el evento 1644 registre el tráfico de LDAP Ping. Las dos operaciones comparten un puerto, pero nada más. Los stacks de procesamiento están separados a nivel de arquitectura.
LDAP Ping es un mecanismo específico de Microsoft para DC Locator: el proceso mediante el cual los clientes Windows encuentran un controlador de dominio al arrancar. Es anónimo por diseño. Tiene que funcionar antes de que se autentique cualquier usuario. Esa es la puerta por la que entra ldapnomnom.
El filtro incluye un elemento User= . Cuando se completa, el servicio Netlogon lo resuelve en el NC del directorio, aplica la prueba de máscara de bits AAC y establece el OperationCode de la respuesta según el resultado:
Código de operación | Nombre simbólico | Estado de la cuenta en el DC | Resultado |
0x17 | LOGON_SAM_LOGON_RESPONSE_EX | Existe, está habilitada y coincide con la máscara AAC | ACIERTO |
0x19 | LOGON_SAM_USER_UNKNOWN_EX | No existe | FALLO |
0x19 | LOGON_SAM_USER_UNKNOWN_EX | Existe pero está DESHABILITADA | FALLO, ventaja para el defensor |
0x19 | LOGON_SAM_USER_UNKNOWN_EX | Existe pero no supera la prueba de máscara de bits AAC | FALLO |
Referencia: [MS-ADTS] § 6.3.1.3 Operation Code para la tabla de valores, y § 6.3.3.2 Domain Controller Response to an LDAP Ping para la lógica de enrutamiento de cuentas deshabilitadas/AAC.
Las tres filas inferiores son la asimetría a favor del defensor que veremos en los datos del laboratorio. El atacante recibe la misma respuesta Unknown tanto si la cuenta no existe como si existe pero está deshabilitada o si existe pero no coincide con la máscara AAC. El DC tiene la información para distinguirlas. El atacante no. La respuesta en la red no la incluye y, con los niveles de registro predeterminados, el DC no deja ningún rastro de la solicitud. El registro de depuración de Netlogon (nltest /dbflag:0x2080FFFF) sí captura los pings entrantes como entradas [MAILSLOT] cuando está habilitado, pero no viene activado de forma predeterminada y, fuera de la resolución activa de problemas de bloqueo de cuentas, rara vez se activa.
La pregunta lógica que sigue: si LDAP Ping funciona de forma anónima y usa el mismo puerto que LDAP normal, ¿no puede un atacante usarlo para volcar usuarios, grupos, equipos y SPN sin credenciales?
No, y la razón es de arquitectura, no una brecha de configuración. LDAP Ping no es realmente una búsqueda LDAP. Es una sonda de DC Locator de función fija con envoltorio LDAP. El DC identifica el patrón de alcance base de rootDSE + atributo netlogon y lo enruta a una instrucción switch en netlogon.dll, no al motor de búsqueda de ntdsa.dll. El controlador devuelve un blob NETLOGON_SAM_LOGON_RESPONSE_EX de formato fijo: identidad del DC, información del sitio, indicadores de capacidad y, si la solicitud incluía User=, un código de operación que te dice si esa cuenta existe y es apta para iniciar sesión. Esa es toda la superficie de salida. No hay ningún campo para pertenencia a grupos, último cambio de contraseña, descripción, SPN ni ACL, y la gramática del filtro no acepta LDAP arbitrario. Es un conjunto fijo de coincidencias de igualdad contra elementos con nombre. Los atacantes no pueden ampliar la consulta porque en el otro extremo no hay código que se pueda ampliar.
El techo previo a las credenciales
La superficie de ataque anónima completa de AD. Todo lo que un atacante sin autenticar puede obtener de un DC:
Operación | Lo que obtienes | Credenciales necesarias |
Consulta anónima a rootDSE | Nombre de dominio, nombre del bosque, FQDN e IP del DC, niveles funcionales, nombre del sitio, mecanismos SASL, indicadores de rol del DC (PDC/GC/KDC/RODC) | No |
LDAP Ping | Por nombre de usuario: válida y habilitada / no encontrada / deshabilitada (indistinguibles para el atacante) | No |
Todo lo demás | Usuarios, grupos, equipos, GPO, SPN, ACL, descripciones, políticas de contraseñas, relaciones de confianza, certificados. El directorio completo | Sí |
La tercera fila es un muro infranqueable. Los binds LDAP anónimos al puerto 389 fallarán o no devolverán nada útil para consultas al directorio. Desde Windows Server 2003, AD exige como mínimo una cuenta de dominio con pocos privilegios para las búsquedas en el directorio.
El valor de LDAP Ping está específicamente en la fase previa a las credenciales: confirmar qué nombres de usuario existen antes de intentar autenticarse, sin generar fallos de autenticación sobre los que los defensores puedan alertar. Responde a "¿cuáles de estos 10,000 nombres de usuario son reales?" antes de un password spray. Ese es el modelo de amenaza: un requisito previo de reconocimiento, no un reemplazo de la enumeración autenticada.
Lectura del código fuente
ldapnomnom es un único archivo de Go. Recorramos las secciones críticas de main.go, con números de línea de la v1.5.1 (commit bdc7fae), la versión actual al momento de escribir esto.
Los flags definidos: no existe ningún flag UDP
// main.go: flag definitions (lines 50-73)
port := flag.Int("port", 389, "LDAP port to connect to (389 or 636 typical)")
tlsmodeString := flag.String("tlsmode", "NoTLS", "Transport mode (TLS, StartTLS, NoTLS)")
// Evasive maneuver flags
throttle := flag.Int("throttle", 0, "Only do a request every N ms, 0 to disable")
maxrequests := flag.Int("maxrequests", 0, "Disconnect and reconnect after n requests")Fíjate en lo que falta: no hay flag --udp, ni flag --cldap, ni flag --protocol. Las únicas opciones de transporte son variantes del modo TLS. Todas TCP. Esto se confirmó en el laboratorio: un intento de ejecutar --ldap udp devolvió flag provided but not defined: -ldap. La conclusión de que solo usa TCP se obtuvo mediante el análisis del código fuente, no ejecutando flags de transporte que no existen. El enum del modo TLS lo deja explícito:
// TLS mode constants: every option is TCP
type TLSmode byte
const (
TLS TLSmode = 0 // TCP + TLS on port 636
StartTLS TLSmode = 1 // TCP, upgrade with STARTTLS
NoTLS TLSmode = 2 // TCP, plaintext (DEFAULT)
)La conexión: siempre TCP
Todas las rutas de conexión de main.go llevan a TCP. El switch sobre tlsmode aparece tres veces en el código (líneas 204–217, 297–310, 371–383) y cada rama llama a ldap.Dial con "tcp" de forma explícita:
// main.go: connection establishment (lines 371-383)
// This exact switch appears 3 times. Every case: TCP.
switch tlsmode {
case NoTLS:
conn, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", server, *port))
case StartTLS:
conn, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", server, *port))
if err == nil {
err = conn.StartTLS(&tls.Config{ServerName: server})
}
case TLS:
conn, err = ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", server, *port), config)
}TCP. TCP. TCP. En todos los casos. La herramienta nunca llama a ldap.DialURL con un esquema cldap://. Siempre usa las funciones heredadas ldap.Dial y ldap.DialTLS, que fijan "tcp" como parámetro de red.
La solicitud LDAP Ping: el filtro que consulta a Netlogon
El núcleo de la herramienta es la construcción de la solicitud de búsqueda (líneas 403–409). Este es el paquete real que se envía al DC por cada nombre de usuario:
// main.go: LDAP Ping search request (lines 403-409)
request := ldap.NewSearchRequest(
"", // Base DN: empty = rootDSE
ldap.ScopeBaseObject, // Scope: base object only
ldap.NeverDerefAliases,
0, 0, false,
fmt.Sprintf(
"(&(NtVer=\x06\x00\x00\x00)(AAC=\x10\x00\x00\x00)(User="+username+"))",
),
[]string{"NetLogon"}, // Attribute: triggers Netlogon response path
nil,
)Nota: en la línea 231, en la ruta de código del benchmark, existe un segundo filtro sin el elemento User, (&(NtVer=\x06\x00\x00\x00)(AAC=\x10\x00\x00\x00)). El filtro de enumeración está en la línea 406. No confundas los dos si haces grep en el código fuente.
Desglose de los elementos del filtro:
Elemento | Valor | Significado |
NtVer | | 0x00000006 en little-endian = NETLOGON_NT_VERSION_5 | NETLOGON_NT_VERSION_5EX. V5EX es el bit que solicita la estructura NETLOGON_SAM_LOGON_RESPONSE_EX que ldapnomnom analiza en busca del código de operación 0x17. |
AAC | | AllowableAccountControlBits = 0x00000010 = USER_NORMAL_ACCOUNT según MS-SAMR § 2.2.1.12 (el esquema de control de cuentas de SAMR, donde una cuenta normal es 0x10, no el atributo LDAP userAccountControl, donde es 0x200). El DC aplica una prueba AND de este valor contra el UAC de la cuenta objetivo. Limita los aciertos solo a cuentas de usuario normales y excluye las cuentas de equipo, de confianza y de DC. |
User | | El nombre de cuenta SAM que se busca. Netlogon hace una única búsqueda dirigida en el NC del directorio. Sin motor LDAP, sin análisis de filtros, sin recorrer un conjunto de candidatos. |
Atributo NetLogon | Atributo solicitado | El pseudoatributo 'NetLogon' es el disparador. Solicitarlo en una búsqueda de objeto base de rootDSE enruta la consulta al controlador del servicio Netlogon en lugar del motor LDAP. |
Sapir Federovsky documentó esta forma de filtro como una huella de ldapnomnom en su charla de TROOPERS24 The (almost) complete LDAP guide, y señaló que la conjunción NtVer + AAC + User es lo que distingue el tráfico de enumeración de nombres de usuario de los pings legítimos de DC Locator.
La comprobación de la respuesta: el byte 0x17
El análisis de la respuesta (líneas 426–432) es donde se determina el acierto o el fallo:
// main.go: response analysis (lines 426-432)
if len(response.Entries) > 0 &&
len(response.Entries[0].Attributes) > 0 &&
len(response.Entries[0].Attributes[0].ByteValues) > 0 {
res := response.Entries[0].Attributes[0].ByteValues[0]
if len(res) > 2 && res[0] == 0x17 && res[1] == 00 {
outputqueue <- username // Valid user found, write to output
}
}La comprobación res[0] == 0x17 es la clave. 0x17 es 23 en decimal, el OperationCode de LOGON_SAM_LOGON_RESPONSE_EX, la respuesta de "el usuario existe y está habilitado" de MS-ADTS § 6.3.1.3. Cualquier otro OperationCode, incluido LOGON_SAM_USER_UNKNOWN_EX (0x19), que se devuelve tanto para cuentas inexistentes como deshabilitadas, no empieza con 0x17. El nombre de usuario se descarta en silencio.
Esta comprobación de un solo byte es la razón por la que ldapnomnom solo muestra cuentas habilitadas y por la que la herramienta no puede distinguir entre una cuenta falsa y una deshabilitada. El defensor ve la diferencia. La herramienta no.
La biblioteca: la ruta UDP que nunca se toma
ldapnomnom usa un fork de la biblioteca go-ldap mantenido por Lars Karlslund (github.com/lkarlslund/ldap). Esta biblioteca sí implementa cLDAP sobre UDP real. Pero ldapnomnom nunca lo llama.
Del archivo conn.go de la biblioteca (líneas 155–213):
// github.com/lkarlslund/ldap/v3: conn.go
// DialURL connects to the given ldap URL.
// Schemas supported: ldap://, ldaps://, ldapi://,
// and cldap:// (RFC1798, deprecated but used by Active Directory).
func DialURL(addr string, opts ...DialOpt) (*Conn, error) {
u, err := url.Parse(addr)
...
switch u.Scheme {
case "cldap": // ← TRUE UDP PATH (RFC1798 connectionless LDAP)
if port == "" { port = DefaultLdapPort }
return dc.dialer.Dial("udp", net.JoinHostPort(host, port))
case "ldap": // ← TCP plaintext
if port == "" { port = DefaultLdapPort }
return dc.dialer.Dial("tcp", net.JoinHostPort(host, port))
case "ldaps": // ← TCP + TLS
if port == "" { port = DefaultLdapsPort }
return tls.DialWithDialer(dc.dialer, "tcp", ...)
}
}La ruta de conexión UDP existe. cldap:// → Dial("udp", ...). Está ahí. Funciona. Es cLDAP real: RFC1798, sin conexión, gestionado por el servicio Netlogon, invisible para el evento 1644 y el evento 5156.
ldapnomnom nunca llama a DialURL. Llama directamente a las funciones heredadas ldap.Dial y ldap.DialTLS, ambas fijadas a TCP. Desde la perspectiva de ldapnomnom, la ruta UDP de la biblioteca es código muerto.
→ La biblioteca sabe hablar cLDAP real. ldapnomnom no lo usa.
La cadena de llamadas completa
Rastrear desde la línea de comandos hasta la red muestra que no hay UDP en ninguna parte:
// Complete execution path: ldapnomnom --server 10.0.1.10 --tlsmode NoTLS
main.go: tlsmode = NoTLS → ldap.Dial("tcp", "10.0.1.10:389")
↓
conn.go: legacy Dial() → net.DialTimeout("tcp", addr, ...)
↓
TCP SYN → 10.0.1.10:389
↓
LSASS.exe receives TCP connection → Windows Filtering Platform sees it
↓
main.go: SearchRequest → (&(NtVer=\x06...)(AAC=\x10...)(User=tony.soprano))
↓
LSASS: NetLogon handler → SAM lookup for tony.soprano
↓
LSASS: response → 0x17 0x00 ... (LOGON_SAM_LOGON_RESPONSE_EX)
↓
main.go: res[0]==0x17 → outputqueue <- "tony.soprano"
↓
Windows Filtering Platform: Event 5156 (source: 10.0.1.100, dest: 389, TCP, if WFP audit enabled, off by default)Como LSASS gestiona la conexión TCP, la Windows Filtering Platform ve cada conexión. Con la auditoría de Filtering Platform Connection habilitada, el evento 5156 se activa en cada solicitud con la IP de origen completa.
Resultados del laboratorio: lo que Windows registra realmente
Evento 1644: nada
El evento 1644 registra consultas LDAP costosas o interesantes contra el directorio. Con Field Engineering configurado en 5 y el umbral de búsqueda en su valor práctico más bajo, ldapnomnom, ejecutando 40 nombres de usuario contra el DC, generó cero entradas del evento 1644.
Es lo esperado. El filtro de LDAP Ping se despacha al controlador de netlogon.dll en LSASS antes de que el motor LDAP llegue a verlo. El evento 1644 monitorea el motor de búsqueda LDAP. Los dos no comparten ninguna ruta de código. No es un problema de umbral. Es estructural.
Evento 1644: CERO eventos generados durante una ejecución de ldapnomnom con 40 nombres de usuario. Ningún umbral, filtro ni ajuste de nivel de detalle cambia esto. LDAP Ping nunca llega al motor LDAP.
netlogon.log: cada nombre de usuario, cada respuesta
Con nltest /dbflag:0x2080ffff habilitado, netlogon.log capturó todas y cada una de las consultas LDAP Ping. El formato muestra dos líneas por nombre de usuario: una línea de recepción cuando llega el ping y una línea de respuesta cuando Netlogon contesta:
Figuras 3 y 4: La ventaja del defensor. ralph.cifaretto y guest muestran la línea [MISC] NlSamVerifyUserAccountEnabled "account is disabled" antes de su Sam User Unknown Ex; los nombres inexistentes solo reciben el Unknown. El DC distingue una cuenta deshabilitada de una falsa; el atacante ve el mismo Unknown para ambas.
Tres cosas destacan en esta salida
Primero, la etiqueta de transporte. El registro dice "on UDP LDAP" incluso para las conexiones TCP de ldapnomnom. Es una peculiaridad del formato del registro de Netlogon: etiqueta todo el tráfico de LDAP Ping como UDP LDAP, sin importar el transporte real, porque LDAP Ping se concibió originalmente como un protocolo UDP. El campo de transporte de netlogon.log no es confiable para distinguir TCP de UDP. El evento 5156 es la fuente de verdad para el transporte.
Segundo, la IP de origen. El atacante aparece como (null) en todo momento. netlogon.log no tiene ningún mecanismo para registrar la IP de origen de un LDAP Ping. El registro te dice qué se enumeró, no quién lo hizo.
Tercero, ralph.cifaretto. Esta es la ventaja del defensor.
La ventaja de visibilidad del defensor
ldapnomnom recibe Sam User Unknown Ex para ralph.cifaretto, la misma respuesta que obtiene para una cuenta completamente inventada. La herramienta no registra nada, lo trata como un fallo y sigue adelante. Pero netlogon.log muestra la línea NlSamVerifyUserAccountEnabled antes de la respuesta, nombrando explícitamente la cuenta deshabilitada.
netlogon.log registra tres resultados distintos por consulta, y la diferencia entre ellos es la ventaja del defensor:
Habilitada: Sam Logon Response Ex. La cuenta existe y está activa.
Deshabilitada: una línea [MISC] NlSamVerifyUserAccountEnabled "...account is disabled" y luego Sam User Unknown Ex. La cuenta existe, pero está desactivada.
Inexistente: Sam User Unknown Ex sin línea NlSamVerify. No había ninguna cuenta que comprobar.
El atacante solo ve dos de estos. Una cuenta deshabilitada y un nombre que nunca se creó devuelven el mismo Sam User Unknown Ex en la red, así que para el atacante son el mismo fallo. El defensor ve la tercera dimensión: la línea NlSamVerifyUserAccountEnabled solo aparece para cuentas que realmente existen, así que su presencia separa una cuenta real pero deshabilitada de una falsa. En la figura, ralph.cifaretto y guest muestran esa línea; las suposiciones genéricas solo muestran el Unknown.
Los defensores ven tres estados. Los atacantes ven dos. No es una falla en la lógica de ldapnomnom. Así funciona Netlogon por diseño. La asimetría es real. Cualquier lista de palabras que incluya tus cuentas deshabilitadas te da una alerta más temprana que si esas cuentas no existieran.
netlogon.log captura cada nombre de usuario consultado y cada tipo de respuesta, y revela las cuentas deshabilitadas que el atacante solo ve como un fallo. La IP de origen está ausente (null) en todos los casos. La atribución requiere correlacionarlo con el evento 5156.
Evento 5156: IP de origen en cada conexión
Con la auditoría de Filtering Platform Connection habilitada, el evento 5156 se activó en cada conexión de ldapnomnom, tanto al puerto 389 como al puerto 636:
// Security Event Log: Event 5156 (SOPRANOS lab EVTX)
Event ID: 5156
Time Created: 2026-06-19T16:52:53
Direction: %%14592 (Inbound)
SourceAddress: 10.0.1.100 <- Kali attacker, fully visible
SourcePort: 58402
DestAddress: 10.0.1.10 <- DC
DestPort: 389
Protocol: 6 <- TCP, confirmed
Application: \device\harddiskvolume2\windows\system32\LSASS.exe
FilterRTID: 65536La IP de origen es 10.0.1.100, el atacante con Kali. La aplicación es LSASS.exe, lo que confirma que la conexión la gestiona el stack del servidor LDAP. El protocolo 6 es TCP, lo que confirma el transporte real pese al marketing de cLDAP. Lo mismo aplica al puerto 636: el cifrado TLS oculta el contenido, pero no la IP de origen, ante la Windows Filtering Platform.
De forma predeterminada, ldapnomnom usa 8 conexiones paralelas por servidor. Eso significa 8 entradas del evento 5156 por ejecución de enumeración, todas con marcas de tiempo separadas por unos pocos milisegundos. El patrón de ráfaga es en sí mismo una señal de detección, independiente del contenido de los nombres de usuario.
El evento 5156 se activa en cada conexión de ldapnomnom con la IP de origen completa del atacante, tanto en el puerto 389 (NoTLS) como en el 636 (TLS). TLS cifra el contenido LDAP; no oculta la conexión ante WFP.
Lo que no se activa
Evento 1644: cero eventos, estructuralmente imposible para LDAP Ping
IP de origen en netlogon.log: ausente, el atacante aparece como (null)
Evento 5807: no se activó. La subred de Kali (10.0.1.0/24) se asignó deliberadamente a un sitio de AD en el laboratorio para sacar al 5807 de la ecuación. El evento 5807 solo se activa con IP de origen que no corresponden a ningún sitio de AD conocido, y queríamos verificar la historia de detección en el peor caso: un atacante en una subred asignada.
cLDAP sobre UDP real: el verdadero punto ciego
ldapnomnom usa TCP. Pero el protocolo LDAP Ping funciona igual de bien sobre UDP. Eso es lo que significaba originalmente cLDAP. La biblioteca lo admite. Cualquier herramienta que llame a DialURL con un esquema cldap:// envía datagramas UDP reales y llega al servicio Netlogon sin conexión TCP, sin participación de LSASS en la capa de red y sin evento 5156.
Para caracterizar con precisión esta brecha de detección, creamos cldap_ping.py: una implementación en Python puro que solo usa la biblioteca estándar, sin dependencias externas. Envía SearchRequests LDAP auténticas codificadas en BER a través de SOCK_DGRAM al puerto 389. Cero TCP. Cero autenticación.
Figura 5: cldap_ping.py enumerando sobre UDP auténtico (SOCK_DGRAM, sin establecer conexión TCP). 9 válidos y 16 desconocidos de 25. Los 16 desconocidos son una mezcla de cuentas deshabilitadas y nombres que no existen, y todos devuelven el mismo Sam User Unknown Ex, así que el atacante no puede distinguir una cuenta deshabilitada de una falsa. El DC sí puede, y netlogon.log registra la diferencia. No se activa ningún evento 5156; solo netlogon.log captura la ejecución.
Los resultados coinciden con precisión con el estado real de AD. Las 9 cuentas válidas están todas habilitadas. Los 16 fallos son una mezcla de cuentas deshabilitadas y cuentas que no existen en absoluto. Ambas devuelven Sam User Unknown Ex. El atacante no puede distinguirlas.
El evento 5156 no se activó en ninguna de las 25 consultas UDP. netlogon.log registró todas. Este es el verdadero punto ciego: cLDAP sobre UDP le da al atacante anonimato total en la capa de registros de eventos de Windows. netlogon.log muestra qué se enumeró, pero no quién lo hizo. Para atribuir cLDAP sobre UDP real, necesitas visibilidad en la capa de red: captura de paquetes, NDR o registros del firewall. Los registros de eventos de Windows por sí solos no pueden hacerlo.
ldapnomnom usa TCP de forma predeterminada, así que en la práctica la atribución con el evento 5156 funciona. Pero cualquier atacante que use cldap_ping.py o cualquier herramienta que envíe paquetes UDP al puerto 389 elimina por completo el registro del 5156.
La historia de detección para cLDAP sobre UDP real se reduce a netlogon.log.
Reconocimiento anónimo: lo que puedes y no puedes obtener
El flag --recon de cldap_ping.py emite un bind LDAP anónimo por TCP a rootDSE antes de enumerar nombres de usuario. Es una operación estándar sin autenticación que cualquier cliente LDAP puede realizar. Tiene una huella de detección diferente a la de LDAP Ping:
Operación | Transporte | Lo que obtienes | Evento 5156 | netlogon.log | Evento 1644 |
Consulta a rootDSE | TCP | Nombre de dominio, bosque, FQDN del DC, nivel funcional, sitio, mecanismos SASL, capacidades del DC | Se activa | Silencioso | Silencioso |
LDAP Ping (cldap_ping.py) | UDP | Por nombre de usuario: habilitada / no encontrada / deshabilitada (para nosotros) | Silencioso | Se activa | Silencioso |
LDAP Ping (ldapnomnom) | TCP | Por nombre de usuario: habilitada / no encontrada / deshabilitada (para nosotros) | Se activa | Se activa | Silencioso |
Búsqueda en el directorio | TCP | Usuarios, grupos, equipos, GPO, SPN, ACL, todo | Se activa | Silencioso | Se activa (si es costosa) |
La fila de búsqueda en el directorio requiere autenticación. Las tres primeras no. Esta es la superficie de ataque anónima completa de AD antes de obtener cualquier credencial.
La consulta a rootDSE usa TCP. El evento 5156 se activa. No genera entradas en netlogon.log porque es una búsqueda LDAP normal contra la entrada especial de DN vacío que AD expone de forma anónima. El evento 1644 no se activa porque las consultas a rootDSE no son costosas en el sentido del 1644.
Qué cambia cuando usas UDP real
El evento 5156 no se activó para LDAP Ping sobre UDP en ninguna de nuestras pruebas. La Windows Filtering Platform rastrea conexiones TCP por diseño. WFP gestiona los datagramas UDP de otra manera y no genera eventos 5156 para paquetes individuales como sí lo hace con las conexiones TCP. LSASS recibe el datagrama UDP a través del socket de Netlogon, lo procesa y responde sin ningún seguimiento de conexión de WFP.
netlogon.log siguió registrando cada nombre de usuario. El servicio Netlogon no distingue entre solicitudes LDAP Ping por TCP y por UDP a nivel de registro. Aparecen las mismas líneas MAILSLOT, con la misma IP de origen (null), sin importar el transporte.
Para cLDAP sobre UDP, la visibilidad desde el propio endpoint Windows termina en netlogon.log. La atribución del origen exige salir del endpoint: captura de paquetes, NDR, registros del firewall o MDI. Este último merece su propia sección.
Otras herramientas: ldeep
ldeep es una herramienta de Python para enumeración LDAP posexplotación que se mantiene en PyPI y viene incluida en Kali Linux. Tiene un subcomando enum_users descrito como "Anonymously enumerate users with LDAP pings." La pregunta clave es qué transporte usa realmente.
ldeep recibe una URL LDAP como parámetro de servidor (-s ldap://IP). Ese esquema de URL, ldap://, corresponde a TCP en las implementaciones estándar de bibliotecas LDAP. La herramienta usa la biblioteca ldap3, que no implementa cLDAP sobre UDP de forma nativa.
Ejecuta esto contra tu DC mientras monitoreas el evento 5156 para confirmarlo:
# Kali: install ldeep
pip3 install ldeep
# Run enum_users while watching DC for 5156
ldeep ldap -d newjersey.sopranos.local -s ldap://10.0.1.10 -a enum_users /tmp/sopranos.txtSi el evento 5156 se activa: ldeep enum_users usa TCP, con la misma historia de detección que ldapnomnom. Si el evento 5156 permanece en silencio: ldeep usa UDP real, y netlogon.log es tu única fuente en el endpoint Windows.
Según la implementación de la biblioteca, se espera que ldeep use TCP. Pero verifícalo en tu laboratorio. La metodología de análisis del código fuente importa más que las suposiciones.
Por qué MDI puede ver lo que los registros de Windows no pueden
Microsoft Defender for Identity añadió la detección 2437 en la versión 2.228 (febrero de 2024), específicamente para la enumeración mediante LDAP Ping. La documentación de MDI la describe como la detección de actividades de búsqueda LDAP desde sensores que se ejecutan en servidores de controladores de dominio.
"Sensores que se ejecutan en servidores de controladores de dominio" es la frase clave. El sensor de MDI se instala en el DC como un componente de captura de red y lee los paquetes directamente de la NIC, antes de que intervenga cualquier infraestructura de registro de eventos de Windows. Ve el tráfico de LDAP Ping tanto por TCP como por UDP, analiza la estructura binaria NETLOGON_SAM_LOGON_RESPONSE_EX y cuenta el volumen de respuestas LOGON_SAM_USER_UNKNOWN.
Por eso MDI puede detectar cLDAP sobre UDP real y el evento 5156 no. MDI opera a nivel de paquete, por debajo de la capa de seguimiento de conexiones de WFP que genera el 5156.
En el laboratorio, una ejecución de ldapnomnom con 25 nombres de usuario contra BADABING produjo exactamente lo que predice la arquitectura. Cero entradas del evento 1644 en el DC. Captura completa en netlogon.log. Atribución con el evento 5156 en cada conexión TCP. Y en MDI, una sola alerta de alta confianza que unió toda la historia: equipo de origen, DC objetivo, número de intentos y clasificación.
Figura 6: El ID externo 2437 de MDI activándose contra ldapnomnom en el laboratorio SOPRANOS y atribuyendo la enumeración a kali (10.0.1.100). La misma ejecución produjo cero entradas del evento 1644 en el DC.
El sensor de MDI leyó esto directamente de la red. No intervino ninguna infraestructura de registro de eventos. Por eso ve lo que el 5156 pasa por alto en UDP. El mismo punto de observación de captura de paquetes, otra capa de detección.
Fíjate en la redacción: "without successfully exposing any accounts". La lógica del 2437 de MDI cuenta los fallos, no los aciertos. Una ejecución con una lista de palabras seleccionada de cuentas mayormente reales produce menos respuestas 0x19 y puede quedarse por completo por debajo del umbral.
La limitación es el propio mecanismo de detección: el ID externo 2437 se activa cuando se acumulan "demasiados usuarios desconocidos" por encima de un umbral no revelado. El README de ldapnomnom documenta explícitamente la evasión:
// ldapnomnom README: evasion flags
--throttle 20 // 20ms delay between requests (slows to ~50/sec)
--maxrequests 1000 // Close and reconnect after 1000 requests
// Resets per-connection counters that MDI may trackUn atacante dirigido que use una lista seleccionada de 50 nombres contra objetivos conocidos de alto valor nunca se acerca a ningún umbral de volumen. El equipo de Synacktiv documentó en su investigación sobre MDI que ldapnomnom no produjo alertas de MDI en sus pruebas. La detección 2437 se añadió después de que se publicara esa investigación.
MDI: el ID externo 2437 lee los paquetes directamente de la NIC mediante la captura basada en Npcap del sensor. Ve el cLDAP sobre UDP que los registros de eventos de Windows pasan por alto por completo. Pero se basa en umbrales, requiere desplegar el sensor en el DC y el flag --throttle es una evasión documentada. Para las organizaciones sin MDI en cada DC, netlogon.log más el evento 5156 es la ruta de detección práctica.
Correlación de las dos fuentes
netlogon.log te da los nombres de usuario. El evento 5156 te da la IP de origen. Ninguno está completo por sí solo. Una ventana de correlación de marcas de tiempo de cinco segundos une de forma confiable las dos fuentes en todas las configuraciones probadas. El evento 5156 se activa en el momento de la conexión TCP, y la entrada de netlogon.log aparece en milisegundos, cuando Netlogon procesa la respuesta.
Pregunta | netlogon.log | Evento 5156 |
¿Se intentó una enumeración? | Sí | Sí (ráfaga de conexiones) |
¿Qué nombres de usuario se sondearon? | Sí, todos | No |
¿Qué cuentas están habilitadas? | Sí | No |
¿Qué cuentas están deshabilitadas? | Sí, más de lo que ve el atacante | No |
¿Cuándo ocurrió? | Sí | Sí |
¿Quién lo hizo (IP de origen)? | No, (null) | Sí, IP + puerto completos |
¿Detección en el puerto 636 (TLS)? | Sí | Sí |
¿cLDAP sobre UDP real? | Sí | No |
Figura 7: Salida de Get-LDAPPingDetection.ps1 durante una ejecución de ldapnomnom en el laboratorio SOPRANOS. El script correlaciona los nombres de usuario de netlogon.log con las IP de origen del evento 5156 en una ventana de cinco segundos y luego escala ante aciertos en honeytokens, volumen de ráfagas o sondeos a cuentas deshabilitadas. La IP de origen (10.0.1.100) se atribuyó a los pocos segundos de iniciarse el ataque.
Detección: qué es gratis, qué es barato y qué vale la pena construir
El panorama completo de detección para ldapnomnom y cLDAP sobre UDP real: qué se activa, qué no y cuánto cuesta habilitarlo:
La matriz de detección completa
Método de detección | ldapnomnom (TCP) | cLDAP sobre UDP real | ¿Confirmado? | Configuración necesaria |
Evento 1644 | No, nunca se activa | No, nunca se activa | Confirmado en laboratorio | N/A |
Evento 5156 (puerto 389) | Sí, IP de origen completa | No, no se activa | Confirmado en laboratorio | Audit FPC: habilitar |
Evento 5156 (puerto 636) | Sí, IP de origen completa | N/A (solo TCP) | Confirmado en laboratorio | Audit FPC: habilitar |
Nombres de usuario en netlogon.log | Sí, cada consulta | Sí, cada consulta | Confirmado en laboratorio | nltest /dbflag:0x2080ffff |
IP de origen en netlogon.log | No, muestra (null) | No, muestra (null) | Confirmado en laboratorio | No disponible en esta fuente |
Evento 5807 | Solo subred no asignada | Solo subred no asignada | Confirmado en laboratorio | Ninguna |
ID externo 2437 de MDI | Sí (umbral de volumen) | Sí (umbral de volumen) | Confirmado en laboratorio | Despliegue del sensor de MDI |
Forma del filtro: conjunción NtVer+AAC+User | Sí, alta fidelidad | Sí, alta fidelidad | Documentado por Sapir (TROOPERS24) | Captura de paquetes / NDR / analizador LDAP de Zeek |
Cuentas honeytoken | Sí, si están en la lista de palabras | Sí, si están en la lista de palabras | Confirmado en laboratorio | Creación de cuentas + alertas |
IDS de red (firma UDP) | N/A (es TCP) | Si no está cifrado | Inferido | Infraestructura de IDS |
Los puntos ciegos que persisten
El panorama de detección descrito aquí no está completo.
El transporte UDP elimina la atribución con el evento 5156. Un atacante que use cldap_ping.py o cualquier herramienta que envíe paquetes UDP sin procesar al puerto 389 no generará eventos 5156. netlogon.log sigue registrando, pero la IP de origen requiere visibilidad en la capa de red: captura de paquetes, NDR o registros del firewall.
netlogon.log no se envía a ningún lado de forma predeterminada. Es un archivo plano en el DC. Sin un agente activo de envío de registros, todos estos datos quedan en disco sin monitorear.
El flag --throttle de ldapnomnom inserta un retraso entre solicitudes. Con --throttle 20 (20 ms entre solicitudes), el patrón de ráfaga desaparece y la detección basada en volumen falla. Las entradas individuales de netlogon.log siguen apareciendo, pero la señal queda al nivel del ruido.
El flag --maxrequests rota las conexiones, lo que podría reiniciar los contadores por conexión que MDI usa para la detección por umbral.
El ID externo 2437 de MDI se basa en umbrales. Una enumeración dirigida contra una lista seleccionada de 50 cuentas conocidas de alto valor puede que nunca alcance el umbral. MDI también requiere desplegar el sensor en cada DC que quieras cubrir.
El evento 5156 es el regalo que sigue dando
Es la segunda vez en esta serie que el evento 5156 resuelve un problema que parecía irresoluble desde la perspectiva de los registros de eventos de Windows.
En la parte 5B, el problema era la atribución de ADWS. El evento 1644 mostraba localhost en todas las consultas de AD con PowerShell porque ADWS las traduce internamente antes de pasarlas a LSASS. El evento 5156 capturó la IP de origen real en el puerto de ADWS, y una ventana de correlación de marcas de tiempo de 60-80 ms unió las dos fuentes. En el Slack de BloodHound se decía que la atribución de la IP de origen no era realmente posible. Los datos dijeron lo contrario.
Aquí, el problema es que LDAP Ping evita por completo el motor LDAP. El evento 1644 es ciego ante él por diseño. Pero ldapnomnom usa TCP, y el evento 5156 ve cada conexión TCP a través de la Windows Filtering Platform, sin importar lo que la capa de aplicación haga con los datos. La misma técnica, otra superficie de ataque, el mismo resultado.
El patrón es constante: cuando una técnica está diseñada para evadir una fuente de registros específica, mira una capa más abajo en el stack de red. WFP ve todo lo que llega a LSASS por TCP, sin importar si la consulta llega al motor LDAP, al proxy ADWS o al controlador de Netlogon.
Patrón: el evento 5156 ha resuelto dos problemas distintos de "sin registros de auditoría de Windows" en esta serie. No es una fuente de detección que la mayoría de las organizaciones tenga en su SIEM. Debería serlo.
Arco 2 completado
Publicación | Punto ciego | Causa raíz | Solución |
Parte 5A | Enumeración de AD con PowerShell | ADWS (puerto 9389) hace de proxy de las consultas. El evento 1644 se activa, pero muestra localhost y oculta la IP de origen | Se documentó la arquitectura de ADWS y la brecha de IP de origen en el evento 1644 |
Parte 5B | IP de origen en ADWS | ADWS es un proxy TCP. LSASS nunca recibe la conexión original, solo una de loopback | Evento 5156 en el puerto 9389 + correlación de marcas de tiempo de 60-80 ms |
Parte 6 | Enumeración con LDAP Ping | Netlogon despacha LDAP Ping a un controlador independiente. Estructuralmente, el evento 1644 no puede activarse | netlogon.log + evento 5156 (TCP) correlacionados por marca de tiempo |
La lección de la parte 6: lee el código fuente antes de confiar en el README. "No se generan registros de auditoría de Windows" es una afirmación que merece comprobarse. El código fuente nos dijo que era TCP. Los registros de eventos confirmaron que el evento 5156 se activa. netlogon.log confirmó que se captura cada nombre de usuario. El README estaba equivocado de una forma que importa a los defensores.
→ La herramienta decía UDP. El código decía TCP. Los registros decían evento 5156. Sigue siempre el código.
En el arco 3, pasamos a la detección por comportamiento y por volumen. El atacante que cambia a cLDAP sobre UDP real, se mantiene por debajo del umbral de MDI y usa una lista de palabras dirigida sigue dejando una huella estadística.
Referencias
Código fuente de ldapnomnom: github.com/lkarlslund/ldapnomnom (Lars Karlslund), v1.5.1 / commit bdc7fae
Biblioteca lkarlslund/ldap: github.com/lkarlslund/ldap (fork de go-ldap/ldap)
RFC 1798: Connection-less Lightweight Directory Access Protocol
MS-ADTS § 6.3: DC Locator and LDAP Ping (Microsoft Open Specifications)
MS-ADTS § 6.3.3.2: Domain Controller Response to an LDAP Ping
MS-SAMR § 2.2.1.12: USER_ACCOUNT Codes (USER_NORMAL_ACCOUNT = 0x00000010, los valores de la máscara de bits AAC)
Notas de la versión v2.228 de MDI: Account enumeration reconnaissance (LDAP), ID externo 2437
MDI Account Enumeration Reconnaissance (LDAP), ID externo 2437: learn.microsoft.com/en-us/defender-for-identity/reconnaissance-discovery-alerts
Synacktiv: A dive into Microsoft Defender for Identity (documentación del punto ciego de MDI previo al 2437)
Sapir Federovsky, The (almost) complete LDAP guide, TROOPERS24, 2024.https://troopers.de/downloads/troopers24/TR24_The_(almost)_complete_LDAP_guide_KYNUWX.pdf
KB de Microsoft: Enable debug logging for the Netlogon service (nltest /dbflag)
Evento de seguridad de Windows 5156: The Windows Filtering Platform has permitted a connection
From Code to Coverage, partes 1–5B: huntress.com/blog
Agradecimientos
A Lars Karlslund, por crear ldapnomnom y publicarlo como código abierto. El código fuente fue el punto de partida para entender la superficie de detección.
Al equipo de Synacktiv, por su detallada investigación sobre MDI, que confirmó el punto ciego previo al 2437 y documentó que ldapnomnom no produjo alertas de MDI en sus pruebas.
A Sapir Federovsky, por la guía LDAP de TROOPERS24, que documentó la forma de filtro NtVer + AAC + User como firma de ldapnomnom e inspiró el enfoque de detección en la capa de contenido de esta publicación.