Password Attacks & Cryptography · beginner · ~10 min

Online attacks, lockout, and MFA

**What you will learn** - Distinguish the three online password attacks — brute force, credential stuffing, and password spraying — by their guess pattern and the defenses each one defeats. - Explain why naive account lockout both fails against spraying and creates a denial-of-service risk, and name better controls (throttling, backoff, anomaly detection, breached-password screening). - Describe how MFA changes the attacker's cost, and correctly reason about its real bypasses (MFA fatigue, SIM-swap, OTP phishing) instead of treating it as magic. - Read authentication logs to *detect* a spray in progress: the low-and-slow, one-attempt-per-account fingerprint. - Recommend a layered defense (defense in depth) and verify each control actually blocks the attack in an authorized lab.

Overview

Security objective. The asset is the login boundary of a live service — the point where a username and password (and ideally a second factor) decide whether someone becomes an authenticated user. The threat is an attacker who never steals your database but simply guesses credentials against the running application. By the end you will be able to detect an online guessing campaign in the logs and prescribe controls that stop it without locking out your own users.

This builds directly on your prereq, Password cracking concepts (authorized labs only). That lesson was about offline attacks: the attacker already has the password hashes and grinds them on their own hardware, as fast as the algorithm allows. This lesson is the mirror image — online attacks, where the attacker has no hashes and must knock on the real front door one guess at a time. Online attacks are slower and noisier, but they target people and their password habits, and they work even when your hashing is perfect.

There are two shapes to know. Brute force / credential stuffing throws many guesses at one account (stuffing reuses passwords leaked from other sites). Password spraying flips it: one common password tried against many accounts, deliberately slow, to slip under lockout thresholds. Spraying is the quiet, practical attack that actually breaches organizations.

Lockout feels like the obvious fix, but it is a double-edged sword — it can be weaponized to lock everyone out, and it does nothing against a one-attempt-per-account spray. The durable answer is defense in depth: slow hashing, length-based policy, breached-password screening, sane throttling, anomaly detection, and above all MFA, which makes a single guessed password no longer enough. Everything here is defensive and lab-only — you will study attacks to build detection and controls, never to run them against systems you do not own.

Why it matters

Spraying and credential stuffing are not exotic — they are how real accounts actually get taken over. Attackers do not need to break your cryptography when a fraction of your users reuse Summer2025! or a password already sitting in a public breach corpus. In authorized professional work this shows up constantly:

  • Penetration testers and red teams routinely gain their first foothold in a corporate network by spraying one seasonal password across every employee account they can enumerate. Understanding it lets you demonstrate the risk and, more importantly, write the remediation.
  • Blue teams and SOC analysts must recognize the spray fingerprint in authentication logs and respond before it succeeds. Miss it and one weak password becomes domain access.
  • Defenders and architects are constantly asked "should we just lock accounts after 3 failures?" You need to explain why that answer is incomplete — it invites denial-of-service and ignores spraying — and steer toward throttling plus breached-password screening plus MFA.

MFA is the single highest-impact recommendation most engineers will ever make. But recommending it well means understanding both its strength (a stolen password is no longer sufficient) and its limits (fatigue, SIM-swap, OTP phishing). Advising "turn on MFA" without knowing that SMS codes are phishable, or that push prompts get fatigue-bombed, is how real deployments fail. This lesson gives you the vocabulary to give correct, defensible advice.

Core concepts

Each attack below is defined the same way: what it is, how it works, when it applies, and the pitfall that makes it dangerous. Read attacks as detection and prevention material — the goal is to recognize and block them.

Concept 1 — Brute force against one account

Definition. Repeatedly guessing many passwords for a single known username.

How it works. The attacker fixes the username and iterates a wordlist or character space against the live login form or API. Each wrong guess is one failed authentication event for that account.

When / when not. Useful only when there is no lockout and no throttling, or against a single high-value account. Against any modern service it is the loudest option: it stacks failures on one account and trips lockout almost immediately.

Pitfall. Because all the noise lands on one account, brute force is the easiest attack to detect and to stop with a simple failure counter — which is exactly why real attackers rarely use it as their first move.

Concept 2 — Credential stuffing

Definition. Replaying username+password pairs leaked from other breaches against your service, betting on password reuse.

