¿Recuerdas “De código a cobertura: Parte 1”? ¿Esa publicación a fondo donde pasamos todo ese tiempo descubriendo que las herramientas de Impacket usan operaciones bit a bit en lugar de valores OID? ¿Donde finalmente celebramos entender que findDelegation.py busca userAccountControl&524288?
Sí, bueno... tengo malas noticias para ti.
Esa fue la parte fácil.
Verás, pensé que ya había terminado. Tenía mi lógica de detección resuelta. Entendía las herramientas. Sabía exactamente qué buscar. Desplegué mis reglas a producción con la confianza de alguien que acababa de resolver un acertijo muy difícil. Luego recibí un mensaje de Slack a las 2 a. m.: "Oye, definitivamente vimos actividad de Impacket durante el ejercicio de red team, pero tus reglas no se activaron."
Ahí fue cuando aprendí que entender QUÉ detectar (Parte 1) es completamente diferente de hacer que realmente FUNCIONE en producción (bienvenido a la Parte 2).
Consigue un café. O tal vez algo más fuerte. Esto se va a poner frustrante.
Entendiendo la transformación: Por qué existe este desastre
Antes de sumergirnos en la pesadilla, déjame mostrarte POR QUÉ pasa esto. Aquí está el flujo real de cómo se transforman tus consultas LDAP mientras viajan a través de Active Directory (basado en mi comprensión a partir de la inmersión a fondo de la Parte 1 sobre cómo Active Directory procesa estas consultas):
Figura 1: El flujo de transformación de OID a bit a bit - Donde la sintaxis OID limpia muere y emerge como caos de espacios en blanco
Ideas clave de detección a partir de este flujo:
- Las herramientas envían sintaxis OID → AD la transforma antes de registrarla → esto prueba el comportamiento de la herramienta
- Active Directory (AD) transforma OID a bit a bit → el evento 1644 registra esto → muestra la ejecución
- Las reglas de detección deben apuntar a operaciones bit a bit (&), no a OID
- EL PUNTO CRÍTICO: ¡La transformación en la capa de servicio LDAP no normaliza los espacios en blanco!
El problema: Tus registros te están mintiendo (más o menos)
Ahora que entiendes DÓNDE ocurre la transformación, aquí está el detalle: ¿esa transformación de la capa de servicio LDAP? No le importa en absoluto el formato consistente. El analizador sintáctico de filtros solo se preocupa de que tu consulta tenga sentido semántico; los espacios en blanco son irrelevantes para la operación bit a bit, así que se conservan (o se agregan, o se eliminan) según un procesamiento interno misterioso.
Esto es lo que me volvió absolutamente loco durante semanas. Tenía reglas que apuntaban a userAccountControl&524288, y funcionaban... a veces. Como, el 30% de las veces. Lo cual es peor que no funcionar en absoluto, porque al menos ahí SABES que algo está roto.
¿El problema? El evento 1644 no registra los filtros LDAP con espacios en blanco consistentes. Y cuando digo "no", quiero decir que es completamente aleatorio. He visto todas estas variaciones en entornos reales:
(userAccountControl&524288)
(userAccountControl & 524288)
( userAccountControl&524288 )
( userAccountControl & 524288 )
(userAccountControl& 524288)
( userAccountControl &524288)
Mira ese desastre. MÍRALO. Es exactamente la misma consulta, de seis maneras diferentes.
¿Por qué pasa esto? Mira de nuevo nuestro diagrama de flujo: la transformación ocurre en la capa de servicio LDAP, pero el espaciado exacto depende de:
- Cómo formateó la consulta original la herramienta cliente
- Las rutinas internas de procesamiento de cadenas en el motor de AD (que, por cierto, Microsoft no ha documentado)
- El formato y la serialización del registro de eventos (también sin documentar, gracias Microsoft)
- Si la consulta pasó por pasos de procesamiento adicionales (adivinaste: sin documentar)
Así que, básicamente, es un caos. Un caos total ocurriendo justo ahí, en ese recuadro verde de la capa de servicio LDAP.
Mi recorrido por las cinco etapas del duelo (edición ingeniería de detección)
Déjame mostrarte cómo evolucionaron mis reglas mientras perdía lentamente la cabeza con este problema.
Etapa 1: Negación ("Mis reglas son perfectas")
selection:
winlog.event_data.LDAPFilter|contains: 'userAccountControl&524288'
Esto funcionó en mi laboratorio, donde usaba la misma herramienta de la misma manera cada vez. "¡A producción!", dije. "¡Es infalible!", dije.
30% de aciertos en producción.
Pasé tres días pensando que nuestro SIEM estaba roto. Spoiler: no lo estaba. La capa de servicio LDAP simplemente se estaba riendo de mis suposiciones.
Etapa 2: Enojo ("Voy a enumerar todas las posibilidades")
selection:
winlog.event_data.LDAPFilter|contains|any:
- 'userAccountControl&524288'
- 'userAccountControl & 524288'
- ' userAccountControl&524288'
- 'userAccountControl&524288 '
Mejor, pero aún se me escapaban casos límite. Además, esto se estaba volviendo ridículo rápidamente. Básicamente estaba tratando de enumerar cada combinación de espaciado posible que pudiera surgir de esa capa de transformación, lo cual es como tratar de enumerar cada forma en que alguien podría estornudar. Es imposible, y te ves tonto intentándolo.
Etapa 3: Negociación ("Quizás Regex me salve")
selection:
winlog.event_data.LDAPFilter|re: '.*userAccountControl\s*&\s*524288.*'
¡Mucho mejor! El \s* coincide con cero o más caracteres de espacio en blanco, así que maneja la mayoría de las variaciones de espaciado. Pensé que era astuto. El universo (y la capa de servicio LDAP) se rieron de mí. Seguía perdiendo coincidencias porque no estaba considerando el espaciado en los paréntesis.
Etapa 4: Depresión ("¿Por qué elegí esta carrera?")
Al fin y al cabo, conseguí un certificado de Gelato University, pensando que estaba escapando de este caos. Resulta que los filtros LDAP son más difíciles de dominar que el stracciatella.
Etapa 5: Aceptación ("Bien, me encargo de todo")
selection:
winlog.event_data.LDAPFilter|re: '.*\(\s*userAccountControl\s*&\s*524288\s*\).*'
Ahora también estamos considerando los espacios alrededor de los paréntesis. Esto finalmente me dio una tasa de aciertos consistente en producción. Solo me tomó tres semanas y probablemente unos cuantos años de vida.
De la teoría de la Parte 1 a la realidad de la Parte 2: Una comparación
¿Recuerdas lo confiados que estábamos al final de la Parte 1? Veamos cómo nos fue:
|
Lo que aprendimos en la Parte 1 |
Lo que la Parte 2 nos reveló de golpe |
|
GetUserSPNs.py usa userAccountControl&2 (en el filtro de exclusión !(userAccountControl&2)) |
Puede aparecer como userAccountControl & 2 o de otras veinte formas distintas |
|
findDelegation.py usa &16777216 y &524288 |
Puede aparecer con espaciados aleatorios que cambian según la fase de la luna |
|
Los OID se transforman en operaciones bit a bit |
Esas operaciones tienen infinitas variaciones de formato después de la transformación |
|
El evento 1644 muestra el filtro LDAP |
Pero no normaliza el formato, porque eso sería demasiado útil |
|
La transformación ocurre en la capa de servicio LDAP |
Esa capa no tiene ninguna documentación sobre el manejo de espacios en blanco |
Cómo crear reglas Sigma que realmente funcionen (y no te hagan llorar)
Después de años de sufrimiento, aquí están los patrones que realmente funcionan en producción:
Para operaciones bit a bit simples:
# Maneja: userAccountControl&524288 con cualquier espaciado
# Esto es lo que la Parte 1 nos enseñó a buscar; la Parte 2 hace que realmente funcione
selection:
winlog.event_data.LDAPFilter|re: '.*userAccountControl\s*&\s*524288.*'
Para filtros complejos con múltiples condiciones:
¿Recuerdas findDelegation.py de la Parte 1? Así es como se le atrapa realmente después de que pasa por la transformación:
# Detección de findDelegation.py con tolerancia de espaciado
# Parte 1: "Busca estas marcas"
# Parte 2: "Busca estas marcas en 47 formatos diferentes"
selection:
winlog.event_data.LDAPFilter|re: '.*userAccountControl\s*&\s*16777216.*'
winlog.event_data.LDAPFilter|re: '.*userAccountControl\s*&\s*524288.*'
condition: all of selection
Para listas de atributos con problemas de espaciado:
# Maneja el espaciado en listas de atributos
# Porque claro, las listas de atributos también tienen problemas de espaciado
selection:
winlog.event_data.AttributeList|re: '.*msDS-PrincipalName\s*,\s*objectSid.*'
Para filtros de negación:
# Maneja: !(userAccountControl&2) con espaciado
# Sí, incluso el signo de exclamación puede tener espacios después
# Ojalá estuviera bromeando
selection:
winlog.event_data.LDAPFilter|re: '.*!\s*\(\s*userAccountControl\s*&\s*2\s*\).*'
Patrones avanzados para cuando el Regex básico no alcanza
Sensibilidad a mayúsculas (porque, ¿por qué alguien sería consistente?):
He visto userAccountControl, UserAccountControl e incluso useraccountcontrol. En el mismo entorno. El mismo día. A la capa de servicio LDAP tampoco le importan las mayúsculas.
# Detección bit a bit sin distinción entre mayúsculas y minúsculas
# Para cuando los administradores no logran ponerse de acuerdo con las mayúsculas
selection:
winlog.event_data.LDAPFilter|re: '(?i).*useraccountcontrol\s*&\s*524288.*'
Múltiples operadores (cuando "&" no es el único problema):
Algunas herramientas usan OR bit a bit en lugar de AND, porque la estandarización es para los que se rinden:
# Captura tanto operaciones con & como con |
selection:
winlog.event_data.LDAPFilter|re: '.*userAccountControl\s*[&|]\s*524288.*'
Cómo probar tu Regex (o: cómo aprendí a dejar de confiar y a verificarlo todo)
Este es mi proceso probado en batalla:
1. Recolecta datos reales (no datos de laboratorio)
Los datos de laboratorio son una mentira. Son demasiado limpios. Demasiado consistentes. Ejecuta tus herramientas objetivo en producción (¡con cuidado!) y recolecta registros reales del evento 1644. Observa cómo pasan por esa capa de transformación en tiempo real.
2. Crea un archivo de prueba "Muro de la vergüenza"
Literalmente tengo un archivo llamado whitespace_hell.txt con todas las variaciones que he visto en mi vida:
(userAccountControl&524288)
(userAccountControl & 524288)
( userAccountControl&524288 )
( userAccountControl & 524288 )
(userAccountControl& 524288)
( userAccountControl &524288)
3. Pon tu Regex a prueba contra el muro
Usa regex101.com o la herramienta que prefieras. Si tu patrón no coincide con cada línea de tu Muro de la vergüenza, no está listo para producción.
4. Despliega con registro paranoico
Habilita el registro detallado para que puedas ver qué estás detectando y qué se te está escapando. Algo se te va a escapar. Acéptalo desde ahora.
Errores comunes (aprende de mi sufrimiento)
Error 1: "Esto funcionó en el laboratorio".
# Demasiado específico: se perderá el 70 % de las coincidencias legítimas
selection:
winlog.event_data.LDAPFilter|contains: '(userAccountControl&524288)'
Solución: Usa siempre regex con tolerancia a espacios en blanco. Siempre.
Error 2: "Voy a tender una red amplia".
# Demasiado amplio: coincidirá con la lista del supermercado de tu abuela
selection:
winlog.event_data.LDAPFilter|re: '.*524288.*'
Solución: Incluye el nombre del atributo y el operador. El contexto importa.
Error 3: "Sé cómo funciona Regex".
# Incorrecto: los paréntesis tienen un significado especial en regex
selection:
winlog.event_data.LDAPFilter|re: '(userAccountControl&524288)'
# Correcto: escapa los paréntesis o llora después
selection:
winlog.event_data.LDAPFilter|re: '\(userAccountControl&524288\)'
Dificultad 4: "Windows no distingue mayúsculas de minúsculas, ¿verdad?"
A veces. Tal vez. Depende. La capa de servicio LDAP podría conservar las mayúsculas, o no. Simplemente haz que todo sea insensible a mayúsculas y sigue con tu vida.
Dificultad 5: "Solo voy a buscar el OID."
# ESTO NUNCA VA A FUNCIONAR
selection:
winlog.event_data.LDAPFilter|contains: '1.2.840.113556.1.4.803'
¡Mira el diagrama de flujo! El OID se transforma en & en la capa de servicio LDAP ANTES de llegar al evento 1644. ¡Estás buscando algo que no existe en estos registros!
Rendimiento (porque tu administrador del SIEM ya está molesto contigo)
Los patrones de regex pueden matar el rendimiento de tu SIEM. Así es como evitas que te despidan:
Prefiltra antes del regex:
# Coincidencia de cadena rápida primero, regex costoso después
selection_prefilter:
winlog.event_data.LDAPFilter|contains: 'userAccountControl'
selection_regex:
winlog.event_data.LDAPFilter|re: '.*userAccountControl\s*&\s*524288.*'
condition: selection_prefilter and selection_regex
Tu administrador del SIEM te lo agradecerá. O al menos dejará de mirarte feo en las reuniones.
La regla real de producción (probada en batalla y bañada en lágrimas)
Después de todo este sufrimiento, aquí está la regla Sigma real que uso para findDelegation.py. Nota cómo maneja múltiples variaciones de espaciado: esto no es paranoia, es experiencia:
Nota: Lo siguiente son extractos de lógica de detección que muestran los criterios clave de selección. Los nombres de campo usan Elastic Common Schema (ECS). Ajústalo según tu SIEM. El filtro de origen coincide con conexiones remotas de IP:puerto, filtrando las operaciones locales del DC.
detection:
selection_event:
event.code: 1644
selection_attrs:
winlog.event_data.AttributesRequested|contains|all:
- sAMAccountName
- pwdLastSet
- userAccountControl
- objectCategory
- msDS-AllowedToActOnBehalfOfOtherIdentity
- msDS-AllowedToDelegateTo
selection_delegation_attrs:
winlog.event_data.LDAPFilter|contains|all:
- (msDS-AllowedToDelegateTo=*)
- (msDS-AllowedToActOnBehalfOfOtherIdentity=*)
selection_excl_8192:
winlog.event_data.LDAPFilter|contains:
- (!(userAccountControl&8192))
- ( ! (userAccountControl&8192) )
- ( ! (userAccountControl&8192) )
selection_excl_disabled:
winlog.event_data.LDAPFilter|contains:
- (!(userAccountControl&2))
- ( ! (userAccountControl&2) )
- ( ! (userAccountControl&2) )
selection_src:
winlog.event_data.Source|re: '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}$'
selection_uac_bits:
winlog.event_data.LDAPFilter|contains|all:
- userAccountControl&16777216
- userAccountControl&524288
condition: selection_event and selection_uac_bits and selection_delegation_attrs and selection_excl_disabled and selection_excl_8192 and selection_attrs and selection_src
¡Mira esos casos de prueba! El primero no tiene espacios. El segundo tiene espacios en lugares donde ni sabía que podía HABER espacios. Por esto necesitamos múltiples variaciones en nuestra lógica de detección.
Lecciones aprendidas (de la manera difícil)
- Prueba con datos de producción, no con datos de laboratorio. Tu laboratorio es una hermosa mentira. La producción es la fea verdad.
-
No asumas nada sobre el formato de los registros. En serio. Nada. ¿Ese espacio con el que cuentas? No estará ahí cuando lo necesites. A la capa de servicio LDAP no le importan tus suposiciones.
-
Empieza con patrones flexibles. Puedes ajustarlos después, cuando entiendas el caos específico de TU entorno.
-
Monitorea tus reglas de forma obsesiva. Esa regla que funcionó durante seis meses dejará de funcionar al azar un martes.
-
Documenta todo. Cuando escribas un patrón de regex a las 2 a. m. después de tu cuarta bebida energética, documenta qué estabas tratando de manejar. Tu yo futuro le agradecerá a tu yo pasado.
-
Hazte amigo de tu administrador del SIEM. Vas a romper cosas. Tener un amigo ayuda.
- Entiende el flujo de transformación. Sabe DE DÓNDE vienen tus registros y QUÉ les pasa en el camino. La transformación de OID a bitwise es solo un ejemplo de las muchas transformaciones que pueden afectar tu lógica de detección.
Reflexiones finales: la prueba de realidad de la ingeniería de detección
En la parte 1, aprendimos a pensar como un atacante, entendiendo cómo las herramientas de Impacket construyen sus consultas LDAP. Nos sentimos inteligentes. Nos sentimos realizados.
En la parte 2, aprendimos a pensar como un analizador sintáctico de registros en plena crisis existencial, manejando cada variación posible que esas consultas podrían tomar después de pasar por el calvario de la transformación. Nos sentimos cansados. Nos sentimos humildes.
El flujo de transformación de OID a bitwise nos muestra exactamente DÓNDE las cosas salen mal: esa capa de servicio LDAP es donde nuestra sintaxis OID limpia y predecible se transforma en el caos de espacios en blanco que vemos en el evento 1644. Entender este flujo importa porque:
- Explica por qué NUNCA encontrarás OID en el evento 1644 (ya están transformados)
- Muestra por qué los espacios en blanco son tan inconsistentes (la transformación no los normaliza)
- Demuestra por qué probar en producción es esencial (no puedes predecir las variaciones de la transformación)
Por ahora, tómate un descanso. Actualiza tus reglas para manejar los espacios en blanco. Pruébalas contra tu Muro de la Vergüenza. Y recuerda, todo ingeniero de detección sénior ha pasado por este mismo proceso. Todos estamos juntos en esta pesadilla de espacios en blanco.
Y si alguien te dice que sus reglas de detección de LDAP funcionan perfectamente en producción al primer intento, está mintiendo. O no ha revisado sus tasas de detección. O no ha analizado el flujo de transformación. Probablemente las tres cosas.
Mantente firme, amigos. Los registros son un desastre, pero les encontraremos sentido juntos.
p.d. Si te preguntas por qué Microsoft simplemente no normaliza los espacios en blanco en la capa de servicio LDAP antes de registrar los eventos... bienvenido al club. Yo básicamente solo lloro.
Gracias a Jonathan Johnson, Charlie Clark, Anton Ovrutsky, Matt Anderson, Tyler Bohlmann, Lindsey O'Donnell-Welch, Michael Haag, Nasreddine Bencherchali, y Adam Bienvenu por su ayuda al revisar esta publicación.