Category: Cloud · IAM · Privilege Escalation
Level: Advanced
IAM privilege escalation in cloud environments is rarely about a leaked admin key. More often it is about a trust relationship that was designed with good intentions and one operator character that made it wider than intended.
This note is about one of the most common patterns: the StringLike condition with a wildcard in a role trust policy.
When a role is created in AWS, it has two types of policies attached:
The trust policy controls the sts:AssumeRole action. A typical trust policy might say: "only the CI/CD pipeline role can assume me."
{
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::111111111111:role/deploy-prod"},
"Action": "sts:AssumeRole"
}]
}
This is specific and safe. The problem starts when conditions are used instead of an exact principal match.
{
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "*"},
"Action": "sts:AssumeRole",
"Condition": {
"StringLike": {
"aws:PrincipalArn": "arn:aws:iam::111111111111:assumed-role/deploy-*/*"
}
}
}]
}
The intent: only roles whose name starts with deploy- can assume this role.
The problem: StringLike with * is a pattern match, not an exact match. Any role in the account whose name starts with deploy- satisfies this condition — including deploy-readonly, deploy-staging, deploy-old-2022, or any future role created with that prefix for any reason.
If a low-privilege identity can assume any role matching deploy-*, it can then use that role to assume the target role.
Starting identity: app-reader — read-only permissions, no obvious path forward.
Step 1: enumerate trust policies using iam:GetRole:
aws iam get-role --role-name automation-deploy \
--query 'Role.AssumeRolePolicyDocument'
Step 2: identify a role matching the pattern that app-reader can assume:
aws iam list-roles --query 'Roles[?starts_with(RoleName, `deploy-`)].RoleName'
# deploy-readonly
Step 3: assume deploy-readonly (permitted by its own trust policy):
aws sts assume-role \
--role-arn arn:aws:iam::111111111111:role/deploy-readonly \
--role-session-name pivot
Caller ARN is now: arn:aws:iam::111111111111:assumed-role/deploy-readonly/pivot
This matches deploy-/. The condition on automation-deploy is satisfied.
Step 4: assume automation-deploy:
aws sts assume-role \
--role-arn arn:aws:iam::111111111111:role/automation-deploy \
--role-session-name escalated
Full automation permissions acquired. Two API calls from a read-only starting point.
Use StringEquals with a specific ARN:
"Condition": {
"StringEquals": {
"aws:PrincipalArn": "arn:aws:iam::111111111111:assumed-role/deploy-prod/session"
}
}
Or, better — use a named principal directly without conditions:
"Principal": {
"AWS": "arn:aws:iam::111111111111:role/deploy-prod"
}
Limit iam:GetRole and iam:ListRoles:
These permissions allow full enumeration of trust relationships. A low-privilege identity should not be able to read the trust policy of every role in the account.
Use IAM Access Analyzer:
AWS IAM Access Analyzer continuously evaluates trust policies and flags external access and overly permissive conditions. Enable it in every account and review its findings regularly.
Audit with a graph mindset:
Trust policies cannot be reviewed in isolation. The question is not "who can assume this role?" but "starting from any identity in this account, can someone reach this role through a chain of assumptions?" Tools like PMapper (Principal Mapper) build this graph automatically.
The difference between a safe trust policy and the vulnerable one above is StringEquals vs StringLike. One character of difference in the operator name. The wildcard in the ARN pattern becomes inert with StringEquals — it matches literally, not as a glob.
This is the class of misconfiguration that does not show up in vulnerability scanners, does not have a CVE, and is invisible to anyone reviewing the policy without understanding how StringLike works in this context. It only becomes visible when you think about the policy as part of a permission graph — not as a standalone document.
Categoria: Cloud · IAM · Escalada de Privilégios
Nível: Avançado
Escalada de privilégios IAM em ambientes cloud raramente envolve uma chave de admin vazada. Na maioria das vezes, trata-se de uma relação de confiança criada com boas intenções e um único caractere que a tornou mais ampla do que o pretendido.
Esta nota é sobre um dos padrões mais comuns: a condição StringLike com wildcard em uma trust policy de role.
Quando uma role é criada na AWS, ela tem dois tipos de policies associadas:
A trust policy controla a ação sts:AssumeRole. Uma trust policy típica pode dizer: "apenas a role do pipeline de CI/CD pode me assumir."
{
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::111111111111:role/deploy-prod"},
"Action": "sts:AssumeRole"
}]
}
Isso é específico e seguro. O problema começa quando condições são usadas em vez de um principal exato.
{
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "*"},
"Action": "sts:AssumeRole",
"Condition": {
"StringLike": {
"aws:PrincipalArn": "arn:aws:iam::111111111111:assumed-role/deploy-*/*"
}
}
}]
}
A intenção: apenas roles cujo nome começa com deploy- podem assumir esta role.
O problema: StringLike com * é correspondência de padrão, não correspondência exata. Qualquer role na conta cujo nome começa com deploy- satisfaz essa condição — incluindo deploy-readonly, deploy-staging, deploy-old-2022, ou qualquer role futura criada com esse prefixo.
Identidade inicial: app-reader — permissões somente leitura, sem caminho óbvio para escalada.
Passo 1: enumerar trust policies usando iam:GetRole:
aws iam get-role --role-name automation-deploy \
--query 'Role.AssumeRolePolicyDocument'
Passo 2: identificar uma role que satisfaz o padrão que app-reader pode assumir:
aws iam list-roles --query 'Roles[?starts_with(RoleName, `deploy-`)].RoleName'
# deploy-readonly
Passo 3: assumir deploy-readonly:
aws sts assume-role \
--role-arn arn:aws:iam::111111111111:role/deploy-readonly \
--role-session-name pivot
ARN do caller agora: arn:aws:iam::111111111111:assumed-role/deploy-readonly/pivot
Isso satisfaz o padrão deploy-/. A condição em automation-deploy está satisfeita.
Passo 4: assumir automation-deploy:
aws sts assume-role \
--role-arn arn:aws:iam::111111111111:role/automation-deploy \
--role-session-name escalated
Permissões completas de automação adquiridas. Duas chamadas de API a partir de um ponto de partida somente leitura.
Use StringEquals com um ARN específico:
"Condition": {
"StringEquals": {
"aws:PrincipalArn": "arn:aws:iam::111111111111:assumed-role/deploy-prod/session"
}
}
Ou, melhor — use um principal nomeado diretamente sem condições:
"Principal": {
"AWS": "arn:aws:iam::111111111111:role/deploy-prod"
}
Limite iam:GetRole e iam:ListRoles:
Essas permissões permitem enumeração completa de relações de confiança. Uma identidade com poucos privilégios não deveria ler a trust policy de todas as roles na conta.
Use o IAM Access Analyzer:
O AWS IAM Access Analyzer avalia continuamente as trust policies e sinaliza acesso externo e condições excessivamente permissivas. Ative-o em cada conta e revise seus findings regularmente.
Faça auditoria com uma mentalidade de grafo:
Trust policies não podem ser revisadas isoladamente. A pergunta não é "quem pode assumir essa role?" mas "partindo de qualquer identidade nessa conta, alguém consegue chegar a essa role por uma cadeia de assumptions?" Ferramentas como PMapper constroem esse grafo automaticamente.
A diferença entre uma trust policy segura e a vulnerável acima é StringEquals vs StringLike. Uma diferença no nome do operador. O wildcard no padrão de ARN torna-se inerte com StringEquals — ele corresponde literalmente, não como glob.
Essa é a classe de misconfiguration que não aparece em scanners de vulnerabilidades, não tem CVE, e é invisível para quem revisa a policy sem entender como StringLike funciona nesse contexto. Ela só se torna visível quando você pensa na policy como parte de um grafo de permissões — não como um documento isolado.
Relacionado: Case 05 — Cloud IAM Trust Policy and Role Chaining