LabbCybersäkerhet · Etisk hackning
0/0
Laboration

Autentisering — lagring, hastighetsbegränsning och MFA

Mål: Bygga en autentiseringsmekanism som håller även om databasen skulle läcka.

Observera

Genomför TryHackMes eller PortSwiggers moduler om autentiseringssvagheter. Studera därefter, konceptuellt, hur en läckt lösenordsdatabas hanteras: skillnaden mellan en databas med lösenord i klartext, med omodern hashning, och med modern hashning är skillnaden mellan en katastrof, ett allvarligt problem och en hanterbar incident.

Analysera grundorsaken

Tre separata brister brukar förekomma tillsammans:

  1. Lösenorden lagras så att de går att återställa — klartext, reversibel kryptering, eller snabba hashfunktioner som MD5 och SHA-1 som är konstruerade för hastighet och därmed lätta att testa sig igenom i stor skala.
  2. Inget tak för antalet inloggningsförsök — ingenting hindrar att samma konto testas om och om igen.
  3. Lösenordet är enda faktorn — ett känt lösenord ger full åtkomst.

Bygg försvaret

1. Modern lösenordshashning. Använd Argon2id (förstahandsval), scrypt eller bcrypt. Dessa är medvetet långsamma och minneskrävande, vilket gör storskalig testning opraktisk. Saltet hanteras automatiskt av biblioteken.

Python:

from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher()

def registrera(losenord: str) -> str:
    return ph.hash(losenord)          # saltet genereras automatiskt

def verifiera(hash_i_db: str, inmatat: str) -> bool:
    try:
        ph.verify(hash_i_db, inmatat)
        return True
    except VerifyMismatchError:
        return False
Vad koden gör — förklaring rad för rad

Säker lösenordshantering i Python. Hashning innebär att lösenordet omvandlas till en oläslig sträng som inte går att vända tillbaka. Vid inloggning jämförs inte lösenord mot lösenord, utan hash mot hash.

from argon2 import PasswordHasher
Hämtar in ett färdigt bibliotek. Du ska aldrig skriva din egen kryptokod — använd det som redan är granskat.
ph = PasswordHasher()
Skapar ett verktyg att hasha med och lägger det i variabeln ph.
def registrera(losenord: str) -> str:
Definierar en funktion som tar emot ett lösenord och lämnar tillbaka en hash. Texten efter kolonet anger att det ska vara text in och text ut.
ph.hash(losenord)
Gör om lösenordet till en hash. Biblioteket lägger automatiskt till ett slumptal, ett så kallat salt, så att två identiska lösenord ändå får olika hashvärden.
try: / except VerifyMismatchError:
Försök göra det här; om det går fel på just det här sättet, gör det andra i stället. Biblioteket signalerar fel lösenord genom att kasta ett fel, inte genom att svara nej.
return True / return False
Funktionen lämnar tillbaka sant eller falskt — lösenordet stämde eller inte.

Node.js:

const argon2 = require('argon2');

const hash = await argon2.hash(losenord);
const giltigt = await argon2.verify(hash, inmatat);
Vad koden gör — förklaring rad för rad

Samma sak i Node.js, på två rader. Kortheten är poängen: korrekt lösenordshantering kräver inte mycket kod när biblioteket gör jobbet.

require('argon2')
Node.js sätt att hämta in ett bibliotek.
await argon2.hash(losenord)
Skapar hashen. await behövs eftersom hashningen avsiktligt tar tid.
argon2.verify(hash, inmatat)
Jämför det inmatade lösenordet mot den sparade hashen och svarar sant eller falskt.

PHP (använder bcrypt eller Argon2 beroende på konfiguration):

$hash = password_hash($losenord, PASSWORD_DEFAULT);
$giltigt = password_verify($inmatat, $hash);
Vad koden gör — förklaring rad för rad

Samma sak i PHP, med funktioner som är inbyggda i språket. Inget extra bibliotek behövs.

password_hash($losenord, PASSWORD_DEFAULT)
Hashar lösenordet. PASSWORD_DEFAULT betyder "använd den metod PHP just nu anser bäst", vilket gör att koden följer med när rekommendationerna ändras.
password_verify($inmatat, $hash)
Jämför och svarar sant eller falskt. Observera ordningen: först det inmatade, sedan det sparade.

