A subdomain takeover happens when a DNS record (CNAME, NS, or MX) points at a third-party service that no longer claims the host, so an attacker can re-register the orphaned resource and serve content from a real corporate subdomain. Impact ranges from phishing and cookie theft to NS-takeover-driven full HTTPS impersonation, supply-chain JS injection from re-registered S3 buckets, and password-reset hijacking via MX takeover.
CNAME takeover is the textbook case but NS-takeover is the higher-severity variant — the attacker controls the entire DNS zone and can issue Let's Encrypt certs at will. The 2024-2025 supply-chain S3 research extended the threat into npm and CI artefacts, so an offboarding runbook that deletes the DNS record before the cloud resource is now the table-stakes defense.
The most common class. A subdomain has CNAME pointing at unclaimed-target.<provider>.com; the provider does not verify ownership when a new account claims the same custom hostname, so the attacker registers the orphaned account/app/bucket and serves content. Each payload below is a CNAME pattern paired with the exact error string indicating an unclaimed host. Re-confirm against EdOverflow/can-i-take-over-xyz before public reporting; vendor patches change status frequently.
CNAME → *.s3.amazonaws.com | body: "The specified bucket does not exist" / NoSuchBucket
Bucket name equals the subdomain (assets.example.com → bucket assets.example.com). Re-create the bucket in the original region and serve arbitrary content. Now also a supply-chain primitive when the bucket is referenced by JS or CDN URLs in npm packages.
CNAME → *.elasticbeanstalk.com | resolution: NXDOMAIN (region-specific)
Beanstalk environments expose region-bound CNAMEs. After environment deletion the hostname becomes claimable by a new environment in the same region.
CNAME → *.cloudfront.net | body: "Bad request. ERROR: ..."
CloudFront added DNS verification on alternate-domain bindings in 2018. Only legacy bindings created before the patch remain exploitable; otherwise the provider rejects new claims.
CNAME → *.azurewebsites.net | resolution: NXDOMAIN
Register a new App Service with the orphaned hostname; Azure will bind it instantly without verifying the original owner.
CNAME → *.cloudapp.net or *.cloudapp.azure.com | resolution: NXDOMAIN
Classic and ARM-style Azure cloud-service hostnames are claimable when the original deployment is deleted; same pattern as App Service.
CNAME → *.trafficmanager.net | resolution: NXDOMAIN
Traffic Manager profile names are global; deleted profiles can be re-registered by any tenant and immediately answer for the dangling CNAME.
CNAME → *.azureedge.net | resolution: NXDOMAIN
Azure CDN endpoint names follow the same global-namespace pattern; orphaned endpoints can be re-registered by any tenant.
CNAME → *.herokuapp.com or *.herokudns.com | body: "No such app"
Create an empty Heroku app and run heroku domains:add <victim subdomain>. Region/account-bound, so confirm before reporting.
CNAME → *.github.io | body: "There isn't a GitHub Pages site here."
When the original repo is deleted the CNAME-targeted user/org slot becomes claimable: create a repo at <takeover-account>/<old-cname-target> with a CNAME file matching the victim subdomain.
CNAME → *.fastly.net | body: "Fastly error: unknown domain"
Fastly added host-header verification in 2018, so the error string surfaces but reclaim requires account-level proof. Document and report; do not assume exploitable without further checks.
CNAME → *.myshopify.com | body: "Sorry, this shop is currently unavailable."
Edge case: claimable when the store name is freed. Wildcard inheritance via *.example.com → tenant.shopify.com extends the surface.
CNAME → domains.tumblr.com | body: "Whatever you were looking for doesn't exist..."
Edge case but historically reliable; the brand.zen.ly Zenly disclosure (HackerOne #1474784) was a Tumblr-class example.
CNAME → proxy.webflow.com or proxy-ssl.webflow.com | body: "The page you are looking for doesn't exist"
Edge-case provider; Webflow patches in 2023 reduced but did not eliminate. Reclaim by binding a new Webflow project to the abandoned hostname.
CNAME → cname.vercel-dns.com | body: "DEPLOYMENT_NOT_FOUND" / 404 + x-vercel-id header
Add the orphan hostname to a Vercel project; verification by HTTP file or DNS TXT, both of which the attacker now controls because DNS already points to Vercel.
CNAME → *.netlify.app or *.netlify.com | body: "Not Found - Request ID:"
Edge-case provider; identical reclaim flow to Vercel. The x-served-by: cache-...netlify header in the dangling-host response is a strong fingerprint.
CNAME → *.helpscoutdocs.com | body: "No settings were found for this company"
Community-reported as vulnerable; reclaim by binding the Help Scout docs site to the abandoned subdomain.
CNAME → subdomain.cargocollective.com | body: "404 Not Found" + "If you're moving your domain away from Cargo..."
Reclaim by registering a Cargo Collective account and binding the orphan hostname; provider does not verify prior ownership.
CNAME → *.tilda.ws | body: "Please renew your subscription" / "Domain has been assigned"
Tilda accounts can re-bind an orphan hostname after the original subscription lapses; the renewal-prompt page is the unique fingerprint.
CNAME → unbouncepages.com | body: "The requested URL was not found on this server."
Marketing landing-page builder; reclaim by adding the orphan hostname to a new Unbounce account.
CNAME → *.pantheonsite.io | body: "The gods are wise, but do not know of the site which you seek."
Distinctive fingerprint string. Reclaim by binding the orphan hostname to a Pantheon site.
CNAME → *.s.strikinglydns.com | body: "PAGE NOT FOUND."
Reclaim by adding the orphan hostname to a new Strikingly site. All-caps fingerprint distinguishes from generic 404s.
CNAME → na-west1.surge.sh | body: "project not found"
Surge's CLI accepts arbitrary CNAME targets without ownership proof; reclaim by deploying any project to the orphan hostname.
CNAME → animaapp.com | body: "If this is your website and you've just created it..."
Anima's onboarding screen on an unbound hostname is the fingerprint; reclaim through the platform UI.
CNAME → *.launchrock.com | body: "It looks like you may have taken a wrong turn somewhere."
Pre-launch landing-page service; orphan hostnames are claimable by new accounts.
CNAME → *.readme.io | body: "Project doesnt exist... yet!"
Documentation host. Reclaim by creating a Readme project with the orphan hostname.
sub.example.com has NS pointing at ns1.deadprovider.com. If deadprovider.com is expired or unregistered, the attacker registers it and controls the entire DNS zone for sub.example.com — including issuing Let's Encrypt certs for full HTTPS impersonation. Severity is materially higher than CNAME takeover. Documented at scale by Patrik Hudak and Matthew Bryant's 120k-domain study (2016, still cited because the vector remains unsolved).
dig +short NS sub.example.com → ns1.deadprovider.com (whois: AVAILABLE)
Confirm the NS target's base domain is unregistered with whois, then register it. The new registration owns the zone and can issue arbitrary records, including DNS-01 ACME challenges for valid TLS certs.
dig +short NS sub.example.com → ns-123.awsdns-12.com (zone unclaimed in Route53 UI)
NS points at Route53 / DigitalOcean / Rackspace nameservers but the zone is not claimed inside the provider account. Attacker creates the zone in their own account and the provider serves their records.
nslookup sub.example.com 8.8.8.8 vs. nslookup sub.example.com 1.1.1.1 → SERVFAIL
If only one of two delegated nameservers responds, the zone is partially claimable / misconfigured. Strong indicator that a delegated zone is up for grabs.
MX 10 mx.deadprovider.com where the MX target is unclaimed. The attacker stands up an SMTP listener and receives mail to that domain. Lower direct severity, but high impact for password-reset hijacking, vendor-invoice fraud, and OAuth/SaaS account takeover where support@-style aliases are accepted by downstream services.
dig +short MX example.com → mx.deadprovider.com (unregistered)
Register the MX target domain, run an SMTP receiver, and collect inbound mail. Use it to receive password-reset tokens for SaaS accounts that allow shared aliases like support@ or admin@.
POST /forgot-password { "email": "support@victim.tld" } → reset link delivered to attacker SMTPAfter hijacking MX, trigger a password reset on a downstream SaaS that accepts shared aliases. The reset email lands on the attacker's SMTP listener, completing the ATO chain.
Class formalised by 2024-2025 research: deleted S3 buckets referenced inside JS files, CDN URLs, npm packages or CI logs are re-registered by attackers, who then serve malicious JavaScript directly into customer build pipelines. Distinct from a CNAME takeover because there may be no DNS record at all — the URL is hard-coded into a published artefact.
GET https://victim-deleted-bucket.s3.amazonaws.com/v1/widget.js → NoSuchBucket
Find references to s3.amazonaws.com or s3-website-<region>.amazonaws.com inside published bundles, npm tarballs, or CI artefacts; if the bucket is deleted, register it in the same region and serve attacker JS to every consumer of that artefact.
postinstall: curl -sSL https://victim-cdn.s3.amazonaws.com/installer.sh | bash
npm postinstall hooks that pull from a deleted S3 bucket allow the attacker to execute arbitrary code on every install once the bucket is re-registered.
Claim an unclaimed Route53/DigitalOcean/Rackspace zone whose NS records still point to the provider; the attacker controls authoritative DNS without needing to register a domain. Trivially issues Let's Encrypt certificates via DNS-01 → full HTTPS impersonation of the victim subdomain.
dig NS sub.example.com → ns-123.awsdns-12.com (zone uncreated in target Route53 account)
If support.example.com MX is dangling, the attacker stands up an SMTP listener and triggers password resets on downstream SaaS that accept shared aliases. 2024 disclosures show this used as a full account-takeover chain, not just a phishing primitive.
*.example.com CNAME → tenant.shopify.com means any non-existent label inherits the dangling target. Shopify, Webflow, and Vercel patches in 2023 reduced but did not eliminate this; wildcard CNAMEs into multi-tenant providers remain a high-risk pattern.
*.example.com IN CNAME tenant.shopify.com (tenant since deleted → every label is takeoverable)
Researchers (Oct 2024 – Jan 2025) re-registered abandoned S3 buckets referenced inside npm packages and CI artefacts, demonstrating that subdomain takeover has graduated into a supply-chain primitive. Hard-coded https://*.s3.amazonaws.com URLs in JavaScript bundles are now an attack surface in their own right.
Provider patches verify ownership on initial bind but did not retroactively invalidate older un-verified bindings. 2024 Detectify scans still found thousands of CloudFront and Fastly hosts exploitable through legacy entries.
Make DNS-record deletion a checklist gate that runs before the cloud resource is torn down. Most takeovers exist because the order was reversed. The script below pairs Route53 record deletion with the resource teardown so the two cannot drift.
#!/usr/bin/env bash
# offboard-subdomain.sh — delete DNS first, then the resource.
# Usage: ./offboard-subdomain.sh assets.example.com s3 my-bucket
set -euo pipefail
SUBDOMAIN="$1" # assets.example.com
RESOURCE_TYPE="$2" # s3 | beanstalk | heroku | netlify | vercel | azure-app
RESOURCE_ID="$3" # bucket / app / project name
ZONE_ID="$(aws route53 list-hosted-zones --query 'HostedZones[?Name==`example.com.`].Id' --output text)"
# 1. Snapshot the current record so we can roll back.
aws route53 list-resource-record-sets --hosted-zone-id "$ZONE_ID" \
--query "ResourceRecordSets[?Name==\`$SUBDOMAIN.\`]" > "/tmp/$SUBDOMAIN.json"
# 2. Delete the DNS record FIRST.
aws route53 change-resource-record-sets --hosted-zone-id "$ZONE_ID" --change-batch \
"{\"Changes\":[{\"Action\":\"DELETE\",\"ResourceRecordSet\":$(jq '.[0]' "/tmp/$SUBDOMAIN.json")}]}"
# 3. Confirm propagation before tearing down the cloud resource.
for i in 1 2 3 4 5; do
if ! dig +short "$SUBDOMAIN" | grep -q .; then
echo "DNS removed; tearing down $RESOURCE_TYPE/$RESOURCE_ID"
case "$RESOURCE_TYPE" in
s3) aws s3 rb "s3://$RESOURCE_ID" --force ;;
heroku) heroku apps:destroy --app "$RESOURCE_ID" --confirm "$RESOURCE_ID" ;;
*) echo "Manual teardown required for $RESOURCE_TYPE"; exit 0 ;;
esac
exit 0
fi
sleep 30
done
echo "DNS still resolving after 2.5 min — abort." >&2
exit 1
Pair nightly subdomain enumeration (amass / subfinder / chaos / SecurityTrails) with provider-fingerprint scanning (subzy, nuclei takeover templates, dnsReaper) and Certificate-Transparency-log monitoring. CT alerts catch the case where someone — friend or foe — already issued a cert for a host you forgot you owned.
# .github/workflows/subdomain-watch.yml
name: subdomain-takeover-watch
on:
schedule:
- cron: '0 3 * * *' # nightly
workflow_dispatch:
jobs:
enumerate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Enumerate subdomains
run: |
subfinder -silent -d example.com > subs.txt
amass enum -passive -d example.com >> subs.txt
sort -u subs.txt -o subs.txt
- name: Fingerprint scan (subzy)
run: |
go install github.com/PentestPad/subzy@latest
subzy run --targets subs.txt --hide_fails
- name: Nuclei takeover templates
run: nuclei -t http/takeovers/ -l subs.txt -severity high,critical
- name: CT-log diff
run: |
curl -sf "https://crt.sh/?q=%25.example.com&output=json" \
| jq -r '.[].name_value' | sort -u > certs.today
diff certs.yesterday certs.today | tee ct-diff.txt || true
mv certs.today certs.yesterday
- name: Alert on findings
if: failure()
run: ./scripts/page-secops.sh
Every DNS record should map to a system-of-record entry naming the cloud account, owner, and resource ARN. CMDB / IPAM ownership records are the upstream control that makes nightly fingerprint scans actionable — without them, alerts have nobody to route to.
On AWS, prefer Route53 alias records pointing directly at S3 / CloudFront / ELB ARNs over CNAMEs. Alias records are bound to the resource at evaluation time, so deleting the resource breaks the record explicitly instead of leaving it dangling.
# Terraform — alias record (no dangling-CNAME class)
resource "aws_route53_record" "assets" {
zone_id = aws_route53_zone.main.zone_id
name = "assets.example.com"
type = "A"
alias {
name = aws_cloudfront_distribution.assets.domain_name
zone_id = aws_cloudfront_distribution.assets.hosted_zone_id
evaluate_target_health = false
}
}
# A null MX record on every domain you do NOT actively send/receive mail from.
resource "aws_route53_record" "null_mx" {
zone_id = aws_route53_zone.main.zone_id
name = "example.com"
type = "MX"
ttl = 86400
records = ["0 ."]
}
Avoid *.example.com → vendor.tld unless every label is owned, monitored, and revoked when retired. Wildcard inheritance into multi-tenant providers is the single largest source of "we did not even know that subdomain existed" takeovers.
Alert on SERVFAIL or REFUSED responses from any authoritative NS for your domains, and set MX 0 . on every domain you do not actively send/receive mail from. This neutralises the NS- and MX-takeover classes that CNAME-only monitoring misses.
For SaaS that issues custom domains: enforce HTTP-file or DNS-TXT verification before activating, and reject re-binding unless the original account proves control. This is what CloudFront/Fastly/Squarespace did; the providers still on the vulnerable list have not yet adopted equivalent controls.
| CVE | Year | Title | Description |
|---|---|---|---|
| HackerOne #1717626 | 2022 | Consensys subdomain takeover | Modern (2022) provider takeover on a CNAME-pointing subdomain. Fix involved DNS record removal rather than provider-side change, demonstrating that customer-owned DNS hygiene is the durable control. |
| HackerOne #1474784 | 2022 | Zenly brand.zen.ly Tumblr-class takeover | Edge-case provider (Tumblr) caught by a missing DNS-cleanup step after decommissioning the brand site. Classic example of the offboarding-runbook gap. |
| HackerOne #202767 | 2017 | AWS S3 NoSuchBucket subdomain takeover | Early canonical disclosure: a CNAME pointing at a deleted S3 bucket allowed an attacker to re-register the bucket and serve arbitrary content under a real corporate subdomain. Still cited because the pattern is alive in 2024-2025 supply-chain studies. |
| HackerOne #1697402 | 2022 | 8x8 subdomain takeover | Public bounty disclosure (partially redacted) showing continued prevalence on enterprise SaaS; reinforced that nightly fingerprint scans remain valuable in 2022-onwards programs. |
| Detectify 2024 measurement | 2024 | Persistent vulnerability across major providers | Large-scale Detectify scan confirmed thousands of dangling CNAMEs across Azure, Heroku, GitHub Pages, Vercel and Netlify despite years of patches. Empirical evidence that defense-in-depth (offboarding + monitoring) is non-optional. |
| Supply-chain S3 study (Oct 2024 – Jan 2025) | 2025 | Re-registered abandoned S3 buckets in npm/CI artefacts | Researchers re-registered abandoned S3 buckets referenced from npm packages and CI build logs, demonstrating that subdomain takeover has graduated into a supply-chain attack primitive against build pipelines. |