Cloud security - Got Milk? IAM enumeration and S3 compromise

(__)
(oo)
/------\/ "Moo-ving to S3!"
/ | ||
* /\---/\
~~ ~~
References:
The IAM policies and commands used in this scenario were based on the following CTF: https://pwnedlabs.io/labs/intro-to-aws-iam-enumeration
Fictitious Scenario
Delaney's Dairy reached out requesting assistance with a recent cloud compromise. A file that contained their pricing to individual distributors was leaked online which caused them a bit of blow back.
They were investigating their on-premise environment to see whether the file had been leaked from there, but they also wanted their cloud environment reviewed as they stored a copy of the file in S3 as a backup.
Getting our logs
Previous to the incident occurring, Delaney's Dairy had enabled CloudTrail logging in their environment and ensured that CloudTrail Data Events were enabled for S3.

In order to download the CloudTrail logs, which are stored in an S3 bucket, we ran the S3 copy command from the AWS CLI after requesting API keys from the customer:
aws s3 cp --recursive s3://aws-cloudtrail-logs<redacted>/ .
The logs that we pull down are GZIP'd, so we needed to gunzip them:
gunzip *.gz
We then wanted to ensure that our logs are beautified (🦋) JSON:
for file in *.json; do jq . "$file" > "$file.tmp" && mv "$file.tmp" "$file"; done
Now we are milk-in'.
Initial Analysis
The customer had informed us that the file in question stored within S3 had the following ARN arn:aws:s3:::company-storage-keys/stock-markets.txt
so reviewing our logs for references to that seemed like a logical place to start. Running a simple grep over our log files led us to the following hit:
{
"eventVersion": "1.10",
"userIdentity": {
"type": "AssumedRole",
"principalId": "AROASU566JDV52SQSOBU4:test-session",
"arn": "arn:aws:sts::redacted:assumed-role/s3-restricted-viewer-role/test-session",
"accountId": "redacted",
"accessKeyId": "ASIASU566JDVWW5WXLYO",
"sessionContext": {
"sessionIssuer": {
"type": "Role",
"principalId": "AROASU566JDV52SQSOBU4",
"arn": "arn:aws:iam::redacted:role/s3-restricted-viewer-role",
--
"mfaAuthenticated": "false"
}
}
},
"eventTime": "2024-11-01T11:05:22Z",
"eventSource": "s3.amazonaws.com",
"eventName": "GetObject",
"awsRegion": "us-east-1",
"sourceIPAddress": "45.236.130.36",
"userAgent": "[aws-cli/2.18.13 md/awscrt#0.22.0 ua/2.0 os/linux#6.8.0-47-generic md/arch#x86_64 lang/python#3.12.6 md/pyimpl#CPython cfg/retry-mode#standard md/installer#exe md/distrib#ubuntu.24 md/prompt#off md/command#s3.cp]",
"requestParameters": {
"bucketName": "company-storage-keys",
"Host": "company-storage-keys.s3.amazonaws.com",
"key": "stock-markets.txt"
},
"responseElements": null,
"additionalEventData": {
"SignatureVersion": "SigV4",
"CipherSuite": "TLS_AES_128_GCM_SHA256",
"bytesTransferredIn": 0,
"AuthenticationMethod": "AuthHeader",
"x-amz-id-2": "0b6RzZuoa0Dsredacted",
"bytesTransferredOut": 295997
},
"requestID": "6HH4SDZPKM5S9D6P",
"eventID": "a75b52b4-6eeb-44fb-a0bc-8161300fe186",
"readOnly": true,
"resources": [
{
"type": "AWS::S3::Object",
"ARN": "arn:aws:s3:::company-storage-keys/stock-markets.txt"
},
{
"accountId": "redacted",
"type": "AWS::S3::Bucket",
"ARN": "arn:aws:s3:::company-storage-keys"
}
],
"eventType": "AwsApiCall",
"managementEvent": false,
"recipientAccountId": "redacted",
"eventCategory": "Data",
"tlsDetails": {
"tlsVersion": "TLSv1.3",
"cipherSuite": "TLS_AES_128_GCM_SHA256",
"clientProvidedHostHeader": "company-storage-keys.s3.amazonaws.com"
}
}
The entry gives us the following:
- The event in question occurred at
2024-11-01T11:05:22Z
, which fits our timeline. - Our threat actor assumed the role
arn:aws:iam::redacted:role/s3-restricted-viewer-role
. When assuming the role they were provided with the temporary access keyASIASU566JDVWW5WXLYO
. The session name provided by the threat actor wastest-session.
- They ran
GetObject
(i.e. requested an object from an S3 bucket) on the bucket/object combination in question:company-storage-keys/stock-markets.txt
- The threat actor performed this request from the following IP address:
45.236.130.36
with the following user-agent, suggesting they were using the AWS CLI[aws-cli/2.18.13 md/awscrt#0.22.0 ua/2.0 os/linux#6.8.0-47-generic md/arch#x86_64 lang/python#3.12.6 md/pyimpl#CPython cfg/retry-mode#standard md/installer#exe md/distrib#ubuntu.24 md/prompt#off md/command#s3.cp]
Given the threat actor has assumed this role, we next need to figure out who or what did the assuming. As a side note - in a real scenario we would also want to explore what else the threat actor did under the same session in full.
We can pivot on the session name, test-session
, and look for our first reference of this in our logs:
{
"eventVersion": "1.08",
"userIdentity": {
"type": "IAMUser",
"principalId": "AIDASU566JDVSPPYBUPW7",
"arn": "arn:aws:iam::redacted:user/brucelee",
"accountId": "redacted",
"accessKeyId": "AKIASU566JDV7D5IKPVS",
"userName": "brucelee"
},
"eventTime": "2024-11-01T11:02:10Z",
"eventSource": "sts.amazonaws.com",
"eventName": "AssumeRole",
"awsRegion": "us-east-1",
"sourceIPAddress": "45.236.130.36",
"userAgent": "aws-cli/2.18.13 md/awscrt#0.22.0 ua/2.0 os/linux#6.8.0-47-generic md/arch#x86_64 lang/python#3.12.6 md/pyimpl#CPython cfg/retry-mode#standard md/installer#exe md/distrib#ubuntu.24 md/prompt#off md/command#sts.assume-role",
"requestParameters": {
"roleArn": "arn:aws:iam::redacted:role/s3-restricted-viewer-role",
"roleSessionName": "test-session"
}
...
"eventType": "AwsApiCall"
Here we can see that the user brucelee
/ AKIASU566JDV7D5IKPVS
access key made an API call for AssumeRole
for the role arn:aws:iam::redacted:role/s3-restricted-viewer-role
with a session name of test-session
.
At this time we passed on the IOCs to the customer, performed another pass of the logs using the IOCs and suggested to our customer to reset the credentials for the user account and revoke the access key identified.
IAM enumeration
Digging in to the user's activity further we can see the following API calls, where the intent of their calls was to enumerate their compromised user, the roles and the policies attached to the user.
GetCallerIdentity
- essentiallywhoami
in AWS land.GetUser
ListGroupsForUser
ListUserPolicies
- lists the inline IAM policies attached to the principalGetPolicy
GetPolicyVersion
GetRole
ListAttachedRolePolicies
After reviewing the logs in full, we came to conclusions that:
- It is likely the user's access key was compromised (later confirmed as being published in GitHub by accident by the customer),
- Upon gaining access to the key, the threat actor ran
GetCallerIdentity
in order to check the validity of the key as well as a basic "whoami" of the compromised user, - They then ran some IAM enumeration commands to figure out the roles, groups and policies associated with the user to scope what else they had access to,
- They then assumed the role
s3-restricted-viewer-role
which provided them with a temporary access key, which was then leveraged to access the sensitive file within S3.

IAM enumeration full command list
aws configure
aws sts get-caller-identity
aws sts get-user
aws iam list-groups-for-user --user-name brucelee
aws iam list-user-attached-policies --user-name brucelee
aws iam list-user-policies --user-name brucelee
aws iam list-policy-versions --policy-arn arn:aws:iam::<ACCOUNT_ID>:policy/test-policy
aws iam get-policy-version --policy-arn arn:aws:iam::<ACCOUNT_ID>:policy/test-policy --version-id v3
aws iam list-attached-role-policies --role-name s3-restricted-viewer-role
aws iam get-role --role-name s3-restricted-viewer-role
aws iam get-policy --policy-arn arn:aws:iam::<ACCOUNT_ID>:policy/s3-restricted-viewer
aws iam get-policy-version --policy-arn arn:aws:iam::<ACCOUNT_ID>:policy/s3-restricted-viewer --version-id v2
aws sts assume-role --role-arn arn:aws:iam::<ACCOUNT_ID>:role/s3-restricted-viewer-role --role-session-name "test-session"