Sam Cox

CTO & Co-Founder

Thu, February 22, 2024

How to find the AWS Account ID of any S3 Bucket

In 2021 Ben Bridts published a highly inventive method for finding the AWS Account ID of a public S3 bucket.

This post describes a technique to find the Account ID of any S3 bucket (both private and public).

I'd highly recommend reading Ben's technique first as we will re-use a lot of concepts.

S3 Bucket to AWS Account ID

Shell output can be worth a thousand words, here's what our technique enables - finding the previously unknown AWS Account ID for the bucket bucket-alpha:

sh-5.2$ python3 find-s3-account.py bucket-alpha

VPC endpoint vpce-0e76855aadb0dafb5 policy already configured
Requesting bucket-alpha using session name 0-----------
Requesting bucket-alpha using session name 1-----------
Requesting bucket-alpha using session name 2-----------
Requesting bucket-alpha using session name 3-----------

SNIP

Requesting bucket-alpha using session name -----------7
Requesting bucket-alpha using session name -----------8
Requesting bucket-alpha using session name -----------9
Finding session names which passed the VPC endpoint in CloudTrail...
Found -----------1 for bucket-alpha in CloudTrail
Found ---------1-- for bucket-alpha in CloudTrail
Found --------9--- for bucket-alpha in CloudTrail
Found -----6------ for bucket-alpha in CloudTrail
Found --3--------- for bucket-alpha in CloudTrail
Found -2---------- for bucket-alpha in CloudTrail
Found 1----------- for bucket-alpha in CloudTrail
Found ----------0- for bucket-alpha in CloudTrail
Found -------8---- for bucket-alpha in CloudTrail
Found ------7----- for bucket-alpha in CloudTrail
Found ----5------- for bucket-alpha in CloudTrail
Found ---4-------- for bucket-alpha in CloudTrail
Bucket bucket-alpha: 123456789101

How exactly does this work?

When exploring possibilities for this technique, I started by breaking down exactly why Ben's method works. There are three key elements which combine to make it work:

  1. The ability to apply an IAM policy to the request

In the Ben's technique, this is achieved by applying a custom policy when assuming the role.

  1. The ability to infer whether this IAM policy permitted the request or not

In the case of public buckets, this is quite simple. If our policy blocked the request, the request will fail with AccessDenied. Otherwise, the request will succeed as expected with requests to public buckets.

  1. The ability to apply a wildcard match on the s3:ResourceAccount condition key

This allows us to discover the Account ID incrementally, one digit at a time, reducing the search space from trillions to hundreds.

A solution

After exploring a few different ideas, I found a solution which works. It involves using a VPC Endpoint for S3, and a difference of behaviour in CloudTrail when a request is denied by a VPC Endpoint policy.

How VPC Endpoint policies interact with CloudTrail

  1. The ability to apply an IAM policy to the request

Creating a VPC Endpoint of type "Interface" for S3 will allow us to apply an IAM policy to the request. This policy intersects with the other policies which apply to the request (e.g. the bucket policy, the IAM policy of the principal making the request etc) when the request is made through the VPC Endpoint.

  1. The ability to infer whether this IAM policy permitted the request or not

As the target bucket is owned by a third party and is a private bucket, we're (thankfully) going to receive an AccessDenied response, regardless of whichever policies we apply to the request. However, we can infer whether the VPC Endpoint policy blocked or permitted the request by whether it appears in our own CloudTrail logs.

  • If the request does appear in our CloudTrail logs, it was permitted by our VPC Endpoint policy but blocked as expected by the bucket policy.
  • If the request does not appear in our CloudTrail logs, it was blocked by our VPC Endpoint policy.
  1. The ability to apply a wildcard match on the s3:ResourceAccount condition key

We can use the full power of IAM policy conditions, including StringLike wildcards and resource condition keys in a VPC Endpoint policy, so the same basic technique will work here.

Step-by-step

Let's say that we want to find the Account ID of the bucket bucket-alpha.

Note that some of our activities here will be visible to the owner of the bucket in their own CloudTrail logs.

Determine the bucket region

We need to find the region in which the bucket lives so that we can create a VPC in the same region. This can be done by curling the bucket's HTTP endpoint and examining the x-amz-bucket-region header (which is returned despite the request being forbidden).

curl -v bucket-alpha.s3.amazonaws.com

...
x-amz-bucket-region: us-east-1

Deploy a VPC and VPC Endpoint in the same region

We need to deploy a VPC and a VPC Endpoint for S3 in the same region as the target bucket. It's best to create a VPC specifically for this purpose as our VPC Endpoint will interfere with requests to S3 from the VPC. The VPC Endpoint should be of type "Interface" so we can apply a policy to the request.

