Simovits

Hasha i onödan tack!

Redan från barnsben blir utvecklare drillade att skriva så effektiv kod som möjligt utan onödiga jämförelser och funktionsanrop.

Vid inloggning till en webbapplikation är det vanligt att man loggar in genom att ange ett användarnamn och ett lösenord. Dessa uppgifter skickas till servern som kontrollerar om det finns en sådan användare med ett sådant lösenord, och svarar därefter med antingen ett ok eller avslag. Då lösenordet enligt best practice inte sparas i klartext, behöver servern räkna ut det angivna lösenordets hash-värde med hjälp av en envägs hashfunktion för att jämföra med det sparade hashvärdet. En lösenordshash-funktion är tidskrävande per design, för att det ska ta längre tid att knäcka lösenord.

Därför är det förståeligt att en vanlig utvecklare som är van att inte anropa tidskrävande funktioner i onödan, programmerar inloggningsvalideringsfunktionen så att servern först kontrollerar om angivet användarnamn finns och därefter kontrollerar om det angivna lösenordet stämmer, men struntar den tidskrävande uträkningen av hashvärdet om ändå ett felaktigt användarnamn angivits.

Detta för en utvecklare logiska flöde ger dock möjlighet för en angripare att göra en enkel tidsbaserad sidechannel-attack för att kartlägga vilka användarnamn som finns i systemet.

Varför bry sig?

Nu kanske läsaren tänker: ”Men vad gafflar han om, ett användarnamn är väl inte så hemligt?”

Och det är korrekt att enstaka användarnamn för sig själva oftast inte anses direkt hemliga, men en angripare som har tillgång till samtliga/många användarnamn tillhörande ett och samma system har ofantligt mycket större chans att ta sig in i systemet.

Om vi antar att systemet/applikationen har 100 användare skulle man statistiskt sett nästan med säkerhet kunna dra slutsatsen att minst en av dessa hundra användare har ”Sommar2019” som lösenord (eller något av de övriga aktuella mest använda lösenorden). Ett fåtal gissningar per användare är oftast det som behövs för att angriparen ska ha berett sig tillträde till något/några konton på systemet.

Det underlättar dessutom mångexponentiellt i tid för angriparen om han vet vilka användarnamn som är giltiga och han avser knäcka lösenord, eftersom han slipper att parallellt även gissa användarnamnen.

Attackexempel

För att demonstrera hur enkel och effektiv attacken är, gjordes en simpel inloggningssida som vi kunde attackera.

En enkel inloggningssida, där omdirigering symboliserar lyckad inloggning

För attacken använder vi här verktyget ZAP, men samma funktionalitet finns givetvis i andra man-in-the-middle-proxies såsom exempelvis Burp.

Vad attacken går ut på är att ”fuzza” användarnamnet, vilket innebär att vi testar en mängd olika, med ett godtyckligt lösenord som troligen är ogiltigt. Det är givetvis mest effektivt om man vet något om formatet på användarnamnen, och för denna demonstration låtsas vi att vi har information om att det används förnamn, och därför använder vi en lista med de ca 1000 vanligaste mansnamnen respektive kvinnonamnen i Sverige. Även namnet ”Admin” lades till i listan pga dess vanliga förekomst.

Om man inte vet formatet på användarnamnen och sidan är sårbar för denna attack borde man dock kunna göra relativt effektiva försök med gissningar på vanliga format på användarnamn eller till och med till viss del använda brute-force eftersom sårbarheten innebär att varje misslyckat försök går relativt snabbt då man inte behöver hasha som vid lösenordsknäckning (dock sker denna attack online, så nätverksfördröjning och svarstider från servern är begränsande faktorer). När man hittat åtminstone ett giltigt användarnamn kan man antagligen gissa sig fram till formatet och effektivisera attacken.

För att göra vår fuzzing-attack börjar vi med att göra ett inloggningsförsök med godtyckliga användaruppgifter, och låter ZAP fånga upp det POST-anrop som då sker. Vi väljer att fuzza med detta POST-anrop (markerat 1 i bilden nedan) som grund. Här har vi exempelvis testat att logga in med användarnamn ”Danilo” och lösenordet ”badpass”. I fuzzing-fönstret väljer vi att markera ”Danilo” (2 i bilden) som blir vår ”fuzz location” (markering 3 i bilden).  För denna fuzz location lägger vi till en payload i form av vår lista på svenska namn (markering 4 i bilden). Resultatet blir att ZAP kommer att göra en mängd motsvarande inloggningsförsök där namnen i listan (markering 5 i bilden) ett och ett kommer att ersätta den markerade texten (“Danilo”).

Samtliga inloggningsförsök kommer att ske med “badpass” som lösenord, vilket vi inte bryr oss om att ändra eftersom målsättningen inte är att inloggningsförsöken ska lyckas, snarare tvärtom.

Attackinställningar i ZAP

Vi kör igång vår fuzzing-attack och några sekunder senare kan vi se resultatet. Listan över de anrop som skett (och dess svar) sorterar vi på svarstid från servern (RTT, Round Trip Time), och ser då att det finns sex anrop (markerade med 1 i bilden nedan) som har en tydlig skillnad i tidsåtgång jämfört med de övriga. I vårt fall ligger de flesta svarstider på under 50 ms, men de sex översta på över en sekund.

Tidsåtgången avslöjar de giltiga användarnamnen

Vid dessa sex anrop kan man alltså dra slutsatsen att servern antagligen gått vidare och räknat ut lösenordets hashvärde för jämförelse med det sparade, och att de användarnamn (markerat 2 i bilden) som användes som payload vid dessa anrop alltså är giltiga.

Resultaten kan vara mindre tydliga än i detta fall och kan ge en del falsk-positiva resultat beroende på exempelvis nätverksfördröjningar etc. men i detta fall kan jag bekräfta att samtliga sex i topp i listan verkligen är 6 av de 8 definierade användarna. De två sista är ”Peder” och ”Sverker” som tydligen inte kvalificeras som några av de 1000 vanligaste namnen i Sverige. En hängiven angripare skulle givetvis använt en större lista med namn, och med största säkerhet hittat även dessa.

Åtgärd

Oftast är det enkelt att åtgärda detta vanliga utvecklarmisstag, genom att flytta någon/några rader eller ta bort någon return-statement i koden så att hashning alltid utförs oavsett användarnamn, och svaret till klienten alltså alltid returneras först efter hashningen även vid ogiltiga användarnamn.

Syftet är alltså att svarstiden skall vara i princip lika lång för misslyckade inloggningsförsök oavsett om användaren finns eller ej.

I vårt fall är det som behövs att flytta de två markerade raderna som definierar $salt samt $hash ut från den första if-satsen enligt bilderna nedan (och eventuellt slå ihop if-satserna, om man vill snygga till koden).

Kod före åtgärd
Kod efter åtgärd

Med denna enkla åtgärd tar den tidigare gjorda attacken som tog ca 10 sekunder istället 40 minuter, och ingen av de giltiga användarna förekommer i topp på RTT-listan.

Attacken fungerar inte efter åtgärden

Så, utvecklare, gå emot era grundläggande principer och ta tiden att hasha lösenordsinput även när användarinput är felaktig, för det är ju onödigt att effektivisera och spara tid för användare som inte finns?