2.2 KiB
← What SSRF costs you · Module README · Allow, not block →
2. The exploit, start to finish
Run the application with no profile, so there is no InetAddressFilter bean at all:
./scripts/run.sh
./scripts/exploit.sh
The transcript is committed at
docs/output/exploit-by-profile.txt. The first block is this
one:
http://127.0.0.1:8080/internal/credentials FETCHED | {"SecretAccessKey":"wJalrXUtnFEMI-...
http://localhost:8080/internal/credentials FETCHED | {"SecretAccessKey":"wJalrXUtnFEMI-...
http://[::1]:8080/internal/credentials FETCHED | {"SecretAccessKey":"wJalrXUtnFEMI-...
http://172.16.10.3:8080/internal/credentials FETCHED | {"SecretAccessKey":"wJalrXUtnFEMI-...
http://example.com/ FETCHED | <!doctype html>...
Four targets, four sets of credentials, and the legitimate outbound call still works. That last row matters as much as the others: any mitigation has to leave it intact, and one of the configurations in chapter 3 does not.
Why four targets and not one
127.0.0.1 is the one every tutorial blocks. The others are why a hand-written check usually
leaks:
| Target | What it defeats |
|---|---|
localhost |
checks written against the literal string 127.0.0.1 |
[::1] |
checks that only ever consider IPv4 |
172.16.10.3 |
checks that stop at loopback and forget RFC 1918 |
The fourth is this container's own address on its network interface. It is the same process, reached the same way, over a route that a loopback-only rule does not cover. In a real deployment it is the pod next door.
Blocking by string is hopeless in a way that is easy to underestimate. 0x7f.0.0.1,
2130706433, 127.1, a DNS name you control that resolves to 127.0.0.1, and a redirect from a
public URL to a private one all reach loopback without the string 127.0.0.1 appearing anywhere
in the request. This is why the check belongs at address-resolution time and not in a validator
over the URL — which is exactly what InetAddressFilter is.