Skriv aldrig egen kryptorelaterad kod. Använd biblioteken ovan.

2. Hastighetsbegränsning. Begränsa både per konto och per IP-adress, med successivt ökande fördröjning:

MAX_FORSOK = 5
LASNINGSTID = 900  # 15 minuter

def kontrollera_sparr(anvandarnamn, ip):
    nyckel_konto = f"forsok:konto:{anvandarnamn}"
    nyckel_ip    = f"forsok:ip:{ip}"
    if redis.get(nyckel_konto) and int(redis.get(nyckel_konto)) >= MAX_FORSOK:
        raise KontoTillfalligtSparrat()
    if redis.get(nyckel_ip) and int(redis.get(nyckel_ip)) >= MAX_FORSOK * 10:
        raise ForMangaForsokFranAdress()

def registrera_misslyckande(anvandarnamn, ip):
    for nyckel in (f"forsok:konto:{anvandarnamn}", f"forsok:ip:{ip}"):
        redis.incr(nyckel)
        redis.expire(nyckel, LASNINGSTID)
Vad koden gör — förklaring rad för rad

Hastighetsbegränsning i Python. Koden räknar misslyckade inloggningsförsök och spärrar tillfälligt när de blir för många — både per konto och per avsändaradress.

MAX_FORSOK = 5
En konstant: ett värde som används på flera ställen och enkelt kan ändras på ett ställe. Versaler är en vana som signalerar att värdet inte ändras under körning.
redis
Ett snabbt minnesbaserat lager där räknarna sparas. Att använda en databas för detta vore onödigt långsamt.
f"forsok:konto:{anvandarnamn}"
En formatsträng. Bokstaven f före citattecknet gör att det som står i klammerparentes byts ut mot variabelns värde — nyckeln blir alltså unik per användare.
raise KontoTillfalligtSparrat()
Avbryter och signalerar ett fel uppåt i programmet. Inloggningen fortsätter inte.
redis.incr(nyckel)
Ökar räknaren med ett.
redis.expire(nyckel, LASNINGSTID)
Säger åt räknaren att radera sig själv efter angiven tid. Det är så spärren släpper automatiskt efter femton minuter.
MAX_FORSOK * 10
Gränsen per adress sätts tio gånger högre, eftersom många legitima användare kan dela samma adress på ett kontor.

3. Enhetliga felmeddelanden. "Fel användarnamn eller lösenord" avslöjar inte vilket av de två som var fel. "Användaren finns inte" låter en utomstående kartlägga vilka konton som existerar.

4. Flerfaktorsautentisering. TOTP (authenticator-app) eller helst WebAuthn/passkeys, som dessutom är motståndskraftiga mot nätfiske eftersom inloggningen är bunden till domänen. Erbjud återställningskoder, och behandla återställningsflödet med samma stränghet som inloggningen — det är ofta den svagaste länken.

5. Lösenordspolicy enligt modern praxis: längd framför komplexitet (minst 12 tecken), kontroll mot listor över kända läckta lösenord, och inga påtvingade regelbundna byten utan konkret misstanke. Påtvingade byten leder i praktiken till svagare, mer förutsägbara lösenord.

Verifiera

Testa i ditt labb att kontot faktiskt spärras efter angivet antal försök, att spärren släpper efter rätt tid, att felmeddelandet är identiskt oavsett om kontot finns, och att MFA-flödet inte går att hoppa över genom att gå direkt till en URL efter lösenordssteget.

Dokumentera

Skriv en kort säkerhetsgenomgång av autentiseringsflödet med en punkt per kontroll och status (uppfylld / delvis / saknas). Detta format är användbart i verkliga uppdrag.

Reflektionsfrågor: Varför är en långsam hashfunktion en fördel? Vad skyddar WebAuthn mot som TOTP inte gör?


Kunskapskontroll

Välj ett svar per fråga. Du får återkoppling direkt, och avsnittet markeras som klart när alla frågor är besvarade.

1Varför är en långsam hashfunktion som Argon2id en fördel för lösenordslagring?

2Vad är problemet med felmeddelandet "Användaren finns inte"?

3Vilken fördel har WebAuthn/passkeys framför TOTP från en authenticator-app?