Just Health Walkthrough — Exfiltrating /flag Through a 31-Hop Redirect Chain
Platform: CyberTalents · Category: Web · Difficulty: Medium/Hard
Goal: Read /flag, which only responds to requests from 127.0.0.1.
Challenge
“Just Health” is a Flask health-check application. You create checks (HTTP, ping, DNS…) and it probes them on a schedule. The source is provided (Dockerfile, app/).
The flag lives at /flag — but only for localhost:
@auth_bp.route("/flag")
def flag():
if request.remote_addr != "127.0.0.1":
abort(403)
with open("/flag.txt") as f:
return f.read(), 200, ...
We’re not localhost. But the app does make outbound requests on our behalf — an SSRF primitive.
The vulnerability: SSRF + response reflection
The HTTP health check uses:
requests.get(url, allow_redirects=True, ...)
The URL is fully user-controlled. Normally the response body is not returned — the result message is just “HTTP 200”.
But look at the exception handler:
except requests.exceptions.TooManyRedirects:
dest = requests.get(last_location, allow_redirects=False, ...)
message = f"Too many redirects ... final destination {dest.text[:8192]}"
When requests raises TooManyRedirects (it does so while processing the 31st redirect response), the app fetches the last Location header with redirects disabled and reflects up to 8192 bytes of its body into the check result.
So the plan: build a redirect chain of 31 hops whose 31st response has Location: http://127.0.0.1:5000/flag (the app listens on port 5000 inside the container — nginx proxies 80 externally). When the chain is followed:
- hop 31 →
TooManyRedirectsfires - app fetches
http://127.0.0.1:5000/flagdirectly → 200 + flag body - flag is reflected into the result message we can read
Building the chain (the hard part)
The check URL is limited to 2048 characters — a pure httpbin nesting of 31 levels (~7KB) won’t fit. We need URL shorteners that:
- are reachable from the challenge server with python-requests’ default UA (no bot-blocking)
- don’t deduplicate or blacklist targets
- redirect anywhere — including
127.0.0.1
The hops
- tinyurl — accepts
http://127.0.0.1:5000/flagdirectly, serves 301 to any UA. Perfect final hop. - clck.ru — each clck link is itself 2 hops:
clck.ru/xxx → 302 → sba.yandex.ru/redirect?url=<target> → 302 → <target>. - httpbin
redirect-to?url=<next>— one hop to chain into the next clck link.
So one clck link whose target is httpbin.org/redirect-to?url=<next-clck-link> gives 3 hops: clck → sba → httpbin → next clck.
The chain
c1 → sba → hb(c2) → c2 → sba → hb(c3) → ... → c10 → sba → hb(T) → T → 301 → /flag
10 clck links × 3 hops + 1 tinyurl hop = 31 redirects. The 31st response is tinyurl’s 301 with Location: http://127.0.0.1:5000/flag — exactly what TooManyRedirects needs.
Dead ends we hit (worth knowing)
- cleanuri.com — 404s the python-requests UA (bot protection). Useless.
- is.gd / v.gd — blacklist shortener domains and follow the chain to reject localhost destinations. Useless.
- clck.ru — dedups by raw target string, and passes clck.ru targets through without creating a new record → you cannot chain clck→clck directly; you must interleave httpbin.
- sba.yandex.ru — throttles hard after ~10 requests (403). Keep sba hops ≤ 10 — exactly why the chain uses 10 clck links.
- spoo.me — rejects localhost in targets.
- Port:
127.0.0.1:80is refused inside the container; the app binds0.0.0.0:5000.
Exploit flow
- Create the tinyurl link to
/flag, then build c10 → c1 backwards (eachclck(hb(next))). - Login (any account works), add a new HTTP check whose URL is
c1. - Trigger the check run.
- The result message contains the reflected flag.
Flag{QCFAWHJYdTlNMFpIRWF1QWcxZURpSGgrSjE2bTRCR3dpS3V4dWNBRU9UV1U5ST1iMjg3YjdkZjJiMThhNDgy}
Note (2026-08-31): the flag rotates per instance. Fresh instance yielded a different flag with the same exploit, unchanged.
Why this worked (the real lesson)
- SSRF with a side channel. The classic fix for blind SSRF is “you can’t see the response” — this app broke that by reflecting the final redirect destination’s body in an error path.
- The 31-hop trick.
TooManyRedirectsfires after receiving the 31st redirect — the app then fetches the last Location from the server side, turning our attacker-chosen URL into a server-side request to localhost. - Shortener ecosystems are unpredictable. The same chain collapses on one service (dedup, bot walls, localhost blacklists) and works on another — recon of each hop’s behavior was half the solve.
Defense takeaways:
- Block loopback/private ranges in any URL-fetching feature (SSRF allowlists + DNS rebinding protection).
- Don’t follow — let alone reflect — arbitrary redirect destinations.
- Never expose response bodies of server-side fetches, even in error messages.
Files
exploit4.py— the working exploit (chain build + check create + run)test_redirect.py— local simulation of the TooManyRedirects reflection
Writeup of a solved CyberTalents web challenge. All attack surface described here is part of the challenge’s intended vulnerable application.