AttachUserPolicy iam.amazonaws.com IAM

AttachUserPolicy in CloudTrail: Who Granted Which Policy to Whom

Published

A managed IAM policy attached to an IAM user; the record names who attached it, the user who received it and the policy's ARN, which together decide whether this was an escalation.

AWS's description

Attaches the specified managed policy to the specified user.

Source: AWS API reference: AttachUserPolicy · copied from AWS's documentation and refreshed weekly

Request parameters

The parameters AWS's API reference documents for AttachUserPolicy. A key is shown only where AWS's example records show it, or where every IAM example names keys after the parameter with a lower-case first letter.

API parameterRequiredIn the CloudTrail record
PolicyArn Yes requestParameters.policyArn
UserName Yes requestParameters.userName

Fields that matter in an investigation

FieldWhat it tells you
1requestParameters.policyArn The policy attached. arn:aws:iam::aws:policy/... is an AWS managed policy such as AdministratorAccess; an ARN with the account ID is one the account created, whose document you need to read.
2requestParameters.userName The user who received the policy. A user created minutes earlier, or the caller's own user, is where escalation shows.
3userIdentity.arn Who attached it. Compare with the identities that are supposed to administer IAM.
4userIdentity.userName The calling user's name, for comparison with requestParameters.userName; when they match, the user granted the policy to themselves.
5userIdentity.type IAMUser, AssumedRole or Root. A workload role or an unfamiliar federated session attaching policies to users stands out.
6userIdentity.accessKeyId The key used, to follow everything else it did around the grant.
7sourceIPAddress Where the call came from. Compare with the caller's usual addresses.
8userAgent The client, a console session, CLI, SDK or a tool that names itself.
errorCode Empty on success. AccessDenied means the caller tried to grant a policy and was refused, which still says what they were after.

Every field is checked on each build against AWS's CloudTrail record contents and userIdentity documentation and the example and recorded AttachUserPolicy records shown below.

What the record looks like

AWS does not publish an example AttachUserPolicy record; the record below was captured during an attack simulation.

Recorded during an attack simulation: Stratus Red Team technique aws.persistence.iam-create-admin-user

{
"awsRegion": "ap-isob-central-3r",
"eventCategory": "Management",
"eventID": "083dc4ad-e264-46bc-a407-d0dd31b58bdc",
"eventName": "AttachUserPolicy",
"eventSource": "iam.amazonaws.com",
"eventTime": "2024-08-01T13:33:28Z",
"eventType": "AwsApiCall",
"eventVersion": "1.09",
"managementEvent": true,
"readOnly": false,
"recipientAccountId": "123456789012",
"requestID": "710f2703-6e8a-46d5-9924-b12a3a681755",
"requestParameters": {
"policyArn": "arn:aws:iam::aws:policy/AdministratorAccess",
"userName": "malicious-iam-user"
},
"responseElements": null,
"sourceIPAddress": "203.0.113.10",
"tlsDetails": {
"cipherSuite": "TLS_AES_128_GCM_SHA256",
"clientProvidedHostHeader": "iam.amazonaws.com",
"tlsVersion": "TLSv1.3"
},
"userAgent": "stratus-red-team_6bf00313-712c-4fd2-9bdd-88f48a4b1282",
"userIdentity": {
"accessKeyId": "AKIAIOSFODNN7EXAMPLE",
"accountId": "123456789012",
"arn": "arn:aws:iam::123456789012:user/operator",
"principalId": "AIDAZ49AHUAJ9OEK73O5",
"type": "IAMUser",
"userName": "operator"
}
}

Numbered lines are the fields in the table.

A real CloudTrail record from a test detonation, published by Stratus Red Team (Datadog, Apache License 2.0) and anonymized there; account IDs, addresses, access key IDs and session tokens are replaced here with AWS's documentation values, and the operator's user name with "operator". The anonymizer also changes values such as the Region.

What it records

AttachUserPolicy attaches a managed policy to an IAM user. AWS’s documentation separates it from PutUserPolicy, which embeds an inline policy instead. The record from iam.amazonaws.com carries the two documented parameters, requestParameters.policyArn and requestParameters.userName, and the caller in userIdentity; responseElements is null in the recorded event, so the grant is described entirely by the request.

The policy ARN carries most of the meaning. AWS managed policies have aws where an account ID would be, arn:aws:iam::aws:policy/AdministratorAccess being the one attackers want, while a customer managed policy has the account’s ID and has to be read to know what it allows. In the recorded attack-simulation event, a user attaches AdministratorAccess to a user the same run had created.

When it matters

