Direct naar inhoud

Achterhalen van geldige gebruikersnamen

CWE-204CWE-203OWASP A07:2021Bijgewerkt 3 september 20265 min leestijd

Wanneer een applicatie anders reageert op een bestaand dan op een niet-bestaand account, kan een aanvaller vaststellen welke gebruikersnamen geldig zijn. Dat verschil zit niet alleen in de foutmelding, maar ook in statuscodes, doorverwijzingen en responstijden. Met een geldige lijst wordt elke vervolgaanval gerichter en goedkoper.

Een inlogscherm dat behulpzaam meldt dat het wachtwoord onjuist is, vertelt daarmee ook dat de gebruikersnaam klopt. Dat is prettig voor wie zich vergist en waardevol voor wie een lijst aan het opbouwen is. In dit artikel leest u langs welke wegen dat verschil zichtbaar wordt, meestal niet alleen via de foutmelding, en hoe u de antwoorden gelijktrekt zonder uw gebruikers in het ongewisse te laten.

Wat is username enumeration?

Username enumeration, in het Nederlands het achterhalen van geldige gebruikersnamen, is het vaststellen welke accounts in een systeem bestaan door het gedrag van de applicatie te vergelijken. De aanvaller heeft geen wachtwoord nodig en hoeft nergens in te breken; hij leidt de informatie af uit het verschil tussen twee antwoorden.

Dat verschil hoeft niet in tekst te zitten. Het kan een afwijkende HTTP-statuscode zijn, een andere doorverwijzing, een net iets langere responstijd, een verschil in de lengte van het antwoord of het al dan niet verschijnen van een captcha. Elk waarneembaar onderscheid tussen “dit account bestaat” en “dit account bestaat niet” is bruikbaar.

Denk aan het aanbellen bij een appartementencomplex waar bij de ene naam een zoemer klinkt en bij de andere niets gebeurt. U komt nergens binnen, maar u weet na een kwartier precies wie er woont. Daarnaast dat is de informatie waarmee de eigenlijke aanval begint.

Waaraan herkent een aanvaller een bestaand account?

De inlogpagina is de bekendste plek, maar zelden de enige.

Kwetsbaar:

app.post('/inloggen', async (req, res) => {
  const gebruiker = await gebruikers.zoekOpEmail(req.body.email);
  if (!gebruiker) {
    return res.status(404).send('Dit e-mailadres is bij ons niet bekend');
  }

  const klopt = await bcrypt.compare(req.body.wachtwoord, gebruiker.hash);
  if (!klopt) {
    return res.status(401).send('Het wachtwoord is onjuist');
  }
  // ...
});

Hier lekt de informatie op drie manieren tegelijk: de tekst verschilt, de statuscode verschilt (404 tegenover 401) en de responstijd verschilt, omdat bcrypt.compare alleen wordt uitgevoerd wanneer het account bestaat. Die laatste is de hardnekkigste, omdat hij overblijft nadat de eerste twee zijn opgelost. Een aanvaller draait er eenvoudig een lijst doorheen:

info@bedrijf.nl      → 404, 12 ms   → bestaat niet
j.dekker@bedrijf.nl  → 401, 180 ms  → bestaat

Dezelfde fout komt vaak terug bij de wachtwoordherstelfunctie (“er is geen account met dit adres”), bij registratie (“dit adres is al in gebruik”) en bij het wijzigen van een e-mailadres.

Veilig:

const DUMMY_HASH = '$2b$12$c8y3Nq0Wl0mVQe1n6JbEZeQmQm2Yy8kQF9nZ0Xf1yqk8dK1vI0Iuq';

app.post('/inloggen', async (req, res) => {
  const gebruiker = await gebruikers.zoekOpEmail(req.body.email);

  // Altijd vergelijken, ook zonder account: gelijke rekentijd
  const hash = gebruiker?.hash ?? DUMMY_HASH;
  const klopt = await bcrypt.compare(req.body.wachtwoord, hash);

  if (!gebruiker || !klopt) {
    await vertraagWillekeurig();
    return res.status(401).send('De combinatie van e-mailadres en wachtwoord is onjuist');
  }
  // ...
});

Het antwoord is nu in alle opzichten identiek: dezelfde tekst, dezelfde statuscode en, doordat er ook bij een onbekend account een hashvergelijking wordt uitgevoerd, vrijwel dezelfde responstijd. Voor de wachtwoordherstelfunctie geldt dezelfde regel, met een neutrale bevestiging:

app.post('/wachtwoord-vergeten', async (req, res) => {
  const gebruiker = await gebruikers.zoekOpEmail(req.body.email);
  if (gebruiker) await stuurHerstelmail(gebruiker);   // stilzwijgend overslaan

  res.send('Als dit adres bij ons bekend is, ontvangt u binnen enkele minuten een e-mail.');
});
Vergeet de zijkanalen niet. Een captcha die alleen bij bestaande accounts verschijnt, een accountvergrendeling die alleen dán optreedt, of een tweefactorstap die alleen bij geldige gebruikers wordt getoond, verraden precies hetzelfde als een afwijkende foutmelding, ook wanneer de tekst zelf keurig neutraal is.

