Skip to main content0xAdham

Command Palette

Search for a command to run...

0xVoid: Loopback Lens

Written by
Avatar of 0xAdham
0xAdham
Published on
--
0xVoid: Loopback Lens

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= payloadResultMeaning
127.0.0.1400 blockedblocklist hit
http://127.0.0.0400 blockedblocks substring 127.
http://127.127.127.127400 blockedblocks substring 127.
http://[0:0:0:0:0:ffff:127.0.0.1]400 blockedcontains 127.0.0.1
http://[::1]/400 blockedblocks ::1
LOCALHOST400 blockedblocklist is case-insensitive
file:///flag.txt400 blockednon-http scheme blocked
http://2130706433/502 URLErrorpassed filter, connect failed (nothing on :80)
http://0x7f000001/502 URLErrorpassed 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:

PortResult
80, 3000, 5000, 8000, 8888, 9000, 9090, 1337, 21001502 URLError (closed)
8080200 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 Host header the internal app receives = the host in url= (so Host: 2130706433:8080).
  • The fetcher does not copy your request headers (User-Agent is hard-coded), so you can't spoof Host: 127.0.0.1 from 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:

  1. 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).
  2. Re-resolve and re-validate after redirects (guard against DNS rebinding / redirect-to-internal).
  3. Use an allowlist of destination hosts and schemes instead of a blocklist.
  4. Don't rely on Host / X-Forwarded-For or "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)
  • 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

Edit on GitHub
Last updated: --