Attaching a policy is how a foothold becomes control of the account (MITRE ATT&CK T1098.003, additional cloud roles). Three shapes deserve attention:

  • AdministratorAccess or IAMFullAccess attached to anyone outside the IAM team. IAMFullAccess is administrator-equivalent because it allows granting any other permission.
  • A self-grant. requestParameters.userName equals the caller’s own user name.
  • A fresh user. CreateUser, then AttachUserPolicy with a broad policy, then CreateAccessKey or CreateLoginProfile for the same user within minutes is a backdoor administrator being built.

The free log analyzer flags AdministratorAccess and IAMFullAccess grants as critical, and so does a customer managed policy created in the same log whose document allows every action on every resource. The validated Sigma rule LTR-0002 alerts on this and the related IAM calls from principals outside an allowlist of administrators.

How to investigate

  1. Read who attached which policy to whom: userIdentity.arn, requestParameters.policyArn, requestParameters.userName.
  2. For a customer managed policy, find its document in the CreatePolicy or CreatePolicyVersion call that defined it, or in IAM.
  3. Check the receiving user’s history: when it was created and by whom, and whether keys or a console password were added afterwards.
  4. Follow the caller’s key across the hour around the grant, then detach the policy if it is not explained.

Policy grants to users in Microsoft Sentinel, with AWS managed policies and self-grants marked:

AWSCloudTrail
| where TimeGenerated > ago(7d)
| where EventName == "AttachUserPolicy" and isempty(ErrorCode)
| extend Request = parse_json(RequestParameters)
| extend PolicyArn = tostring(Request.policyArn), TargetUser = tostring(Request.userName)
| extend PolicyName = extract(@"([^/]+)$", 1, PolicyArn),
    AwsManaged = PolicyArn startswith "arn:aws:iam::aws:policy/",
    SelfGrant = TargetUser == UserIdentityUserName
| project TimeGenerated, UserIdentityArn, TargetUser, PolicyName, AwsManaged, SelfGrant, SourceIpAddress, UserAgent
| order by TimeGenerated desc

Broad AWS managed policies attached to users, roles or groups:

AWSCloudTrail
| where TimeGenerated > ago(30d)
| where EventName in ("AttachUserPolicy", "AttachRolePolicy", "AttachGroupPolicy") and isempty(ErrorCode)
| extend PolicyArn = tostring(parse_json(RequestParameters).policyArn)
| where PolicyArn in ("arn:aws:iam::aws:policy/AdministratorAccess", "arn:aws:iam::aws:policy/IAMFullAccess")
| project TimeGenerated, EventName, UserIdentityArn, PolicyArn, RequestParameters, SourceIpAddress

In Athena, over a table on the trail’s S3 bucket (named cloudtrail_logs here):

SELECT eventtime, useridentity.arn AS granted_by,
       json_extract_scalar(requestparameters, '$.userName') AS target_user,
       json_extract_scalar(requestparameters, '$.policyArn') AS policy_arn, sourceipaddress
FROM cloudtrail_logs
WHERE eventname = 'AttachUserPolicy' AND errorcode IS NULL
ORDER BY eventtime DESC
LIMIT 200;

Related events

  • CreateUser IAM
  • PutUserPolicy IAM
  • AttachRolePolicy IAM
  • AttachGroupPolicy IAM
  • AddUserToGroup IAM
  • CreatePolicyVersion IAM

Detect it

MITRE ATT&CK: T1098.003

Frequently Asked Questions

How do I tell an AWS managed policy from one the account created?
An AWS managed policy's ARN has aws in the account position, as in arn:aws:iam::aws:policy/AdministratorAccess. A customer managed policy carries the account's own ID, and its name says nothing reliable about its permissions, so read the policy document from the CreatePolicy or CreatePolicyVersion call that defined it.
What is the difference between AttachUserPolicy and PutUserPolicy?
AWS's documentation puts it plainly, AttachUserPolicy attaches a managed policy to a user and PutUserPolicy embeds an inline policy in a user. For an inline policy the document itself is part of the PutUserPolicy request, so the record shows exactly what was granted; for a managed policy you need to know what the attached policy allows.
Is a user attaching a policy to their own user always an escalation?
It is an escalation whenever the policy grants more than the user had, and it means the user holds iam:AttachUserPolicy on themselves, which is effectively administrator access already. Self-grants of AdministratorAccess or IAMFullAccess from a user who does not administer IAM should be treated as an intrusion until explained.
What do I do after finding an unexpected AttachUserPolicy?
Detach the policy with DetachUserPolicy, list what the receiving user did after the grant, check whether the same caller also created access keys or a console password for that user, and find how the caller came to hold permission to attach policies in the first place.

Find every AttachUserPolicy in your own CloudTrail logs

Load CloudTrail logs (the Records files CloudTrail writes to S3, or aws cloudtrail lookup-events output) into the free browser analyzer: logging changes, new access keys, administrator grants, root activity and failed console sign-ins are flagged, with event names linked to their pages here. Nothing is uploaded.