5 min read

Cloud security - Got Milk? IAM enumeration and S3 compromise

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 key ASIASU566JDVWW5WXLYO. The session name provided by the threat actor was test-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 - essentially whoami in AWS land.
  • GetUser
  • ListGroupsForUser
  • ListUserPolicies - lists the inline IAM policies attached to the principal
  • GetPolicy
  • 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"