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:
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
.
{
"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.
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 againsts3: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.