August 14, 2026
picoCTF: No FA
Challenge Description
By Parthib Dewanjee
3 min read
Challenge Description
- CTF Name: picoCTF
- Challenge Name: No FA
- Category: Web Exploitation
- Difficulty: Medium
- Author: Darkraicg492
Problem Description
Seems like some data has been leaked! Can you get the flag?
You can get started here to find the flag!
The application code can be found here
The leaked data can be found here
Hints
1. What happens when there's no salt?
2. rockyou rockyou rockyou
3. What makes 2FA safe?
The challenge provides the following downloadable files: app.py (Application Source Code) and users.db (Leaked Database Dump).
Initial Analysis
Understanding the Hints
- Hint 1 ("What happens when there's no salt?"): Indicates that password hashes stored in the database were created without any cryptographic salt. This allows us to perform an offline dictionary attack to easily crack the hashes.
- Hint 2 ("rockyou rockyou rockyou"): Points directly to using the famous
rockyou.txtwordlist for cracking the extracted password hashes. - Hint 3 ("What makes 2FA safe?"): Highlights a fundamental architectural flaw in the Two-Factor Authentication (2FA) implementation (specifically storing the generated OTP inside client-side session cookies).
Strategy
- Extract Database Credentials: Locate the
adminuser's raw SHA-256 password hash from the leakedusers.dbfile. - Hash Cracking: Crack the admin hash using John the Ripper combined with
rockyou.txt. - Session Cookie Decoding (OTP Bypass): Log in as admin and decode the client-side Flask session cookie to retrieve the generated OTP secret directly.
- Retrieve Flag: Submit the leaked OTP on the
/two_faendpoint to bypass authentication and capture the flag.
Solution Steps
Step 1: Code Inspection & Admin Password Cracking
First, inspecting the source code app.py reveals that user authentication relies on raw, unsalted SHA-256 hashes:
hashlib.sha256(password.encode()).hexdigest() == user['password']hashlib.sha256(password.encode()).hexdigest() == user['password']Checking the leaked users.db SQLite database yielded the SHA-256 hash for the admin account: c20fa16907343eef642d10f0bdb81bf629e6aaf6c906f26eabda079ca9e5ab67
I saved this hash into hash.txt and executed John the Ripper with rockyou.txt:
echo 'c20fa16907343eef642d10f0bdb81bf629e6aaf6c906f26eabda079ca9e5ab67' > hash.txt
john --format=Raw-SHA256 --wordlist=/usr/share/wordlists/rockyou.txt hash.txtecho 'c20fa16907343eef642d10f0bdb81bf629e6aaf6c906f26eabda079ca9e5ab67' > hash.txt
john --format=Raw-SHA256 --wordlist=/usr/share/wordlists/rockyou.txt hash.txtResult: The plaintext password for the admin user was cracked: apple@123
Step 2: Analyzing Session Storage & Leaking the OTP
Analyzing the /login and /two_fa routes in app.py exposes a severe client-side session security vulnerability:
otp = str(random.randint(1000, 9999))
session['otp_secret'] = otpotp = str(random.randint(1000, 9999))
session['otp_secret'] = otpFlask uses client-side signed cookies by default. These cookies are base64-encoded JSON payloads โ signed for integrity, but not encrypted. Consequently, any user can inspect and decode their session cookie client-side.
Exploit Process:
- Logged into the application with username
adminand passwordapple@123.
- The server verified the credentials, redirected to the
/two_fapage, generated a random 4-digit OTP, and stored it inside the session.
- I copied the
sessioncookie value from Browser DevTools (F12 -> Application -> Cookies) and decoded it usingflask-unsign:
flask-unsign --decode --cookie '.eJwty0sKgCAQANC7zFpiGtQ-l4khJwn8obaK7l6Ltg_eDSF7Lw5WODg0AQW5l63JXqV_SETmt35GaZ1jgXWcZmtpQaRBTxoRjYKrSU0c5Uvs4pngeQFDfgHaeQWBHsVWiJOhcPsc'flask-unsign --decode --cookie '.eJwty0sKgCAQANC7zFpiGtQ-l4khJwn8obaK7l6Ltg_eDSF7Lw5WODg0AQW5l63JXqV_SETmt35GaZ1jgXWcZmtpQaRBTxoRjYKrSU0c5Uvs4pngeQFDfgHaeQWBHsVWiJOhcPsc'
The 4-digit OTP was directly revealed in the decoded session payload: 2225
Step 3: Bypassing 2FA and Retrieving the Flag
- Navigated back to the
/two_faOTP verification page in the browser. - Entered the leaked OTP
2225and clicked Verify & Login. - The 2FA check succeeded, setting
session['logged'] = 'true', granting full admin access, and rendering the flag on the home page.
Flag: picoCTF{n0_r4t3_n0_4uth_487507fc}
Key Takeaways
- Always Salt Passwords: Storing raw SHA-256 hashes without unique salts makes credentials extremely susceptible to dictionary attacks using tools like John the Ripper or Hashcat.
- Never Store Secrets in Client-Side Sessions: Flask session cookies are signed to prevent tampering, but they are not encrypted. Storing sensitive values (like 2FA OTP codes, reset tokens, or secrets) inside session cookies allows users to read them freely.
- Implement Rate-Limiting: Authentication mechanisms (especially 2FA/OTP validation routes) must enforce strict rate-limiting policies to mitigate brute-forcing. Without rate-limiting, short numeric OTP tokens can be rapidly exhausted even if the secret is hidden successfully.
Thank you for reading! If you enjoyed this writeup, feel free to follow for more CTF solutions and cybersecurity content.