Platform: 0xVoid (wave2) Challenge: Loopback Lens Category: Web Subcategory: SSRF Points: 500 Author: 0x4sh
The public lens can fetch pages for you, but its private calibration route is supposed to exist only from inside the machine. The guard checks the obvious faces of localhost. Give it a less obvious mirror.
Target: http://35.192.106.100:21001/
TL;DR
/lens?url= is a classic SSRF fetcher guarded by a naive string blocklist (127., localhost, 0.0.0.0, ::1). Numeric loopback encodings — decimal 2130706433, hex 0x7f000001, bare 0 — slip past the blocklist yet still resolve to 127.0.0.1. Point the SSRF at the internal app on port 8080 and hit the internal-only route:
Recon — understand the endpoint
The homepage advertises the fetcher and the hint outright:
Sanity check the SSRF works (send from Burp Repeater):
→ 200 OK, Example Domain HTML wrapped in <pre>. Confirmed: the server fetches arbitrary URLs and echoes the body back.
Map the filter
Throw the obvious localhost forms at it in Repeater and watch the responses:
url= payload | Result | Meaning |
|---|---|---|
127.0.0.1 | 400 blocked | blocklist hit |
http://127.0.0.0 | 400 blocked | blocks substring 127. |
http://127.127.127.127 | 400 blocked | blocks substring 127. |
http://[0:0:0:0:0:ffff:127.0.0.1] | 400 blocked | contains 127.0.0.1 |
http://[::1]/ | 400 blocked | blocks ::1 |
LOCALHOST | 400 blocked | blocklist is case-insensitive |
file:///flag.txt | 400 blocked | non-http scheme blocked |
http://2130706433/ | 502 URLError | passed filter, connect failed (nothing on :80) |
http://0x7f000001/ | 502 URLError | passed filter |
http://0/ | 200 (later) | passed filter, resolves to loopback |
Conclusion: the guard is a pure string blocklist — it blocks 127., localhost (any case), 0.0.0.0, ::1, and non-http(s) schemes. It does not resolve the host and check is_loopback, so any alternate numeric encoding of 127.0.0.1 walks straight through.
Why this works: 2130706433 is the 32-bit integer form of 127.0.0.1 (127*256³ + 0 + 0 + 1). inet_aton() / the socket layer happily parses decimal, hex (0x7f000001) and octal integer IPs, so the OS still connects to loopback — but the string never contains the blocked substrings.
Find the internal service (port scan via SSRF)
Reaching loopback on port 80 gave URLError (nothing listening), so sweep common ports with the decimal bypass in Repeater:
| Port | Result |
|---|---|
| 80, 3000, 5000, 8000, 8888, 9000, 9090, 1337, 21001 | 502 URLError (closed) |
| 8080 | 200 OK — returns the app homepage |
The app itself is served internally on 127.0.0.1:8080 (21001 is just the external port-map). The private route lives on this internal app.
Reading the error codes is the whole game here:
502 fetch failed: URLError→ TCP connect failed → port closed.502 fetch failed: HTTPError→ the internal server answered with a non-2xx (404/403/400) → port open, wrong path/guard.200 OK→ internal server returned 2xx → the lens echoes the body (this is what we want).
(Optional) Confirm the outbound request with Collaborator
To understand what the guard actually sees, aim the lens at a Burp Collaborator payload and inspect the interaction:
Collaborator's HTTP interaction shows exactly what the fetcher sends:
Takeaways:
- The
Hostheader the internal app receives = the host inurl=(soHost: 2130706433:8080). - The fetcher does not copy your request headers (
User-Agentis hard-coded), so you can't spoofHost: 127.0.0.1from the outside — the "inside only" gate depends on the real loopback connection, which is exactly what the SSRF gives you.
Locate the private route
Path-brute the internal app through the decimal bypass. Direct external requests to guessed paths return the generic 404 not found, and non-2xx paths via the lens all collapse to HTTPError, so brute and watch for a 200:
Tip: send it to Burp Intruder — mark the path as the payload position:
Wordlist ideas: calibration, calibrate, internal, internal/flag, flag, flag.txt, admin, private, secret, calibration/flag. Sort by response length / status — the flag response stands out (200, distinct length).
Hit:
Root cause & remediation
Root cause: SSRF protection implemented as a string blocklist on the raw URL. It never resolves the hostname, so alternate representations of loopback (decimal / hex / octal integer IPs, 0, obscure IPv6 forms) bypass it, and any "internal only" route that trusts a loopback connection becomes reachable.
Fix:
- Resolve the hostname first, then reject if any resolved address is loopback / link-local / private / multicast (
ipaddress.ip_address(...).is_loopback,is_private,is_link_local,is_reserved). - Re-resolve and re-validate after redirects (guard against DNS rebinding / redirect-to-internal).
- Use an allowlist of destination hosts and schemes instead of a blocklist.
- Don't rely on
Host/X-Forwarded-Foror "came from loopback" as an auth boundary for sensitive routes — put real authentication on them.
Cheat-sheet
- Localhost mirrors that bypass string filters:
- Decimal:
2130706433 - Hex:
0x7f000001 - Octal:
0177.0.0.1/017700000001 - Short:
127.1,0 - IPv6:
[::1],[::ffff:127.0.0.1],[0:0:0:0:0:ffff:7f00:1] - DNS:
localtest.me,*.nip.io(e.g.127.0.0.1.nip.io)
- Decimal:
- Distinguish SSRF outcomes by error text:
URLError= port closed,HTTPError= open + non-2xx,200= win. - Use Collaborator to see the exact request the fetcher emits (headers, UA, redirect behaviour) — it removes guesswork about what the internal guard sees.
0xAdham