Launch an EC2 instance within the VPC and confirm that it's using the VPC Endpoint for S3

We'll need to send requests to S3 from within the VPC so that the VPC Endpoint is used. An EC2 instance is a convenient way of doing so.

Modify the VPC Endpoint policy to determine whether the account ID of the target bucket starts with "0"

Apply a policy to the VPC Endpoint which performs a wildcard match on the s3:ResourceAccount condition key. This will only permit requests through the endpoint if the bucket's Account ID starts with "0".

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "s3:*",
            "Effect": "Allow",
            "Resource": "*",
            "Principal": "*",
            "Condition": {
                "StringLike": {
                    "s3:ResourceAccount": "0*"
                }
            }
        }
    ]
}

Make a request to the target bucket

Via the EC2 instance, make a request to the target bucket. This request will be denied as expected. It's best to use a "Management" request rather than a "Data" request so we don't need to do anything special with our CloudTrail setup. In this case I used GetBucketAcl.

aws s3api get-bucket-acl --bucket bucket-alpha

An error occurred (AccessDenied) when calling the GetBucketAcl operation: Access Denied

Check whether the request appears in CloudTrail

Now we want to check whether our request appears in CloudTrail.

aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=GetBucketAcl --start-time $(date -d "-10 minutes" +%s)

If we find our request in CloudTrail, it means that the VPC Endpoint policy permitted the request - i.e. the Account ID of the bucket starts with 0. If we don't find the request, then the VPC Endpoint policy blocked the request - i.e. the Account ID of the bucket does not start with 0.

{
    <SNIP>
    "eventSource": "s3.amazonaws.com",
    "eventName": "GetBucketAcl",
    "resources": [
        {
            "accountId": "HIDDEN_DUE_TO_SECURITY_REASONS",
            "type": "AWS::S3::Bucket",
            "ARN": "arn:aws:s3:::bucket-alpha"
        }
    ],
    "vpcEndpointId": "vpce-0e76855aadb0dafb5",
}

Bear in mind that it will take a few minutes for the request to appear in CloudTrail. To be safe, I'd recommend waiting a 10 minutes before deciding the event won't appear in CloudTrail.

Rinse and repeat

Depending on the result of the previous step, modify the VPC Endpoint policy to discover more information about the account ID. For instance, if the event didn't appear in CloudTrail, modify the condition to test whether the first digit is 1:

"Condition": {
    "StringLike": {
        "s3:ResourceAccount": "1*"
    }
}

If it did appear in CloudTrail (so the first digit of the account ID is 0), we can start work on the second digit:

"Condition": {
    "StringLike": {
        "s3:ResourceAccount": "00*"
    }
}

Bear it mind, it takes a few minutes for policy changes to fully propagate and take effect. I've found waiting 5 minutes after modifying the policy to work well.

Results

I wrote a script to automate this process and it could reliably find the Account ID of a bucket. As it's quite a slow process, I used a slightly modified technique of performing a binary search on each digit so fewer tests were needed, e.g:

"Condition": {
    "StringLike": {
        "s3:ResourceAccount": ["0*", "1*", "2*", "3*", "4*"]
    }
}

Leaving it for a few hours returned the account ID successfully:

[ssm-user@ip-172-31-8-184 ~]$ python3 find-s3-account.py bucket-alpha
Searching for bucket bucket-alpha
Modifying VPC endpoint policy...
Modified VPC endpoint policy
Made S3 request to bucket: GPSWS2M4TH9ABX3C
Looking for event in CloudTrail...
Did not find event in CloudTrail (not permitted through VPC endpoint)
State: {'found': '', 'next_digits': [0, 1, 2, 3, 4]}
Modifying VPC endpoint policy...
Modified VPC endpoint policy
Made S3 request to bucket: 8T809NPVDGQSGB1N
Looking for event in CloudTrail...
Did not find event in CloudTrail (not permitted through VPC endpoint)
State: {'found': '', 'next_digits': [0, 1]}
Modifying VPC endpoint policy...
Modified VPC endpoint policy
Made S3 request to bucket: C9F0RTC7QK0G70TB
Looking for event in CloudTrail...
Found event in CloudTrail (permitted through VPC endpoint)
State: {'found': '1', 'next_digits': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}

...

State: {'found': '123456789101', 'next_digits': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}
Found account: 123456789101

Making it faster

Waiting for the VPC Endpoint policy to take effect, and waiting for long enough to determine whether the request appears in CloudTrail is quite a slow process. Even using a binary search, it will take approximately (40 * 12 minutes = 8 hours) to find the Account ID.