How it works. The attacker feeds a list of real (email, password) pairs from an unrelated breach. Each pair is tried once. Because people reuse passwords, a small percentage simply work — no "guessing" required.

When / when not. Effective anywhere users reuse passwords and where there is no breached-credential screening. Less effective if you force unique passwords and screen against known-breached corpora.

Pitfall. Stuffing can look like legitimate traffic — each user is tried with one plausible password — so a naive failure counter barely notices it. Detection needs behavioral signals (many distinct accounts from one source, impossible-travel, datacenter IPs).

Concept 3 — Password spraying

Definition. Trying one (or a few) common passwords across many accounts, slowly.

How it works. The attacker enumerates or guesses many valid usernames, then tries a single common password — Winter2025!, Company@123 — against all of them, waits, and tries the next password later. Each account sees only one or two failures, so no per-account threshold is crossed.

When / when not. The go-to online attack inside organizations (also central to the Active Directory track). It thrives wherever per-account lockout is the only defense and where common/seasonal passwords are allowed.

Pitfall / insecure assumption. The whole attack exploits one bad assumption: "if no single account crosses the failure threshold, nothing is wrong." Spraying is invisible to per-account counting; you must correlate failures across accounts by source and time.

Concept 4 — Account lockout: a double edge

Definition. Disabling an account after N failed attempts within a window.

How it works. A counter increments on failure and, past a threshold, refuses further attempts for a lockout period.

Two weaknesses.

Problem Why it hurts
Denial of service An attacker who knows usernames can deliberately fail logins to lock out real users — locking is now an attack, not just a defense.
Useless vs spraying One attempt per account never reaches the threshold, so the counter never fires.

Better controls: progressive throttling / exponential backoff, IP- and ASN-level rate limiting, anomaly detection (many accounts from one source), CAPTCHA on suspicion, and breached-password screening — layered, not lockout alone.

Concept 5 — MFA: the equation-changer

Definition. Requiring a second, independent proof of identity beyond the password (an authenticator-app code, a push approval, or a hardware security key).

How it works. Even a correct password only clears the first factor; the attacker still needs the second, which they do not possess.

Real bypasses (know them, so you recommend the right kind).

Bypass What happens Mitigation
MFA fatigue / push bombing User is spammed with push prompts until they tap Approve Number-matching, limited prompts, FIDO2
SIM-swap Attacker hijacks the phone number to receive SMS codes Avoid SMS as a factor; use app/hardware
OTP phishing A fake site relays the user's typed code in real time Phishing-resistant FIDO2 / WebAuthn

Critical misconception to correct: MFA raises attacker cost dramatically but nothing is "completely secure." Prefer phishing-resistant factors (FIDO2 / WebAuthn hardware keys) over SMS.

Threat model

            UNTRUSTED (Internet)                 |   TRUST BOUNDARY   |     TRUSTED (your service)
  --------------------------------------------   |  (login endpoint)  |  --------------------------------
  Attacker
   |  guesses via public login form / auth API
   |  Entry point 1: POST /login  (username+password)
   |  Entry point 2: SSO / OAuth token endpoint
   |  Entry point 3: legacy protocol w/o MFA (IMAP/SMTP/RDP)
   v
  ==========================[ AUTH BOUNDARY ]==========================
   [Factor 1: password check]  -> throttle + breached-pw screen + lockout
   [Factor 2: MFA check]       -> phishing-resistant preferred
   [Detection]                 -> auth logs, cross-account failure correlation
           |
           v
  ASSET PROTECTED: authenticated session -> mailbox, files, admin console

Entry points to remember: the visible login form is only one door. Legacy protocols (IMAP, SMTP-AUTH, old RDP) and some token endpoints frequently bypass MFA entirely and are a favorite spray target.

Knowledge check.

  1. What asset is protected? The authenticated session and everything behind it (mailbox, files, admin console) — reached only by clearing the auth boundary.
  2. What insecure assumption makes spraying work? That per-account failure counting is sufficient — one attempt per account never trips it.
  3. Which logs detect a spray? Authentication logs correlated across accounts by source IP/ASN and time: many distinct usernames, one failure each, from one origin, in a slow cadence.
  4. Why only in an authorized lab? Guessing credentials against systems you do not own or lack written permission to test is unlawful and unethical; all practice here runs on localhost / containers / intentionally-vulnerable VMs.

