Threat Hunting Exercise: Analyzing AWS IAM PassRole Privilege Escalation Dataset
Returning to CloudTrail for T1078.004: Valid Accounts - Cloud Accounts combined with IAM privilege escalation, where a compromised low-privilege IAM user with an over-permissioned iam:PassRole + lambda:CreateFunction combination creates a Lambda function that assumes a highly-privileged role, executing arbitrary AWS API calls as if it were an administrator - one of the most common real-world AWS privesc paths (à la the "PrivEsc" cheat sheets).
Step 1: Hypothesis Formation
Hypothesis: A compromised IAM user with no direct admin rights, but with iam:PassRole and lambda:CreateFunction/lambda:InvokeFunction permissions, creates a new Lambda function, passes it an existing high-privilege execution role (e.g., the org's AdminLambdaRole), and invokes it to run privileged API calls (e.g., creating a new IAM admin user or attaching AdministratorAccess policy) - all attributable back to the Lambda's assumed role rather than the original user, evading simple "who did X" review. Indicators:
- CloudTrail
CreateFunctionevent withroleparameter referencing a role the calling user could not otherwise assume directly. - CloudTrail
InvokeFunctionshortly after creation. - Subsequent CloudTrail events (
CreateUser,AttachUserPolicy,CreateAccessKey) withuserIdentity.type=AssumedRolematching the Lambda's execution role, occurring within the Lambda's invocation window. - Originating IAM user has no prior history of Lambda usage (baseline anomaly).
Null Hypothesis: Legitimate DevOps deployment of a new Lambda function using a shared execution role per IaC pipeline convention. Invalidate via the specific combination of a brand-new, narrowly-scoped user creating admin-equivalent IAM objects immediately after Lambda invocation, and absence of any CI/CD user-agent or CloudFormation/Terraform stack correlation.
Rationale: This complements the earlier EC2/S3 exfiltration hunt in this series by covering a different cloud attack surface - privilege escalation through IAM misconfiguration rather than direct data theft - a critical and underhunted AWS pattern.
Step 2: Data Sources and Scope
- Sources: AWS CloudTrail (management events: iam.amazonaws.com, lambda.amazonaws.com); AWS Config for role/policy drift; GuardDuty findings if enabled (PrivilegeEscalation:IAMUser/*).
- Scope: ~2022-04-07T11:20:00-11:23:00 UTC; Account: 123456789123; IAM User: contractor-devops (compromised via leaked access key); Role: AdminLambdaRole.
- SIEM Queries (Athena/Splunk Cloud):
- PassRole+Lambda:
eventName='CreateFunction' AND requestParameters.role LIKE '%Admin%' - Invoke-then-IAM-change:
eventName IN ('InvokeFunction') | join within 2m [search eventName IN ('CreateUser','AttachUserPolicy','CreateAccessKey') userIdentity.type='AssumedRole'] - Baseline anomaly:
userIdentity.arn='arn:aws:iam::123456789123:user/contractor-devops' AND eventSource='lambda.amazonaws.com' AND NOT seen in prior 90 days
Step 3: Key Findings
Parsed CloudTrail events (9 shown) confirm the full escalation chain: role-passing Lambda creation, invocation, and resulting admin-equivalent IAM object creation, all within 3 minutes.
| Timestamp (UTC) | Event Name | User Identity | Key Details | IOC/Why Suspicious? |
|---|---|---|---|---|
| 2022-04-07 11:20:02 | GetCallerIdentity | IAMUser: contractor-devops | userAgent=aws-cli/2.4.6; sourceIPAddress=185.220.101.33 (Tor exit node range). | Recon IOC: Basic identity check right after obtaining leaked credentials - common first move for stolen-key validation. |
| 2022-04-07 11:20:05 | ListRoles | IAMUser: contractor-devops | Enumerates all IAM roles in the account. | Actor surveys available roles to find one to abuse via PassRole. |
| 2022-04-07 11:20:30 | GetRole | IAMUser: contractor-devops | roleName=AdminLambdaRole; policy attached=AdministratorAccess. | Targeting IOC: Identifies the over-privileged Lambda execution role - the actual vulnerability being exploited. |
| 2022-04-07 11:21:00 | CreateFunction | IAMUser: contractor-devops | functionName=maintenance-task; role=arn:aws:iam::123456789123:role/AdminLambdaRole; runtime=python3.9; code=inline zip (60 lines). | Core IOC: contractor-devops has no direct admin rights but successfully passes an admin-equivalent role to a new Lambda - the PassRole+CreateFunction combination is the actual privesc primitive. |
| 2022-04-07 11:21:30 | InvokeFunction | IAMUser: contractor-devops | functionName=maintenance-task; invocationType=RequestResponse. | Triggers the function, which now runs with AdminLambdaRole's permissions. |
| 2022-04-07 11:21:32 | CreateUser | AssumedRole: AdminLambdaRole (via Lambda) | userName=svc-backup-sync (innocuous-looking name); sourceIPAddress=AWS internal (Lambda execution environment). | Escalation IOC: New IAM user created by the assumed role, not by contractor-devops directly - obscures attribution at a glance. |
| 2022-04-07 11:21:35 | AttachUserPolicy | AssumedRole: AdminLambdaRole (via Lambda) | userName=svc-backup-sync; policyArn=arn:aws:iam::aws:policy/AdministratorAccess. | Privilege IOC: Full admin policy attached to the newly created, innocuously-named backdoor user. |
| 2022-04-07 11:21:40 | CreateAccessKey | AssumedRole: AdminLambdaRole (via Lambda) | userName=svc-backup-sync; accessKeyId=AKIA...(new). | Persistent long-lived credential created for the backdoor account - survives Lambda deletion. |
| 2022-04-07 11:23:00 | DeleteFunction | IAMUser: contractor-devops | functionName=maintenance-task. | Cleanup IOC: Function deleted immediately after use - removes the obvious CreateFunction/InvokeFunction trail from casual console review. |
Validation:
- Timeline: Recon → role targeting → function creation → invocation → backdoor user creation → cleanup, all within 3 minutes - fast, scripted, and deliberate.
- False Positives: No CloudFormation/Terraform stack ID or CI/CD user-agent on any event; contractor-devops has zero prior Lambda activity in 90 days of baseline history; the source IP is a known Tor exit range, inconsistent with the contractor's normal corporate VPN egress.
- Correlation:
AssumedRoleevents citeAdminLambdaRolewith a session name tying back to the Lambda function ARN, directly linking the "anonymous" admin actions to contractor-devops's original CreateFunction call.
Step 4: Recommendations & Next Steps
- Response: Disable the contractor-devops access key immediately; delete the svc-backup-sync backdoor user and its access keys; review and remove
iam:PassRolefor any role broader than the specific function needed (principle of least privilege on PassRole is the actual fix); rotate all credentials with any Lambda-execution-role exposure. - Detection: GuardDuty
PrivilegeEscalation:IAMUser/AnomalousBehavior; custom Sigma-for-CloudTrail:title: Lambda PassRole PrivEsc→selection: eventName='CreateFunction' role contains 'Admin' | followed_by: eventName='InvokeFunction' | followed_by: eventName IN ('CreateUser','AttachUserPolicy') userIdentity.type='AssumedRole' within 5m. - Pro Tip:
iam:PassRoleis one of the most under-scrutinized permissions in AWS - audit every principal that holds it against every role it can pass, not just against the roles it can directly assume.
Hypothesis confirmed - IAM privilege escalation via Lambda PassRole abuse, complete with a persistent admin backdoor account! CloudTrail's AssumedRole attribution chain was essential to unmask the true actor behind the "anonymous" admin actions.