Wat is de impact van username enumeration?

Als losstaande bevinding is dit een kwestie van lage ernst: er wordt geen toegang verkregen en er lekken geen gegevens uit de accounts zelf. De betekenis ligt in wat het mogelijk maakt.

Met een geverifieerde lijst van bestaande accounts wordt elke vervolgaanval efficiënter. Een wachtwoordaanval die eerst duizenden niet-bestaande adressen moest afgaan, richt zich nu uitsluitend op echte accounts, wat de kans op succes vergroot en de kans op detectie verkleint. Bij password spraying, één veelgebruikt wachtwoord proberen op veel accounts, is zo’n lijst zelfs de belangrijkste voorwaarde.

Daarnaast is er een direct misbruik dat losstaat van inloggen. De bevestiging dát iemand een account heeft, is op zichzelf gevoelige informatie. Bij een medische, juridische, financiële of politiek gevoelige dienst kan het bestaan van een account al meer verraden dan de inhoud ervan. Bovendien voor gerichte phishing is het een geschenk: een e-mail die verwijst naar een dienst waarvan de ontvanger daadwerkelijk gebruikmaakt, is aanzienlijk overtuigender.

Hoe spoor je username enumeration op?

Een tester stuurt twee reeksen verzoeken: één met adressen waarvan zeker is dat er een account bij hoort en één met adressen die niet bestaan. Vervolgens worden de antwoorden vergeleken op alles wat waarneembaar is: de tekst, de statuscode, de lengte van het antwoord, de headers, de doorverwijzing en de responstijd.

Dat gebeurt niet alleen op de inlogpagina. Juist de omliggende functies zijn vaker kwetsbaar: wachtwoord vergeten, registratie, het wijzigen van een e-mailadres, uitnodigingen, en API-endpoints die de beschikbaarheid van een gebruikersnaam controleren voor de frontend. Die laatste categorie is een bekende: een endpoint dat bij het invullen van een registratieformulier meldt of een naam nog vrij is, is per definitie een enumeratie-interface. AssistSec meet daarbij ook de responstijden statistisch, omdat een tijdsverschil dat in een enkele meting verdwijnt over honderden verzoeken duidelijk zichtbaar wordt, en dat is precies hoe een aanvaller het zou aanpakken.

Hoe voorkom je username enumeration?

  • Geef bij het inloggen altijd dezelfde melding en dezelfde statuscode, ongeacht of het account bestaat.
  • Voer ook bij een onbekend account een hashvergelijking uit, zodat de responstijd niet verraadt wat er is gebeurd.
  • Bevestig bij wachtwoordherstel neutraal dat er een e-mail is verstuurd als het adres bekend is.
  • Meld bij registratie niet dat een adres al in gebruik is; leg dat uit in de e-mail naar dat adres.
  • Zorg dat captcha’s, vertragingen en vergrendelingen bij bestaande en niet-bestaande accounts gelijk optreden.
  • Vermijd endpoints die de beschikbaarheid van een gebruikersnaam of e-mailadres controleren, of beperk ze streng.
  • Beperk het aantal pogingen per IP-adres en per tijdseenheid, zodat grootschalige enumeratie opvalt en stokt.
  • Bewaak en alarmeer op een ongewoon aantal mislukte inlogpogingen met wisselende gebruikersnamen.

Bronnen

Veelgestelde vragen

Is dit erg als gebruikersnamen toch e-mailadressen zijn?

Het maakt het minder ernstig, maar niet onbelangrijk. Een aanvaller weet dan misschien wel het adres maar niet of er een account bij u bestaat. Die bevestiging is waardevol: ze maakt gerichte phishing geloofwaardiger en beperkt een wachtwoordaanval tot accounts die daadwerkelijk bestaan.

Hoe voorkom ik enumeratie bij registratie?

Meld bij het aanmaken van een account nooit direct dat een adres al in gebruik is. Bevestig in plaats daarvan dat er een e-mail is verstuurd, en leg in die e-mail uit of het om een nieuw account gaat of om een bestaand account. De informatie komt dan alleen terecht bij wie het postvak beheert.

Waarom is de responstijd relevant?

Omdat een applicatie bij een bestaand account meestal het wachtwoord verifieert en bij een onbekend account niet. Die berekening kost meetbaar tijd, vaak tientallen milliseconden. Een aanvaller die duizenden pogingen doet, ziet dat verschil duidelijk, ook als de foutmelding identiek is.

Is dit met tweefactorauthenticatie nog een probleem?

De ernst neemt af, want een geldige gebruikersnaam en een geraden wachtwoord zijn dan niet genoeg. Maar de lijst blijft bruikbaar voor gerichte phishing en voor aanvallen op de tweede factor zelf, zoals het bestoken van iemand met pushmeldingen tot hij er een goedkeurt.

Verwante artikelen

Druk op / om te zoeken · Esc