Syntax notes

This is a concept lesson, so the "syntax" is the shape of an authentication log line and the throttling decision — the two things you actually read and write when defending. Below is a lab-safe, annotated example of the log fields a defender should emit and then correlate.

# One structured auth log line (JSON) — lab example, no real data
{
  "ts": "2026-07-07T09:14:02Z",   # timestamp (UTC) — needed to see slow cadence
  "event": "auth_attempt",
  "username": "alice",             # the account targeted
  "result": "failure",             # success | failure | locked | mfa_required
  "reason": "bad_password",        # security decision, not the password itself
  "src_ip": "10.0.0.5",           # source — correlate across accounts
  "asn": "AS64500",               # network origin — datacenter vs residential
  "corr_id": "c1f2-..."           # correlation id to stitch a session together
}

The defensive control expressed in words (pseudo-rule you will implement in code below):

# Spray detection heuristic (correlate ACROSS accounts, not within one)
IF   distinct_failed_usernames(src_ip, window=10m) >= N_ACCOUNTS
AND  failures_per_username <= 2
THEN raise "possible password spray" + throttle/deny src_ip + alert SOC

Never log the attempted password, the session cookie, or the MFA code — only the decision (result, reason).

Lesson

Not every password attack happens offline. Online attacks guess against a live service. They are slower and noisier than offline cracking, but they target people, not stolen hashes.

Brute force vs spraying

  • Brute force / credential stuffing: Many guesses against one account. (Stuffing reuses credentials leaked elsewhere.) This triggers lockouts fast.
  • Password spraying: One common password tried against many accounts. It runs slowly and quietly to stay under lockout thresholds.

Spraying is the more practical online attack inside organizations. (It is also covered in the Active Directory track.)

Account lockout: a double edge

Locking an account after N failed attempts stops brute force. But it has two weaknesses:

  • Denial of service. An attacker can deliberately lock everyone out.
  • It does not stop spraying. Spraying sends only one attempt per account, so the threshold is never reached.

Better controls include rate limiting, throttling and backoff, anomaly detection, and breached-password screening. Use these instead of relying on aggressive lockout alone.

MFA: the equation-changer

Multi-factor authentication (MFA) requires a second proof of identity beyond the password, such as a code from an app or a hardware key.

This means a cracked or guessed password is not enough on its own. The attacker also needs the second factor. MFA is the single most effective control against password attacks.

Still, know the bypasses (covered in the web and API tracks):

  • MFA fatigue: flooding the user with push prompts until they approve one.
  • SIM-swap: taking over a phone number to intercept SMS codes.
  • OTP phishing: tricking users into handing over one-time codes.
  • Skippable or poorly enforced MFA steps.

Prefer phishing-resistant factors such as FIDO2 / WebAuthn.

The defender's stack

Good defense combines several layers:

  • Strong, slow password storage
  • Length-based password policy
  • Breached-password screening
  • Sensible throttling (not just lockout)
  • MFA everywhere

This is defense in depth, so that no single weak password is fatal.

Code examples

The example is a tiny, self-contained Python auth simulator you run only on your own machine. It shows the insecure shape (per-account-only defense), the secure fix (throttling + cross-account spray detection + MFA gate), and a verification harness proving the fix rejects a spray while still accepting a legitimate login. No network, no real target.

(1) WARNING: intentionally vulnerable — use only in a local, isolated, authorized lab. Do not deploy.

# insecure_login.py  -- per-account lockout ONLY. Blind to spraying.
from collections import defaultdict

USERS = {"alice": "S3cret-unique-A", "bob": "S3cret-unique-B", "carol": "Winter2025!"}
failures = defaultdict(int)          # counts failures PER ACCOUNT only
LOCK_THRESHOLD = 5

def login(username, password):
    if failures[username] >= LOCK_THRESHOLD:
        return "locked"
    if USERS.get(username) == password:
        failures[username] = 0
        return "success"
    failures[username] += 1
    return "failure"

# A spray: ONE common password across MANY accounts -> 1 failure each,
# threshold (5) never reached, carol (a reuser) is compromised silently.
if __name__ == "__main__":
    spray_pw = "Winter2025!"
    for user in USERS:
        print(user, login(user, spray_pw))   # carol -> success. No alarm.

Expected output:

