A few months ago, we created a new Open Source tool to determine the AWS Account of any S3 bucket you can access. It works for public objects and specific permissions. Now let’s dive a bit deeper and look at how this works, and some of the potential consequences.
How we do this
We can determine an AWS account by taking advantage of the new S3:ResourceAccount Policy Condition Key. This condition restricts access based on the S3 bucket an account is in (other account-based policies restrict based on the account the requesting principal is in).
If we start with access to an object and write a condition with an “Allow” on exactly one account ID, we can determine if this bucket is in this account. We will be able to access the bucket if we get the account ID right, but we will see an access denied if we try the wrong number.
Reducing the search space
Finding exactly the right account might seem impractical, with literally one trillion possibilities to try, but we don’t need to test them all.
Conditions in policies use string matching and support wildcards. We can leverage that to exclude a whole range at a time. For example, to find the first digit, we can test “1*”, “2*”, “3*” etc. Once we gain access, we know our first digit and can do the same for the second, reducing the number of requests (in the worst case) from a trillion (10^12) to one hundred and twenty (10*12).
Making the requests
Now that we know which policies to test, we need a way to try them.
If the object is public, we have many options since any principal can test these conditions.
If we have to use a specific principal to access the object, we could change that user or role’s policy. Editing policies isn’t ideal as these are eventually consistent, and we would have to wait after each change.
If the principal is a role, we can take advantage of an assume-role call’s extra parameters to get strongly consistent changes:
The default way to create temporary credentials from a role is a command like
aws sts assume-role --role-arn arn:aws:iam::123456789012:role/name --role-session-name ben. That will give us an access key, secret access key and session token with that role’s full permissions. If we add the
--policy parameter to that command, we also get temporary credentials, but the permissions will be the intersection of the role’s permission and the supplied policy. This gives us a way to have an additional condition without editing anything in our account. Doing that between 10 and 120 times will eventually reveal the account ID, and that is what the tool does.
Is this a problem?
It’s never a good idea to make discoverable any information that doesn’t need to be. That’s why we replaced all the account IDs in this blogpost with 123456789012.
However, we don’t consider the account ID to be secret information either. There can be many reasons to share an account ID with an external party, and there are more ways to find it out anyway. Your protections should assume they’re public anyway.
Can I see when someone does this?
The STS actions all happen in the account of the person trying to find the account ID, which will not be visible to the bucket owner’s account.
S3 data events (the kind of API calls used to test the policy) are not logged in CloudTrail by default. This can be enabled, but it comes with an extra cost, making it probably unrealistic to do so for buckets that get many requests. Alternatively, you can enable server access logging on each bucket, which is much more economical.
When enabled, you should be able to see multiple denied requests in a short time. However: it’s trivial to do this over a longer period, and you will only see that the request was denied (not that there was an abnormal condition).
And if you still see it, there is still a chance that it was someone from your organization who didn’t remember which account a specific bucket was in, which has happened before! With luck, this can help people with that problem!
If you’d like to know more, get in touch with Cloudar today!