De código a cobertura (Parte 6): lo que netlogon.log ve que el evento 1644 jamás verá

Puntos clave

  • El evento 1644 no es la respuesta para la detección de LDAP Ping porque el LDAP Ping verdadero nunca llega al motor LDAP. Se gestiona a través de Netlogon en una ruta de código separada, por lo que el 1644 es estructuralmente ciego ante él.

  • La gran corrección en esta publicación es que ldapnomnom no es tan invisible como afirma su README. El código muestra que usa TCP de forma predeterminada, lo que significa que el evento 5156 puede capturar la IP de origen real, mientras que netlogon.log registra cada nombre de usuario que se prueba.

  • El verdadero punto ciego es el cLDAP UDP verdadero. En ese caso, netlogon.log sigue mostrando lo que se enumeró, pero los registros de eventos de Windows no dan atribución, así que los defensores necesitan captura de paquetes, NDR, registros de firewall o MDI para averiguar quién lo envió.

  • La estrategia práctica de detección es la correlación. Para la actividad de LDAP Ping basada en TCP, netlogon.log da a los defensores los nombres de usuario y el evento 5156 da la IP de origen, mientras que MDI puede ver tanto la actividad TCP como UDP, pero sigue dependiendo de la cobertura de sensores y la detección basada en umbrales.

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

\x06\x00\x00\x00

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

\x10\x00\x00\x00

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

<username>

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:     65536

La 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.txt

Si 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 track

Un 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.

También te podría interesar

Más información sobre Cerrando la brecha: Managed ITDR ahora admite la deshabilitación de identidades para identidades sincronizadas con Active Directory
Cerrando la brecha: Managed ITDR ahora admite la deshabilitación de identidades para identidades sincronizadas con Active Directory
Blog

Una de las funciones más solicitadas de Huntress Managed ITDR ya está disponible: la deshabilitación de identidades para identidades sincronizadas con Active Directory. Esto es lo que significa para tu flujo de contención y respuesta.

Más información sobre CMMC: la oportunidad ($$) y el desafío para los MSP
CMMC: la oportunidad ($$) y el desafío para los MSP
Blog

CMMC no es solo otra casilla de cumplimiento: es un cambio de mercado que dividirá a los MSP entre ganadores y perdedores. Si el mandato de EMR te enseñó algo, es que quienes se mueven primero ganan.

Más información sobre Resiliencia operativa: cómo convertir tu plan de respuesta a incidentes en memoria muscular
Resiliencia operativa: cómo convertir tu plan de respuesta a incidentes en memoria muscular
Blog

Un plan de respuesta a incidentes solo funciona si se pone a prueba. Conoce los 5 pilares de la resiliencia operativa y cómo convertir tu plan en memoria muscular antes de que ocurra un ataque.