alice failure
bob failure
carol success

The spray wins: carol reused a common password, no single account hit 5 failures, and nothing was logged or alerted.

(2) SECURE fix — throttle + cross-account spray detection + breached-password screen + MFA gate

# secure_login.py -- layered defense. Still lab-only.
import time, logging
from collections import defaultdict, deque

logging.basicConfig(level=logging.INFO, format="%(message)s")
log = logging.getLogger("auth")

USERS = {"alice": "S3cret-unique-A", "bob": "S3cret-unique-B", "carol": "Winter2025!"}
MFA_ENABLED = {"alice", "bob", "carol"}              # second factor required
BREACHED = {"Winter2025!", "Summer2025!", "Company@123", "Password1!"}

WINDOW = 600            # 10 minutes
SPRAY_ACCOUNTS = 3      # distinct accounts failed from one source -> suspicious
IP_RATE = 10            # max attempts per source per window

src_events = defaultdict(deque)     # src_ip -> timestamps (rate limit)
src_failed_users = defaultdict(set) # src_ip -> distinct usernames failed (spray)

def _prune(dq, now):
    while dq and now - dq[0] > WINDOW:
        dq.popleft()

def verify_mfa(username, code):
    # Lab stub: a real system verifies a TOTP/FIDO2 assertion here.
    return code == "good-second-factor"

def login(username, password, src_ip, mfa_code=None, now=None):
    now = time.time() if now is None else now
    ev = src_events[src_ip]; _prune(ev, now)

    # Control A: source rate limit (blunts brute force AND spraying volume)
    if len(ev) >= IP_RATE:
        log.info('{"event":"throttled","src_ip":"%s","result":"denied"}', src_ip)
        return "throttled"
    ev.append(now)

    # Control B: cross-account spray detection (the key fix)
    if len(src_failed_users[src_ip]) >= SPRAY_ACCOUNTS:
        log.info('{"event":"spray_suspected","src_ip":"%s","accounts":%d}',
                 src_ip, len(src_failed_users[src_ip]))
        return "blocked_spray"

    ok_pw = USERS.get(username) == password
    if not ok_pw:
        src_failed_users[src_ip].add(username)
        log.info('{"event":"auth","user":"%s","result":"failure","reason":"bad_password","src_ip":"%s"}',
                 username, src_ip)
        return "failure"

    # Control C: breached-password screening (blocks reuse of common/leaked pw)
    if password in BREACHED:
        log.info('{"event":"auth","user":"%s","result":"failure","reason":"breached_password"}', username)
        return "failure_breached"

    # Control D: MFA gate -- a correct password is NOT enough
    if username in MFA_ENABLED and not verify_mfa(username, mfa_code):
        log.info('{"event":"auth","user":"%s","result":"mfa_required"}', username)
        return "mfa_required"

    log.info('{"event":"auth","user":"%s","result":"success","src_ip":"%s"}', username, src_ip)
    return "success"

(3) VERIFY — prove the fix REJECTS the spray and ACCEPTS a legitimate login

# verify.py
from secure_login import login

# Bad input: a spray from one source across many accounts -> must NOT succeed
attacker = "203.0.113.9"          # TEST-NET-3, documentation range (RFC 5737)
results = [login(u, "Winter2025!", attacker) for u in ("alice", "bob", "carol", "dave")]
assert "success" not in results, results
assert any(r in ("blocked_spray", "failure", "failure_breached") for r in results)
print("SPRAY REJECTED:", results)

# Good input: legitimate user, correct password, correct second factor -> success
ok = login("alice", "S3cret-unique-A", "192.0.2.10", mfa_code="good-second-factor")
assert ok == "success", ok
print("LEGIT LOGIN ACCEPTED:", ok)

# Correct password but NO second factor -> must be stopped at MFA
no_mfa = login("bob", "S3cret-unique-B", "192.0.2.11")
assert no_mfa == "mfa_required", no_mfa
print("PASSWORD-ONLY BLOCKED BY MFA:", no_mfa)

Expected output:

SPRAY REJECTED: ['failure', 'failure', 'blocked_spray', 'blocked_spray']
LEGIT LOGIN ACCEPTED: success
PASSWORD-ONLY BLOCKED BY MFA: mfa_required

