Peter Rakolcza Get in touch
← back to blog

From Blind SSRF to Cloud Takeover: Chaining the Boring Bug

Server-Side Request Forgery rarely ends at the bug bounty triage queue. Here is how a 'low-impact' blind SSRF becomes full cloud compromise when you understand the metadata layer.

Most reports I review stop at the same sentence: “The application makes a request to a user-controlled URL.” Triage marks it low, the bounty is modest, everyone moves on. That sentence is where the interesting work begins, not where it ends.

This is a sanitised walk-through of how a blind SSRF — the kind that returns no response body — turns into a path to cloud account compromise. No client, no target; just the methodology.

Why “blind” is a state of mind

A blind SSRF gives you no output. You point the server at a host, and you get a generic 200 or a timeout. The instinct is to give up. The discipline is to build an oracle.

You almost always have something: response time, status codes, error strings, or an out-of-band channel. Each is a single bit of information, and a few bits are enough to read a remote service.

# Time-based oracle: open ports answer fast, filtered ports hang
for port in 80 443 6379 8500 9200; do
  t=$(curl -s -o /dev/null -w "%{time_total}" \
    "https://target/fetch?url=http://169.254.169.254:$port/")
  echo "port $port -> $t"
done

If port 80 on 169.254.169.254 answers in 40ms and everything else hangs for the full timeout, you have just discovered that the application is running in a cloud environment with an exposed metadata endpoint — without ever seeing a response body.

The metadata layer is the real target

The link-local address 169.254.169.254 is the instance metadata service (IMDS). On a misconfigured or IMDSv1 host, it hands out temporary credentials to anyone who can make it issue a request. SSRF makes the server issue that request for you.

The vulnerability is in the application. The blast radius is in the cloud identity attached to it.

The catch with blind SSRF is exfiltration. You can reach IMDS, but you cannot read the JSON it returns. You need to turn the response into something observable.

Turning a blind read into data

Three techniques, in order of how often they work:

  1. Gadget chaining — find a second endpoint that reflects or stores the fetched content (an avatar importer, a PDF generator, a webhook validator). Route the IMDS response through that.
  2. DNS exfiltration — if the fetcher resolves attacker-controlled hostnames, encode bytes into subdomains and read them off your authoritative server.
  3. Response-shape oracle — diff timing or error behaviour for valid vs. invalid credential prefixes, and brute-force the token a few characters at a time. Slow, ugly, but it works.
GET /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ HTTP/1.1
Host: target

Once a role name leaks, the credentials live one path deeper. From there it is the AWS CLI and whatever IAM policy the developers attached “temporarily” eighteen months ago.

The fix is older than the bug

None of this is exotic. The mitigations are well known and routinely skipped:

  • Enforce IMDSv2 (session-token bound, hop-limited) and disable v1 outright.
  • Treat the URL fetcher as a deny-by-default component: allowlist destinations, resolve DNS once, and block link-local and private ranges after resolution.
  • Scope the instance role to the minimum. A web server almost never needs iam:*.

The lesson I keep relearning: severity is not a property of a bug, it is a property of the system the bug lives in. A blind SSRF in a static-site generator is noise. The same bug in front of an over-privileged cloud role is an incident. Always ask what is one request away.