To make this process faster, and eliminate the need to wait between each step, I modified the VPC endpoint policy like so:

{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": [
          "s3:*"
        ],
        "Resource": "*",
        "Principal": "*",
        "Condition": {
          "StringLike": {
            "aws:userid": "*:0-----------",
            "s3:ResourceAccount": "0???????????"
          }
        }
      },
      {
        "Effect": "Allow",
        "Action": [
          "s3:*"
        ],
        "Resource": "*",
        "Principal": "*",
        "Condition": {
          "StringLike": {
            "aws:userid": "*:1-----------",
            "s3:ResourceAccount": "1???????????"
          }
        }
      },
      SNIP
      {
        "Effect": "Allow",
        "Action": [
          "s3:*"
        ],
        "Resource": "*",
        "Principal": "*",
        "Condition": {
          "StringLike": {
            "aws:userid": "*:-----------9",
            "s3:ResourceAccount": "???????????9"
          }
        }
      }
    ]
}

There are 120 statements in the policy - one for each possible digit in each possible position. The condition on aws:userid is used to match particular values of the RoleSessionName parameter (which we can freely specify) used in an STS AssumeRole call. In effect this means we can selectively choose which policy statement (i.e. a particular digit in a particular position) we want to test for each request, by assuming a role with a particular RoleSessionName before doing so.

Using RoleSessionName to selectively test digits

As this policy (only just!) fits within the maximum character length of a VPC Endpoint policy, we can test all 120 possibilities in parallel, without modifying the policy or waiting for the results individually in CloudTrail.

This reduced the time taken to find the Account ID to less than 10 minutes:

sh-5.2$ python3 find-s3-account.py bucket-alpha

VPC endpoint vpce-0e76855aadb0dafb5 policy already configured
Requesting bucket-alpha using session name 0-----------
Requesting bucket-alpha using session name 1-----------
Requesting bucket-alpha using session name 2-----------
Requesting bucket-alpha using session name 3-----------

SNIP

Requesting bucket-alpha using session name -----------7
Requesting bucket-alpha using session name -----------8
Requesting bucket-alpha using session name -----------9
Finding session names which passed the VPC endpoint in CloudTrail...
Found -----------1 for bucket-alpha in CloudTrail
Found ---------1-- for bucket-alpha in CloudTrail
Found --------9--- for bucket-alpha in CloudTrail
Found -----6------ for bucket-alpha in CloudTrail
Found --3--------- for bucket-alpha in CloudTrail
Found -2---------- for bucket-alpha in CloudTrail
Found 1----------- for bucket-alpha in CloudTrail
Found ----------0- for bucket-alpha in CloudTrail
Found -------8---- for bucket-alpha in CloudTrail
Found ------7----- for bucket-alpha in CloudTrail
Found ----5------- for bucket-alpha in CloudTrail
Found ---4-------- for bucket-alpha in CloudTrail
Bucket bucket-alpha: 123456789101

Remarks

  • I consulted the AWS Security team before publishing this blog post.

  • There has already been a lot of interesting discussion about whether AWS account IDs should be considered sensitive. It's noteworthy that in the CloudTrail events themselves, AWS have chosen to leave the third-party account ID HIDDEN_DUE_TO_SECURITY_REASONS. You've probably also spotted that I've chosen to redact my own account ID from the examples in this post!

  • This technique should also work to uncover other resource condition keys (e.g. aws:ResourceOrgID, aws:ResourceOrgPaths, aws:ResourceTag) associated with the bucket - or indeed for services other than S3 to which this technique could be applied

  • It's possible that by creating mutually peered VPCs and VPC Endpoints in all regions you could create a setup which works regardless of the particular region of the target bucket.

  • These techniques are only feasible due to the ability to use StringLike conditions against s3:ResourceAccount in a policy. I can't think of a use case where a partial match against a randomly-generated identifier is necessary.

  • It seems like it might otherwise be beneficial for events which are denied by a VPC Endpoint policies to be logged in CloudTrail.

Acknowledgments

  • Ben Bridt's original technique inspired this work.

  • I'm very grateful to Chris Farris for his invaluable help and advice.

Thanks for reading this far, we hope you got something from this article. Tracebit is building a new kind of security product to be the ‘easy button’ for adding detections to cloud environments using decoys. If you’re interested to learn more about what that looks like please reach out (we currently support AWS, with Azure & GCP coming soon).

Sign up for our newsletter

Subscribe to stay in the loop of future posts and Tracebit updates