The secure version stops the spray on the third distinct account from one source, screens out the reused breached password, and enforces MFA so a guessed password alone never yields a session — while a genuine user with the right factors still gets in. All addresses used are RFC 5737 documentation ranges, not real hosts.

Line by line

Walkthrough of the secure example, top to bottom.

Step Code / field What happens Why it matters
1 USERS, MFA_ENABLED, BREACHED Lab data: valid creds, which accounts require a second factor, and a small known-bad password set Models the three layers a defender controls
2 src_events, src_failed_users Per-source structures: a timestamp queue and a set of distinct failed usernames The set is the whole trick — it correlates across accounts, which per-account lockout cannot
3 _prune Drops events older than WINDOW Makes rate and spray checks apply to a rolling 10-minute window, not all history
4 Control A rate limit If a source made IP_RATE attempts this window, return throttled Caps the volume any single origin can push — hurts brute force and slows spraying
5 Control B spray check If one source has already failed SPRAY_ACCOUNTS distinct users, return blocked_spray Fires even though no single account is near lockout — this is the fix for the insecure version's blind spot
6 password compare On mismatch, add username to src_failed_users[src_ip] and log a failure with reason, not the password Feeds the spray detector; logs the decision, never the secret
7 Control C breached screen Even a correct password is rejected if it is in BREACHED Kills credential stuffing / reuse of leaked or seasonal passwords
8 Control D MFA gate Correct password still returns mfa_required unless the second factor verifies Makes a guessed password insufficient — the core value of MFA
9 success Only a correct password + passing MFA + clean source reaches success The asset (session) is released only after every layer clears

Tracing the verify spray: attempt 1 (alice, wrong pw) → failure, set = {alice}. Attempt 2 (bob) → failure, set = {alice, bob}. Attempt 3 (carol) → set now has 3 → blocked_spray; carol is not compromised even though her password was the sprayed one. Attempt 4 (dave) → still blocked_spray. Contrast the insecure version, where attempt 3 returned success and no alarm fired.

Common mistakes

Real mistakes, each shown as WRONG → WHY → CORRECTED → how to recognize/prevent.

1. "Account lockout is our brute-force defense, so we're covered."

  • Why wrong: Lockout counts failures per account. Spraying makes one attempt per account, so the counter never fires — and lockout can be abused to DoS your own users.
  • Corrected: Add cross-account/per-source correlation, rate limiting, and breached-password screening. Keep lockout modest (or use progressive delays) so it can't be weaponized.
  • Recognize/prevent: Alert when many distinct accounts fail from one source in a window; test your defense with a simulated spray in a lab, not just repeated failures on one account.

2. "We decoded the JWT / read the token, so the login is verified."

  • Why wrong: Decoding a token (or any credential) only reads it. Decoding a JWT is not verifying its signature; an attacker can craft a payload you'll happily decode.
  • Corrected: Always verify the signature and claims server-side against your key before trusting anything in the token.
  • Recognize/prevent: In review, look for base64-decode with no signature check. Treat "we can read it" and "we proved it's authentic" as different things.

3. "The vulnerability scan passed, so we're secure."

  • Why wrong: Passing an automated scanner does not prove a system is secure. Scanners miss logic flaws, spray exposure, and MFA-bypassing legacy protocols. Nothing is completely secure.
  • Corrected: Complement scans with authenticated testing, log-based detection review, and explicit checks that MFA is enforced on every auth path.
  • Recognize/prevent: Ask "which legacy endpoints skip MFA?" A green scan report is a starting point, not a conclusion.

4. "We enabled SMS MFA, so phishing and takeover are solved."

  • Why wrong: SMS codes are phishable and SIM-swappable; push prompts get fatigue-bombed until a tired user approves.
  • Corrected: Prefer phishing-resistant FIDO2/WebAuthn; use number-matching and prompt limits for push; drop SMS where possible.
  • Recognize/prevent: Inventory your factors by phishing-resistance. Watch logs for repeated MFA prompts to one user (fatigue signal).

5. "Let's log the attempted password so we can debug bad logins."

  • Why wrong: Logging secrets turns your log store into a credential dump — a breach amplifier and a compliance violation.
  • Corrected: Log only the decision (result, reason), source, timestamp, and correlation id — never the password, cookie, or MFA code.
  • Recognize/prevent: Grep logs for anything resembling a secret before shipping; add a lint/redaction step.

