- Cloud Security Lab a Week (S.L.A.W)
- Posts
- Accidentally Expose All Your Stuff on S3 with a Bucket Policy
Accidentally Expose All Your Stuff on S3 with a Bucket Policy
Last week we learned how to misconfigure S3 ACLs. This week we learn how to misconfigure bucket policies. It's important to see why we don't run with scissors.
Prerequisites
None, but I strongly recommend you complete our lab on S3 ACLs.
The Lesson
In our last lab we learned about S3 ACLs and how risky they can be. The fact that ACLs use XML and not JSON is a major clue to how old they are, but I think we still have them because the tech is baked deeply into S3, and a ton of older buckets still use them. I've released a ton of public content via S3, and only recently have I finally realized I should stop being a bad example and stop using ACLs myself.
Today we will focus on bucket policies, the resource-based policies for S3. As a reminder the three main policy types in AWS are organization-based policies (SCPs), identity-based policies (IAM), and resource-based policies (multiple, including bucket policies).
An easy way to remember how these interact is to understand their scopes (what they each apply to):
Identity-based policies apply to identities, including IAM users and roles.
Organizations-based policies apply to AWS Accounts, and are set at the Organizational Unit or individual account level.
Resource-based policies apply to resources which can potentially be accessed by things outside your account. This can be a bunch of different things including an identity in another AWS account, an IP address, or some other attribute/origin.
As a reminder, we need resource policies because there are real needs to share resources beyond one account, while still controlling access. These identities/origins don’t live in our account so an IAM policy wouldn’t work well, and an SCP would be even messier.
All these different policy types interact as I previously documented. Access is only allowed if there is an allow in the policies, and no deny statements block access. Normally you need allow policies in every policy type for access, but with resource policies if the resource policy allows access, and there are no explicit deny statements, someone might get access without all policy types being checked.
This is a diagram from AWS that shows the order in which policies evaluated — it’s called the policy evaluation logic, and this is one to clip and keep for future reference:
The first important point to notice is that every policy is evaluated for deny statements no matter what; this is called an explicit deny, and there’s no way around them.
But if you don’t have an allow statement, that’s called an implicit deny. Normally you need explicit allow statements in every policy type for an identity to do something. But with resource policies if the resource policy allows access to S3, you don’t also need an identity based policy to allow the user access to the S3 APIs. The resource policy is evaluated before identity policies, and an explicit allow overrides implicit denies.
I really hope that makes sense. The practical application is that if you give a role access to a bucket in the bucket policy, unless there’s a deny statement, they get access — even if that role doesn’t have access to S3 in its IAM policy. And while we are discussing S3 today, this is true for all resource policies.
Alrighty, let’s learn more about bucket policies themselves.
Bucket Policies
S3 bucket policies are JSON documents which define fine-grained access controls for Amazon S3 buckets and the objects within them. These policies use the AWS Identity and Access Management (IAM) policy language and can be up to 20KB. A bucket policy consists of one or more statements, each defining who can do what under which conditions. What makes bucket policies particularly powerful is their flexibility in controlling access based on a wide range of conditions, and their ability to grant permissions to different types of principals (users, roles, or AWS services). This is also how we get ourselves in trouble.
Here’s an example of a bucket policy showing different options, highlighting the complexity:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadFromSpecificIAMRole",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/MyApplicationRole"
},
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
]
},
{
"Sid": "DenyUnencryptedTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
},
{
"Sid": "AllowCrossAccountAccess",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::987654321098:root"
},
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-bucket/shared-folder/*",
"Condition": {
"StringEquals": {
"aws:PrincipalOrgID": "o-exampleorgid"
}
}
},
{
"Sid": "RestrictToSpecificIPRange",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"NotIpAddress": {
"aws:SourceIp": [
"192.0.2.0/24",
"203.0.113.0/24"
]
}
}
}
]
}
The first statement allows a specific IAM role to read objects and list bucket contents. This is a common pattern for application access.
The second statement enforces encryption in transit by denying all actions if the request isn't made via HTTPS. This is a security best practice.
The third statement enables cross-account access to a specific folder, but only if the accessing account is part of the same AWS Organization. This demonstrates more complex conditions.
The final statement restricts access to specific IP ranges, showing how network-level controls are implemented.
Okay, that all makes sense, but what does it look like to make something public in a bucket policy?
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadViaAllUsers",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::example-public-bucket/*",
"arn:aws:s3:::example-public-bucket"
]
},
{
"Sid": "PublicReadViaIPAddress",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::example-public-bucket/*",
"arn:aws:s3:::example-public-bucket"
],
"Condition": {
"IpAddress": {
"aws:SourceIp": "0.0.0.0/0"
}
}
}
]
}
This shows 2 ways to make something public:
Allow access from any principal.
Allow access from any IP address.
These are the two simplest ways to screw up, but there are plenty more. But hey, let’s start with showing you how to poke one eye out with scissors at a time — we’ll get into lasers, angle grinders, or maniacal-clowns-with-pencils-smashing-your-head-down later.
And one final reminder. I’m teaching you how to make things public so you know what to look for and what not to do. You need to be able to recognize these when you operate in an environment outside your complete control; in upcoming labs I’ll show you some very effective ways to find, fix, and block public S3 buckets without pissing everyone off.
Key Lesson Points
Resource policies are like IAM policies, but apply to individual resources.
Resource policies are evaluated before identity policies.
A resource policy can never override a deny statement in any other policies.
If there isn’t a deny statement, an identity will get access to the resource if the resource policy allows it, even if no identity policies explicitly allow it.
Bucket policies can make something public using multiple kinds of statements.
The Lab
Unsurprisingly we will create another bucket like last week and, this time make it public with a bucket policy. Notice that since we aren’t using ACLs we don’t need to mess with them at all and can keep them disabled (we don’t change bucket ownership this time).
Video Walkthrough
Step-by-Step
Start in your Signin portal > TestAccount1 > AdministratorAccess. Then go to S3 and click the hamburger (3 vertical lines)
Then go to Buckets > Create bucket:
Choose General purpose and then enter a name. S3 bucket names must be globally unique and comply with character restrictions! This means lowercase only, no spaces, dashes are okay but not slashes, and other stuff. I use first initial last name -slaw-bp-random keyboard bashes, so mine looks like rmogull-slaw-bp-32149870erhij. I added the “bp” so I know this one is for the bucket policy, and the other one uses ACLs.
Next keep the selection ACLs disabled, then Un-check Block Public Access, and Check the acknowledgment in the yellow box.
Then scroll all the way down and Create bucket.
Now we need to add the bucket policy. Click the bucket you just created (the one with “bp” in its name):
Click Permissions, then Edit in the bucket policy section:
Then copy out this policy and paste it into the box:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadViaAllUsers",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::example-public-bucket/*",
"arn:aws:s3:::example-public-bucket"
]
},
{
"Sid": "PublicReadViaIPAddress",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::example-public-bucket/*",
"arn:aws:s3:::example-public-bucket"
],
"Condition": {
"IpAddress": {
"aws:SourceIp": "0.0.0.0/0"
}
}
}
]
}
Notice a couple things:
This shows 2 techniques at the same time. One allows any anonymous access; the other does the same for general Internet access. These are somewhat redundant, but they are both patterns you will encounter.
We cover two actions: ListBucket shows you the contents of a bucket, like viewing a directory. GetObject enables you to download an object.
This is the most dangerous combination because you can see all the items in a bucket and download them. If you remove ListBucket permission, an adversary needs to figure out the full name of an object to get it. This is legitimate obfuscation which works, and I’ve used it to share files when I seed the name with crypto-generated random characters to make guessing effectively impossible.
Since GetObject refers to objects, we need “/*” after the bucket’s Amazon Resource Name (ARN) so the permission applies to everything in the bucket.
Because the bucket itself is at the root, we also need to specify the bucket ARN alone so ListBucket works (the bucket doesn’t exist in the /* path, so that doesn’t work).
Okay REPLACE THE ARN WITH YOUR OWN ARN, BUT MAKE SURE YOU LEAVE /* AND THE QUOTES!!
So where’s the ARN? Scroll up a little and copy it right there. This is, in my book, the single greatest thing the AWS UX team has ever done for me!
It should then look like this (notice I swapped in my actual ARN):
Scroll down and Save changes.
You now have 2 public buckets, with no objects in either. In our next couple labs we’ll learn how to find, fix, and prevent this.
Lab Key Points
Bucket policies can make something public through various different kinds of statements.
As an extra warning… if you ever allow PutObject without restriction, anyone can house all their personal files in your bucket, while you pay the bill.
-Rich
Reply