OWASP Top 10:2025 Course — Module 4 of 10. Course home · Previous: Module 3 — Software Supply Chain Failures · Next: Module 5 — Injection
Learn first: what is a Cryptographic Failure
A04:2025 covers data exposed because encryption was missing, weak, or misconfigured — not because any clever exploit broke the math. The two patterns in this module are both common and both boring to execute once the weakness exists: plaintext HTTP that any on-path observer can read, and fast, unsalted password hashing that a GPU chews through in no time.
Client ──HTTP (plaintext)──▶ [on-path proxy / MITM] ──▶ Server
│
└─ Set-Cookie: session_id=abc123 <- readable in cleartext,
no Secure/HttpOnly/SameSite
Attacker captures the header, replays the exact cookie value directly
to the server — no password needed, no "hacking," just reading the wire.
Switch to HTTPS with a certificate the client actually trusts, and the same on-path proxy can’t read (or MITM) anything — it just gets a certificate error.
Scenario briefing
You’re reviewing CryptoBank, a demo banking app, on an engagement that includes testing for data exposed in transit — the kind of review you’d do before a client connects over public Wi-Fi.
Lab setup
- Download: Download the lab (zip)
- Prerequisites: Docker Engine 20+, Docker Compose v2,
curl. - Start it:
cd owasp-a04-cryptographic-failures-lab docker compose up -d --build - Check it worked:
curl -s --proxy 127.0.0.1:8080 http://127.0.0.1:5000/ -o /dev/null -w "%{http_code}\n" # 200, via the on-path proxy
Missions
Mission 1 — capture a cookie in flight. Route a login through the on-path proxy (127.0.0.1:8080) and see what it captures.
Show solution
Solution — Mission 1
curl -s --proxy 127.0.0.1:8080 -d "username=alice&password=hunter2" http://127.0.0.1:5000/login
docker compose logs vulnerable | grep CAPTURED
[CAPTURED] http://127.0.0.1/login -> Set-Cookie: session_id=<32 hex chars>; Path=/
The proxy addon (sniff_cookies.py) just watches for Set-Cookie/Cookie headers — this is exactly what an attacker on the same Wi-Fi, or anyone who’s downgraded your connection from HTTPS to HTTP, sees.
Mission 2 — replay the stolen cookie. Use the captured session_id value directly, with no login at all, and read the account page.
Show solution
Solution — Mission 2
curl -s -H "Cookie: session_id=<the captured value>" http://127.0.0.1:5000/account
Because the cookie carries no Secure/HttpOnly flags, nothing stops you from setting it by hand from a completely different client. The page returns the account balance and FLAG{a04-cookie-stolen-in-cleartext} — full session takeover with zero password cracking.
Mission 3 — try the same attack over HTTPS. Point the same proxy capture at the fixed app’s HTTPS port, 5443.
Show solution
Solution — Mission 3
curl -s --proxy 127.0.0.1:8080 --max-time 5 https://127.0.0.1:5443/
The request fails outright (connection/certificate error) — mitmproxy can’t forge a certificate your client is willing to trust, so it can’t sit in the middle of a TLS connection the way it could with plain HTTP. This is the fix in action, not something you need to write code to prove.
Mission 4 — compare hashing algorithms (no flag, build your intuition). Run the benchmark script.
Show solution
Solution — Mission 4
docker compose exec vulnerable python /work/hash_comparison.py
MD5 and SHA-256 both hash on the order of a million times a second on commodity hardware — great for checksums, terrible for passwords, because an attacker with a leaked hash database can try that many guesses per second too. Argon2id deliberately runs at roughly 16 hashes/sec on the same hardware — slow enough that brute-forcing a leaked hash database becomes impractical, while a single real login is still instant for a legitimate user.
Hints
- The attack only "works" because the request goes through the proxy (
--proxy 127.0.0.1:8080) — without that flag you’re just talking to the app directly, nothing to capture. fixed_app.pylistens on5443, not5000— the port is part of the fix (it’s a completely separate HTTPS listener, not a toggle on the same port).
Fix it + re-test
Three independent fixes, already applied in app/fixed_app.py: serve HTTPS only with a real certificate, hash passwords with Argon2id instead of MD5, and set Secure, HttpOnly, and SameSite=Lax on the session cookie. Re-run mission 3’s proxy capture against :5443 — it fails, which is the whole point.
Detect it
You can’t "log" a successful MITM after the fact in the same way you log a failed login — the fix here is preventive and network-level: enforce HTTPS with HSTS so browsers refuse to downgrade, monitor for unexpected certificate changes (certificate transparency logs, pinning), and treat any cleartext HTTP in production traffic as an incident, not a tuning problem.
Quick self-check
- Why does
Secure/HttpOnly/SameSiteon a cookie not help at all if the connection is still plain HTTP? - Why is "we’ll add HTTPS later" a bigger risk for session cookies specifically than for, say, static marketing pages?
- Why does Argon2id intentionally run slower than MD5 or SHA-256?
Show answers
Answers
- Those flags control how the browser handles the cookie (not sending it over JS, not sending it cross-site) — they do nothing about the cookie being readable by anyone who can see the raw network traffic, which is the attack this module demonstrates.
- A leaked session cookie is immediate full-account takeover with no further steps; a leaked static page is, at worst, a cache-timing curiosity — the blast radius is completely different.
- Speed is exactly the attacker’s advantage when brute-forcing a leaked hash database — a deliberately slow, memory-hard function like Argon2id makes guessing billions of passwords computationally expensive, while a single real login (one hash operation) stays fast enough for a real user.