Debugging tips

When your lab defense behaves unexpectedly, work through these.

  • The spray still succeeds. Check that you correlate across accounts (src_failed_users[src_ip] is a set of usernames), not per account. A per-account counter will never catch a one-attempt spray. Print the set size on each attempt.
  • Legitimate users get blocked (false positives). Your SPRAY_ACCOUNTS/IP_RATE thresholds may be too tight, or many users share one NAT/proxy IP so the source looks like a spray. Correlate on richer signals (ASN, device, geo) and consider CAPTCHA/step-up instead of outright deny. Ask: is this one attacker, or one office behind one IP?
  • MFA isn't stopping password-only logins. Confirm the MFA gate runs after the password check and that verify_mfa fails closed (returns False) on missing/blank codes. A gate that defaults to allow is worse than none.
  • Window logic seems off. Verify _prune uses the same now you pass into login; mismatched clocks make old events linger or fresh ones vanish. In tests, pass an explicit now so timing is deterministic.
  • Nothing shows in logs. Ensure logging is configured before the first call and that failures log a reason. Silent failures mean your SOC sees nothing.

Questions to ask when auth defense fails: Am I correlating within one account or across many? Does any auth path (legacy protocol, token endpoint) skip these controls? Does the control fail open or closed? Can a normal shared IP trip my spray rule? Am I accidentally logging a secret?

Memory safety

Security & safety — detection and logging. For this topic the "safety" work is making the attack visible without creating new exposure.

What to log on every auth attempt:

  • Timestamp in UTC (to reveal slow, low-and-slow cadence).
  • Source: IP and ASN/network origin (datacenter vs residential is a strong signal).
  • Targeted resource/account (username or user id).
  • Result and the security decision: success | failure | locked | throttled | mfa_required | blocked_spray, plus a reason.
  • A correlation id to stitch related events into one session/campaign.

What to NEVER log: the attempted or actual password, session cookies or tokens, MFA/OTP codes, private keys, full payment card numbers (PANs), or unneeded PII. Log the decision, not the secret.

Events that signal abuse:

  • Many distinct usernames failing from one source in a window with ~1 failure each → password spray.
  • Many accounts tried once each with plausible passwords from datacenter IPs → credential stuffing.
  • Repeated MFA push prompts to a single user in minutes → MFA fatigue attempt.
  • Successful login preceded by failures from a different geo/ASN → possible takeover (impossible travel).
  • Auth succeeding on a legacy protocol that skips MFA → high-risk bypass path.

How false positives arise: many real users behind one corporate NAT or VPN egress IP can mimic a spray; a mail client retrying a stale password looks like brute force; travel and mobile networks trip impossible-travel rules. Tune thresholds, enrich with device/ASN context, and prefer step-up (CAPTCHA, extra MFA) over hard blocks so you protect users without locking them out.

Real-world uses

Authorized real-world use case. A penetration tester with a signed engagement scope assesses a client's Microsoft 365 tenant. In a controlled window they simulate a single-password spray against a provided test set of accounts to demonstrate exposure, then deliver the real value: findings showing which legacy protocols bypassed MFA, which accounts used breached/seasonal passwords, and a remediation plan (disable legacy auth, enforce phishing-resistant MFA, add breached-password screening, enable spray-aware alerting). The blue team then verifies each control by re-running the simulation and confirming it is now detected and blocked.

Professional best-practice habits.

Practice Beginner Advanced
Validation Enforce length-based policy; reject known-breached passwords Continuous screening against updated breach corpora; passwordless where possible
Least privilege Don't hand every account admin Just-in-time elevation; conditional access by risk
Secure defaults MFA on by default for all users Phishing-resistant FIDO2/WebAuthn as the default factor; number-matching for push
Logging Log auth decisions (never secrets) Cross-account/per-source correlation, impossible-travel, SIEM alerting
Error handling Fail closed; identical messages for bad user vs bad password Progressive throttling + step-up instead of blunt lockout

Beginner takeaway: turn on MFA everywhere, block breached passwords, and log auth decisions. Advanced takeaway: kill MFA-bypassing legacy paths, adopt phishing-resistant factors, and build detection that correlates failures across accounts and sources — then prove it works by simulating the attack in an authorized lab.

Practice tasks

