De voorspelbaarheid van PHPSESSID-tokens

El Niño
4 min readMay 22, 2017

Tijdens het werken met bestaande code kwam ik de volgende paar regels tegen voor het genereren van een wachtwoordresettoken:

$random = substr(str_shuffle("abcdefghijklmnopqrstuvwxyz"), 0, 5); 
$passwordResetToken = hash("md5", $email . $random);

In regel 1 wordt het alfabet geshuffled. Van het resultaat worden de eerste 5 karakters genomen en in de variabele $random geplaatst. In regel 2 wordt aan het e-mailadres van de gebruiker $random toegevoegd. Van het resultaat wordt een md5-hash gemaakt en dat is het $passwordResetToken.

Het probleem hier is dat $random niet echt random karakters bevat, want str_shuffle() gebruikt mt_rand() om de getallen voor het shuffelen te verzorgen. mt_rand() is een zogeheten PRNG, een pseudorandom number generator. Dit is een deterministisch soort generator die altijd dezelfde output geeft als het geseed is met dezelfde seed. Omdat de seed van mt_rand() maar 32 bit is (minder dan een wachtwoord van 5 karakters), is het gebruik van deze functie in een webapplicatie al vele keren de zwakste schakel gebleken. 5 jaar geleden werden PHPSESSID-tokens nog gegenereerd met mt_rand() en konden deze vrij gemakkelijk voorspeld worden als je een eerdere output van PRNG had. Sessies waren dus vrij eenvoudig over te nemen, wat inhoudt dat je effectief in kon loggen als een ander persoon. Dit is de reden waarom PHPSESSID niet meer met een PRNG gegenereerd wordt. Dit alles werd mogelijk gemaakt doordat in hetzelfde PHP-proces mt_rand() maar 1 keer met een random 32-bitgetal als seed geïnitialiseerd wordt. Een getal uit mt_rand() geeft dus informatie over de seed. Als je de seed hebt kun je eenvoudig de volgende random getallen voorspellen.

Kwetsbaarheid door mt_rand()

Wat er in het bovenstaande stukje code gebeurt is dat de alfabetstring wordt geshuffeld en elke letter wordt gewisseld met een andere letter. Welke letter dat is wordt bepaald door de output van mt_rand(). Dus stel dat je uit de md5-hash, bestaande uit e-mailadres + random, het random gedeelte kunt halen dan geeft je dat een beetje informatie over de seed van mt_rand(). Nog lang niet genoeg, want je krijgt alleen maar de eerste 5 karakters van het resultaat van de shuffle, maar het is een begin. Wat het kraken mogelijk maakt is het feit dat het e-mailadres makkelijk te vinden is. Daarmee verkleint de aanwezigheid ervan de entropie van wat er gebruikt wordt als input voor de hash ten opzichte van wanneer het willekeurige of minder makkelijk te voorspellen karakters waren.

Indien het emailadres bekend is kan het random gedeelte gemakkelijk gekraakt worden. Dit kost een moderne GPU minder dan 1 seconde tijd in combinatie met hashcat. In theorie zou de volgende aanval mogelijk zijn:

  1. Vraag 10 password reset requests aan voor jezelf
  2. Vraag meteen daarna 1 password reset aan voor de admin van de site (e-mailadres benodigd)
  3. Haal het random gedeelte uit de password reset tokens die je hebt ontvangen
  4. Seed mt_rand() met elke mogelijke seed
    • Genereer per seed 32 getallen
    • Gebruik getal 1–6 als startpunt voor het shuffelen van het alfabet
    • Noteer startpunt en seed als het overeenkomt met een ontvangen wachtwoordresettoken
  5. Er blijven nu één of een paar mogelijke seeds over. Deze kunnen nu gebruikt worden om kandidaten te genereren voor het wachtwoordresettoken van de admin.
  6. Verifiëer alle mogelijkheden voor het token en reset het wachtwoord als je raak hebt.

Omdat er geen compleet getal uit mt_rand() in de hash die naar de gebruiker gaat zit en er geen andere plek is waar zo’n getal te vinden is, is het achterhalen van de seed een stuk lastiger dan wanneer dat wel het geval zou zijn. Maar het is zeker wel mogelijk. Stel dat een aanvaller geen informatie van de achterliggende code zou hebben, dan kan deze er achterkomen dat het token bestaat uit e-mailadres + 5 kleine letters. Dit is te weinig informatie om het password reset token te achterhalen. Aangezien dit alleen te controleren is door een request naar de server te sturen en er (26!/21!=)7893600 opties zijn, dan zijn dat er realistisch gezien nog te veel om te proberen (10 requests per seconde voor 9 dagen dus gemiddeld 4.5 dagen). Zolang het echter weliswaar niet realistisch maar toch niet onmogelijk is is het natuurlijk niet goed genoeg.

Om dit te voorkomen moet er voor het genereren van de tokens een betere bron voor random gebruikt worden, meer random bits gebruikt worden om de hash te genereren en een hashfunctie die niet al jaren verouderd/te makkelijk te kraken is gebruikt worden zoals SHA256 of sterker.

De oplossing

$passwordResetToken = hash_hmac(‘sha256’, random_bytes(40), null)

SHA256 op zichzelf is 8x langzamer te kraken met hashcat dan MD5, maar de grootste stap is de random_bytes() functie als input voor de hash, want in tegenstelling tot mt_rand afgeleiden is random_bytes() wel geschikt voor gebruik in cryptografie. Daar bovenop komt nog dat een relatief makkelijk te vinden stuk informatie, het e-mailadres, ook niet gebruikt wordt als input voor de hash en daarmee de entropie verkleint

Denk dus goed na over hoe je tokens genereert in je webapplicatie, zorg dat je een goede bron van entropie gebruikt als je random bits nodig hebt en gebruik niet zomaar iets anders dan random bytes als input voor een hash. Hashes zijn niet omkeerbaar dus dat is eigenlijk nergens goed voor.

Meer info

Door Maurits van der Vijgh. Als backend programmeur zorgt Maurits ervoor dat de niet zichtbare kant van applicaties goed functioneert.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

El Niño
El Niño

Written by El Niño

http://www.elnino.tech. Digital Development Agency building tailor made solutions, ensuring success by making it measurable.

No responses yet

Write a response