AWS S3 Bucket Misconfiguration: Enumeration, ACL & Policy Flaws, and Takeover
Amazon S3 remains one of the most reliable sources of cloud data exposure in modern engagements. The service itself is secure by default, but the moment a team flips an object to public, attaches a sloppy bucket policy, or hardcodes a predictable bucket name into a frontend, the blast radius can include source code, customer PII, database backups, and long-lived IAM credentials. For a pentester, S3 testing is rarely about exploiting a flaw in AWS — it is about finding the human misconfiguration layered on top of it.
This guide walks through the full S3 attack surface from an authorized-testing perspective: discovering buckets tied to your scope, distinguishing the four distinct permission models (ACLs, bucket policies, IAM, and Block Public Access), reading and writing objects you should not be able to touch, escalating from a writable bucket to code execution or account compromise, and finally locking the whole thing down. Everything below assumes you have explicit written authorization for the target AWS account and its buckets.
Understanding the S3 Permission Model
Before enumerating anything, you need a mental model of why a bucket is exposed, because the remediation and the impact differ by mechanism. Four independent layers govern access to an object:
- Bucket and object ACLs — legacy, per-object or per-bucket grants. The dangerous ones are
AllUsers(anyone on the internet) andAuthenticatedUsers(any AWS account holder, not just yours — a frequently misunderstood foot-gun). - Bucket policies — JSON resource policies. A
"Principal": "*"with"Effect": "Allow"and noConditionis effectively anonymous access. - IAM policies — identity-based grants on the caller side. Relevant when you have leaked credentials and want to know what they can reach.
- S3 Block Public Access (BPA) — an account- and bucket-level override that can neuter public ACLs and policies regardless of the layers above. When BPA is fully on, public grants are ignored.
Access is the union of allows minus any explicit deny, gated by BPA. A bucket can be "public" via an ACL while its policy is silent, or vice versa, so always test both anonymous and authenticated-AWS access paths.
Bucket Enumeration and Discovery
You cannot test a bucket you cannot name. Bucket names are global and DNS-addressable, which makes them enumerable. Start by harvesting names tied to your scope from page source, JS bundles, mobile app archives, S3 redirect URLs, and CNAME records pointing at s3.amazonaws.com or *.s3-website-*.amazonaws.com.
Permutation fuzzing turns a company name into candidate buckets. Common patterns: company, company-prod, company-dev, company-backups, company-assets, company-staging-uploads. Tools like s3scanner and cloud_enum automate this:
# Probe a wordlist of candidate bucket names (authorized scope only)
s3scanner scan --buckets-file candidates.txt
# Multi-permutation discovery across AWS/GCP/Azure
cloud_enum -k company -k company-corp --quickscan
You can also confirm existence manually. A bucket that exists but denies you returns 403 AccessDenied; a non-existent one returns 404 NoSuchBucket. That distinction is the entire enumeration oracle:
# Existence + readability check via virtual-hosted-style URL
curl -sI https://company-backups.s3.amazonaws.com/
# Or via the SDK without credentials
aws s3 ls s3://company-backups --no-sign-request
Search-engine sourcing scales discovery further. Crafting targeted queries for indexed S3 listings and object URLs surfaces buckets that crawlers already found — our Google dork generator can build site:s3.amazonaws.com and intitle:"index of" style queries scoped to your target domain.
ACL and Bucket Policy Flaws
Once you have a live bucket, fingerprint its permission surface. The classic checks map directly to S3 API actions: s3:ListBucket (read the index), s3:GetObject (read files), s3:PutObject (write files), s3:GetBucketAcl / s3:GetBucketPolicy (read config), and s3:PutBucketAcl / s3:PutBucketPolicy (rewrite config — the worst-case grant).
# Anonymous listing — if this returns keys, ListBucket is public
aws s3 ls s3://company-assets --no-sign-request
# Read a specific object anonymously
aws s3 cp s3://company-assets/internal/config.json . --no-sign-request
# Dump the ACL to see who is granted what
aws s3api get-bucket-acl --bucket company-assets --no-sign-request
# Dump the bucket policy
aws s3api get-bucket-policy --bucket company-assets --no-sign-request
Watch for an ACL grant to the URI http://acs.amazonaws.com/groups/global/AllUsers (fully public) or .../AuthenticatedUsers (any AWS account). The latter is insidious: developers test with their own credentials, see it "works only when logged in," and assume it is private — when in fact every one of the millions of AWS accounts can read it.
A high-severity finding is a writable ACL or policy. If anonymous or authenticated callers hold s3:PutBucketAcl, you can grant yourself full control and pivot to total bucket ownership. Always validate write with a benign, clearly-marked test object and remove it afterward:
# Confirm anonymous write (PutObject) — use a harmless canary file
echo "authorized pentest canary - remove me" > canary.txt
aws s3 cp canary.txt s3://company-uploads/pentest-canary.txt --no-sign-request
# Clean up
aws s3 rm s3://company-uploads/pentest-canary.txt --no-sign-request
Public Objects and Data Exposure
The most common real-world impact is straightforward data exposure: objects that are individually public even when the bucket index is locked down. Because ListBucket may be denied while GetObject is allowed, you often cannot enumerate the index but can still read files if you know the keys. This is where leaked key names matter.
Sources of object keys include: the HTML/JS of the application (asset URLs reveal the key naming scheme), CloudFront distributions fronting the bucket, error pages that echo paths, and DNS records exposing website endpoints. Once you understand the naming convention (e.g. users/{id}/avatar.png or exports/2026/backup.sql), you can predict sensitive keys.
If versioning is enabled and an object was "deleted," prior versions frequently survive. A bucket where you can list versions may still serve a secret that the team believes is gone:
# Enumerate object versions — deleted secrets often persist
aws s3api list-object-versions --bucket company-data --no-sign-request
# Retrieve a specific historical version
aws s3api get-object --bucket company-data \
--key config/prod.env --version-id <VERSION_ID> \
prod.env --no-sign-request
When triaging exposed data, prioritize: .env files, *.sql / *.dump backups, id_rsa and other keys, .git directories, terraform state (*.tfstate often contains plaintext secrets), and any JSON containing aws_access_key_id. Cloud credential exposure escalates an S3 finding into full-account compromise — pair it with the techniques in our Cloud Attacks cheat sheet.
From Writable Bucket to RCE and Account Takeover
A writable bucket is rarely "just" defacement. The escalation paths depend on what consumes the bucket:
- Static site / SPA hosting: if a bucket serves a website behind CloudFront, overwriting
index.htmlor a JavaScript bundle injects stored XSS that runs in every visitor's session under the trusted origin — effectively client-side code execution against all users. - CI/CD and deployment artifacts: buckets holding Lambda zip packages, Helm charts, or deployment scripts let you tamper with code that the pipeline later executes server-side. Overwriting a Lambda artifact that the account redeploys is a direct path to code execution in the victim account.
- Config and content buckets: applications that load configuration, feature flags, or templates from S3 will ingest your modified file. SSRF-style internal pivots and template injection often follow.
- Bucket takeover via dangling references: if a CNAME or application reference points to a bucket name that no longer exists, you can register that name (bucket names are first-come, globally unique) and serve attacker content from a trusted subdomain. This is the S3 variant of subdomain takeover — the
NoSuchBucketresponse is your fingerprint.
For every escalation, demonstrate impact with the least intrusive proof possible: a benign banner injection, a non-destructive canary in the artifact path, or a screenshot of read access to a sensitive key. Do not modify production code paths or exfiltrate real customer data beyond what is necessary to prove the finding.
Leaked Credentials and IAM Pivoting
When an exposed object yields AWS keys, validate and scope them before doing anything else. Identify the principal, then enumerate reachable resources without making changes:
# Who am I? Confirms the keys are live and shows the ARN
aws sts get-caller-identity
# Enumerate buckets this identity can see
aws s3 ls
# Check IAM permissions for the current principal (if allowed)
aws iam get-user
aws iam list-attached-user-policies --user-name <name>
Over-privileged keys (a common pattern is s3:* or even AdministratorAccess baked into a deployment user) turn a single exposed file into account-wide access. Document the exact permissions and stop at proof — privilege escalation chains across IAM should be exercised only within the agreed scope.
Remediation and Secure Configuration
S3 misconfiguration is almost entirely preventable with a defense-in-depth baseline. Recommend the following to remediation teams:
- Enable S3 Block Public Access at the account level. This is the single highest-value control — it overrides public ACLs and policies even if someone reintroduces them. Set all four BPA flags to true unless a specific bucket has a documented public-hosting need.
- Disable ACLs entirely. Set
Object OwnershiptoBucketOwnerEnforcedso the legacy ACL layer is removed and access is governed solely by IAM and bucket policies — a smaller, auditable surface. - Write least-privilege bucket policies. Never use
"Principal": "*"without a tightCondition(e.g.aws:SourceVpce,aws:SourceArn, or a specific CloudFront OAC). Front public content with CloudFront and an Origin Access Control rather than a public bucket. - Encrypt and require TLS. Enforce default SSE-KMS encryption and add a policy denying requests where
aws:SecureTransportis false. - Turn on logging and detection. Enable S3 server access logging or CloudTrail data events, and let IAM Access Analyzer / Macie flag publicly accessible or sensitive buckets continuously.
- Govern the namespace. Remove dangling references promptly when a bucket is deleted, and avoid predictable, guessable bucket names for sensitive data.
# Lock down a bucket: enforce BPA and kill ACLs
aws s3api put-public-access-block --bucket company-assets \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
aws s3api put-bucket-ownership-controls --bucket company-assets \
--ownership-controls 'Rules=[{ObjectOwnership=BucketOwnerEnforced}]'
S3 testing rewards methodical enumeration over flashy exploits — most critical findings are simply a public object that should have been private. Map the permission layers, enumerate broadly within scope, prove impact conservatively, and hand back a remediation plan that starts with Block Public Access. For broader cloud methodology to chain alongside S3 findings, keep the Cloud Attacks cheat sheet close.
Level up your security testing
Install the CLI
npx payload-playgroundExplore All Tools
Encoding, hashing, JWT & more
Browse Cheat Sheets
Quick-reference payload guides