All tasks are lab-only: run against your own scripts, localhost, containers, or intentionally-vulnerable VMs (e.g. a lab VM you built). Never target systems you do not own or lack written authorization to test.

Authorization checklist (before any lab):

  • I own the target or have explicit written permission and a defined scope.
  • The environment is isolated (localhost/container/VM), not production or the internet.
  • I have a rollback/reset plan and will clean up test accounts and logs afterward.

Beginner 1 — Read the fingerprint.

  • Objective: Given a sample auth log, classify each burst as brute force, stuffing, spray, or normal.
  • Requirements: Produce a table: pattern → evidence (accounts touched, failures-per-account, source spread).
  • Hints: Spray = many accounts, ~1 failure each, one source. Brute force = one account, many failures.
  • Concepts: cross-account correlation, detection signals.

Beginner 2 — Break the naive defender.

  • Objective: Run the provided insecure_login.py and show, in output, that a spray compromises the reused-password account without tripping lockout.
  • Requirements: Capture the output; write one sentence naming the insecure assumption.
  • Constraints: Lab script only; do not modify USERS to real credentials.
  • Concepts: lockout blind spot, password reuse.

Intermediate 1 — Add spray detection.

  • Objective: Extend a per-account-lockout login to also track distinct failed usernames per source and block on a threshold.
  • Requirements: Input a spray sequence; output must show a blocked_spray/alert before any account is compromised.
  • Constraints: Log the decision and reason, never the password.
  • Hints: Use a set per source; choose a window.
  • Concepts: rate limiting, anomaly detection, secure logging.

Intermediate 2 — Prove MFA closes the gap.

  • Objective: Add an MFA gate so that a correct password with no valid second factor cannot obtain a session.
  • Requirements: Verification output showing password-only → mfa_required, and password+valid-factor → success. Make verify_mfa fail closed.
  • Constraints: MFA code is a lab stub; never log the code.
  • Concepts: MFA as second factor, fail-closed design.

Challenge — Detection + remediation report (defensive conclusion).

  • Objective: In an isolated lab, simulate a spray against a small set of test accounts, detect it with your controls, then write a short finding using the template: title, severity (justified by exploitability/access/impact — not everything is critical), affected component, preconditions, safe reproduction, evidence (redacted logs, no secrets), impact, likelihood, remediation, retest.
  • Requirements: Remediation must be at least as detailed as the attack description, include a mitigation verification step (re-run the spray, show it is now blocked and alerted), and note detection/logging. Correct at least one misconception explicitly (e.g. "a passed scan ≠ secure" or "decoding a token ≠ verifying it").
  • Constraints: No real targets, IPs, hostnames, or secrets; use placeholders and RFC 5737 ranges. Cleanup/reset: remove lab test accounts, clear generated logs, and reset thresholds.
  • Concepts: full defensive loop — detect, remediate, verify, report.

Summary

  • Three online attacks: brute force (many guesses, one account — loud, easy to lock), credential stuffing (leaked pairs replayed, exploits reuse), and password spraying (one common password across many accounts, slow — evades per-account lockout). Spraying is the quiet, practical one.
  • Key insecure assumption: "no single account crossed the threshold, so nothing is wrong." Spraying defeats per-account counting; you must correlate failures across accounts by source and time.
  • Lockout is a double edge: it can be weaponized into denial of service and does nothing against spraying. Prefer throttling/backoff, source rate limiting, anomaly detection, and breached-password screening.
  • MFA changes the equation by making a guessed password insufficient — but it is not magic. Know the bypasses (MFA fatigue, SIM-swap, OTP phishing) and prefer phishing-resistant FIDO2/WebAuthn over SMS. Nothing is completely secure.
  • Detection & logging: log timestamp, source/ASN, account, result+reason, correlation id; never log passwords, tokens, or MFA codes. The spray fingerprint is many distinct usernames, ~1 failure each, from one source, slowly.
  • Misconceptions to keep straight: decoding a token ≠ verifying it; a passed scan ≠ secure.
  • Common mistakes: trusting lockout alone, SMS-only MFA, logging secrets, failing open on MFA.
  • Remember: defense in depth — slow hashing, length policy, breached-password screening, sane throttling, and MFA everywhere — then verify each control by simulating the attack in an authorized lab and confirming it is detected and blocked.

Practice with these exercises