Direct naar inhoud

LDAP injection

CWE-90OWASP A03:2021Bijgewerkt 31 augustus 20266 min leestijd

LDAP injection is een kwetsbaarheid waarbij een aanvaller eigen filtertekens in een directoryzoekopdracht smokkelt, doordat de applicatie invoer en filter niet uit elkaar houdt. Met een payload als *)(uid=* omzeilt hij de inlogcontrole of leest hij de hele gebruikersadministratie uit. De oplossing is escaping volgens RFC 4515, gecombineerd met een echte LDAP-bind voor de wachtwoordcontrole.

LDAP injection is de directoryvariant van SQL injection: minder bekend, maar in bedrijfsnetwerken vaak dichter bij de kroonjuwelen. Een applicatie die haar zoekfilter opbouwt door tekst uit een invoerveld aan elkaar te plakken, geeft de aanvaller zeggenschap over de vraag die aan de gebruikersadministratie wordt gesteld. Hieronder leest u wat de kwetsbaarheid inhoudt, hoe een aanval verloopt en hoe u haar structureel afsluit.

Wat is LDAP injection?

LDAP injection is een kwetsbaarheid waarbij een aanvaller eigen filtertekens in een directoryzoekopdracht van uw applicatie weet te smokkelen. LDAP staat voor Lightweight Directory Access Protocol: het protocol waarmee applicaties een directory zoals Active Directory of OpenLDAP bevragen om gebruikers, groepen en rechten op te zoeken. Zodra invoer van de gebruiker ongefilterd in zo’n zoekfilter belandt, bepaalt de bezoeker mee wat er precies gevraagd wordt.

Een alledaagse vergelijking: u geeft de receptionist een briefje met de opdracht om het dossier van medewerker Jansen op te halen, en onderweg schrijft iemand er “of eigenlijk van iedereen” achter. De receptionist leest de zin voor wat hij is en levert de hele kaartenbak af. De directory doet hetzelfde: zij voert het filter uit dat zij krijgt, zonder te weten welk deel van u kwam en welk deel van de bezoeker.

Gevoelig maakt LDAP de filtersyntaxis zelf. Een zoekfilter bestaat uit haakjes, logische operatoren zoals &, | en !, vergelijkingstekens en de wildcard *. Dat zijn gewone leestekens die iemand ongehinderd in een naam of een afdelingsnaam kan typen. Wie ze niet neutraliseert, laat de gebruiker meeschrijven aan de logica van de vraag in plaats van alleen aan de waarde.

Hoe werkt een LDAP injection-aanval?

Het probleem ontstaat op de klassieke manier: het filter wordt als tekst samengesteld. Neem een inlogformulier dat gebruikersnaam en wachtwoord samen in één filter zet en aanneemt dat een treffer gelijkstaat aan een geslaagde aanmelding.

Kwetsbaar:

// Invoer wordt rechtstreeks in het LDAP-filter geplakt
const username = req.body.username;
const password = req.body.password;

const filter =
  "(&(uid=" + username + ")(userPassword=" + password + "))";

const treffers = await zoek(client, "ou=users,dc=voorbeeld,dc=nl", filter);
if (treffers.length > 0) {
  // toegang verleend
}

Vult een gewone medewerker zijn naam en wachtwoord in, dan werkt dit naar behoren. Een aanvaller typt in het gebruikersnaamveld echter geen naam, maar *)(uid=*))(|(uid=*, met een willekeurig wachtwoord ernaast. Het filter dat de directory uiteindelijk te verwerken krijgt, wordt dan:

(&(uid=*)(uid=*))(|(uid=*)(userPassword=willekeurig))

De aanvaller heeft de haakjes van uw filter voortijdig gesloten en er een eigen constructie omheen gebouwd. Vooraan staat nu (&(uid=*)(uid=*)), een voorwaarde die waar is voor elk object met een uid. De wachtwoordcontrole is verhuisd naar de tekst achter dat eerste volledige filter. Veel directoryservers verwerken alleen dat eerste filter en negeren de rest, andere weigeren de opdracht. Geldt het eerste, dan is de wachtwoordcontrole simpelweg verdwenen: de zoekopdracht levert treffers op en de aanvaller is binnen, meestal als de eerste gebruiker in de resultaatlijst.

Ook zonder volledige omzeiling is zo’n lek waardevol. Eén sterretje verandert (uid=jansen) in (uid=*) en levert in één klap de complete gebruikerslijst op. Met een filter als (&(uid=beheerder)(description=a*)) raadt een aanvaller bovendien attribuutwaarden teken voor teken, zolang de applicatie zichtbaar anders reageert op een treffer dan op nul resultaten.

De structurele oplossing bestaat uit twee delen. Ten eerste escapet u elke waarde die in een filter terechtkomt volgens RFC 4515, de standaard die voorschrijft hoe de tekens *, (, ), de backslash en het nulbyte in een filterwaarde worden gecodeerd. Ten tweede controleert u het wachtwoord niet met een zoekopdracht, maar met een bind: u zoekt eerst de DN (distinguished name) van de gebruiker op met een serviceaccount en laat de directory daarna zelf beoordelen of het opgegeven wachtwoord bij die DN hoort.

Veilig:

// Elke waarde die in een filter belandt, wordt volgens RFC 4515 ge-escaped
function escapeFilterValue(value) {
  return String(value)
    .replace(/\\/g, "\\5c")
    .replace(/\*/g, "\\2a")
    .replace(/\(/g, "\\28")
    .replace(/\)/g, "\\29")
    .replace(/\0/g, "\\00");
}

// 1. Zoeken met een serviceaccount, uitsluitend om de DN op te halen
await client.bind(SERVICE_DN, SERVICE_WACHTWOORD);

const filter =
  "(&(objectClass=person)(uid=" + escapeFilterValue(req.body.username) + "))";

const treffers = await zoek(client, "ou=users,dc=voorbeeld,dc=nl", filter);
if (treffers.length !== 1) {
  throw new Error("Aanmelden mislukt");
}

// 2. De directory zelf het wachtwoord laten controleren
const password = req.body.password;
if (password.length === 0) {
  throw new Error("Aanmelden mislukt");
}

const gebruikerClient = maakClient();
await gebruikerClient.bind(treffers[0].dn, password); // faalt bij een fout wachtwoord

Nu is *)(uid=*))(|(uid=* een merkwaardige gebruikersnaam die op geen enkel account past: de haakjes en sterretjes belanden als \28, \29 en \2a in het filter en laten de structuur intact. Het wachtwoord verlaat de applicatie bovendien nooit meer als onderdeel van een zoekopdracht.

Weiger een leeg wachtwoord expliciet voordat u bindt. Veel directoryservers vatten een bind zonder wachtwoord op als een anonieme bind en melden dan alsnog succes, waardoor uw inlogcontrole met een leeg veld te omzeilen is.

Wat is de impact van LDAP injection?

De ernst hangt af van wat het serviceaccount mag en van wat de applicatie met het resultaat doet. Aan de onderkant van het bereik staat informatielekkage: met één wildcard haalt een aanvaller de volledige personeelslijst op, inclusief e-mailadressen, telefoonnummers en organisatiestructuur. Dat is op zichzelf een privacyincident en levert daarnaast uitstekend materiaal voor gerichte phishing.

Aan de bovenkant staat de authenticatie-omzeiling uit het voorbeeld. Wie zonder geldige inloggegevens binnenkomt, erft alle rechten van het account waarop de applicatie uitkomt, en dat is bij een filter zonder sortering vaak willekeurig of juist het eerste beheerdersaccount. Ook filters op groepslidmaatschap zijn te manipuleren, en gevoelige attributen komen binnen bereik, tot wachtwoordhashes toe als het serviceaccount die mag lezen.

Zeldzamer, maar niet uitgesloten, is een applicatie die ook wijzigingen doorvoert op een DN die de gebruiker beïnvloedt; dan verschuift het risico van uitlezen naar aanpassen. Omdat het bereik loopt van een uitgelezen adreslijst tot overgenomen accounts, bewegen wij de ernst tussen medium en hoog.

Hoe spoort u LDAP injection op?

De snelste test is een enkel sterretje. Voer in elk veld dat een directory bevraagt de waarde * in en kijk of het aantal resultaten opvallend toeneemt of dat u accounts ziet die u niet mag zien. Voer daarna een enkel open haakje in. Een foutmelding over een ongeldig zoekfilter, een lege pagina of een HTTP 500 verraadt dat uw invoer de filtersyntaxis bereikt.

# Zelfde endpoint, twee zoekopdrachten: een naam en een wildcard
curl -s "https://app.voorbeeld.nl/zoek?naam=jansen" | wc -l
curl -s "https://app.voorbeeld.nl/zoek?naam=%2A" | wc -l

Reageert de applicatie zonder foutmelding, dan werkt een tester blind verder: hij vergelijkt de respons op een voorwaarde die waar is met die op een voorwaarde die zeker onwaar is. Geautomatiseerde scanners vinden de opvallende gevallen, maar missen deze kwetsbaarheid regelmatig, omdat LDAP-fouten in de applicatielaag worden weggevangen en het verschil alleen in het gedrag zichtbaar is. Handmatig onderzoek is hier doorslaggevend, en het is precies het soort zwakke plek dat AssistSec bij een penetratietest gericht opspoort en reproduceerbaar aantoont.

Hoe voorkomt u LDAP injection?

  • Escape elke filterwaarde volgens RFC 4515 met de standaardfunctie van uw LDAP-bibliotheek en schrijf daar geen eigen variant voor.
  • Controleer wachtwoorden altijd met een bind, nooit door het wachtwoord in een zoekfilter te vergelijken. Zoek de DN op, bind daarna als die gebruiker.
  • Bouw DN’s niet zelf op. Moet het toch, gebruik dan de escaperegels van RFC 4514, die andere bijzondere tekens kennen, waaronder de komma en het gelijkteken.
  • Valideer invoer tegen een strikte allowlist. Een gebruikersnaam of afdelingscode heeft een voorspelbaar formaat; wijs alles af wat daar niet aan voldoet.
  • Geef het serviceaccount minimale rechten. Beperk de search base en de attributen die worden teruggegeven.
  • Stel een limiet in op het aantal zoekresultaten en log de zoekopdrachten die daar tegenaan lopen; een wildcardaanval valt dan direct op.
  • Toon nooit ruwe LDAP-foutmeldingen aan de gebruiker, maar log ze intern; ze verraden precies hoe uw filter is opgebouwd.
  • Laat inlog- en zoekfunctionaliteit periodiek testen met een penetratietest en een code review; juist deze kwetsbaarheid overleeft geautomatiseerde scans.

Bronnen

Veelgestelde vragen

Wat is het verschil tussen LDAP injection en SQL injection?

De oorzaak is identiek: invoer die als onderdeel van een opdracht wordt geïnterpreteerd in plaats van als waarde. Het doelwit verschilt. SQL injection richt zich op de database, LDAP injection op de directory die uw gebruikers, groepen en rechten bijhoudt. Omdat die directory vaak de centrale identiteitsbron van de organisatie is, raakt een geslaagde LDAP injection meteen de authenticatie.

Komt LDAP injection nog voor in moderne applicaties?

Ja. Veel intranetportalen, VPN-koppelingen, printersystemen en oudere webapplicaties zoeken nog rechtstreeks in Active Directory of OpenLDAP. De kwetsbaarheid duikt vooral op in zelfgebouwde zoek- en inlogschermen waar het filter met stringconcatenatie wordt opgebouwd.

Is invoer escapen genoeg tegen LDAP injection?

Escaping volgens RFC 4515 sluit de aanval op het zoekfilter af en is de belangrijkste maatregel. Zij beschermt echter niet tegen een inlogcontrole die het wachtwoord vergelijkt binnen een filter, en niet tegen een DN die u zelf samenstelt: daarvoor gelden de escaperegels van RFC 4514. Combineer escaping daarom altijd met een bind voor de wachtwoordcontrole.

Hoe test ik of mijn applicatie kwetsbaar is voor LDAP injection?

Voer in elk veld dat een directory bevraagt een enkel sterretje in en kijk of het aantal resultaten opvallend toeneemt. Voer daarna een enkel open haakje in: een foutmelding of een lege pagina wijst erop dat uw invoer de filtersyntaxis bereikt. Verifieer een vermoeden altijd met een gecontroleerde payload in een testomgeving.

Verwante artikelen

Druk op / om te zoeken · Esc