IAM Role Trust Policy Abuse for Bug Bounty Hunters
Trust policies decide who may call sts:AssumeRole. Permissions policies decide what that role can do afterward. Hunters often quote the permissions JSON and miss the trust document. Flip that order. If you cannot assume the role, the attached admin policy is theoretical.
The interesting bugs are trust that is wider than the team intended.
Patterns that keep paying
"Principal": { "AWS": "*" }with a weak or missing external ID- Trust on an entire account root when only one CI role should assume
Serviceprincipals left over from an old Lambda or EC2 use case while humans still attach admin policies- Federated principals with a claim condition that never actually matches—or matches too broadly (
subwildcards, missingaud)
External ID is not magic. If the trusted principal is still *, anyone who learns the external ID can try to assume. Treat the ID like a shared secret that often ends up in Terraform state, issue trackers, or onboarding docs. Search public repos for sts:ExternalId and partner account numbers next to role ARNs. That combo is how cross-account assume bugs leave the building.
Also watch for trust updates that widen temporarily during a migration and never shrink. CloudTrail UpdateAssumeRolePolicy events, when you have access to logs in a pentest, tell that story cleanly.
How to test without being destructive
You need an identity you control. That might be your own AWS account (if the program allows cross-account tests), a sandbox role the target gave researchers, or a low-privilege principal already obtained in-scope.
Attempt:
sts:AssumeRole
RoleArn: arn:aws:iam::TARGET:role/SuspiciousRole
RoleSessionName: bugflare-canary-48192
Success returns temporary keys. Stop there for the assumption proof. Then call the least-privileged read that shows impact: sts:GetCallerIdentity, s3:ListBucket on a non-sensitive bucket named in the policy, or secretsmanager:ListSecrets if the role clearly grants it. Do not dump production secrets into your report.
If assume fails with AccessDenied, save the error. Sometimes a condition key like sts:ExternalId or aws:PrincipalOrgID is the only gate—document that rather than inventing a bypass. A failed assume with a clear condition is still useful context when combined with a second bug that leaks the external ID.
Session duration matters for severity narrative. A role that hands out twelve-hour sessions with admin rights is uglier than a fifteen-minute read-only session. Capture Expiration from the STS response.
Confused deputy and service roles
Classic shape: an account trusts edgelambda.amazonaws.com or a partner SaaS account "temporarily," then forgets to remove it. If you can trigger that service with attacker-controlled parameters, you may inherit the role. Prove it by showing the trust statement plus a controlled invocation path, not by claiming every service principal is vulnerable.
GitHub Actions OIDC and GitLab CI federated roles belong in this family too. Wrong sub conditions let another repo assume the deploy role. Same trust-policy brain, different issuer.
Reporting
Lead with: who can assume, from where, and what one successful session can touch. Include the trust policy, the permission boundary that matters, session tags if any, and your canary GetCallerIdentity output. Recommend replacing account-wide principals with specific role ARNs, requiring external IDs where cross-account is real, scavenging unused service trusts quarterly, and alerting on unexpected AssumeRole callership in CloudTrail.
Trust is the front door. Check it before you argue about the furniture inside.