Subdomain takeover is a subtle but serious vulnerability that happens whenever you host a DNS record for a subdomain pointing at an external service (e.g., a third-party CDN, storage bucket, or older app) that is no longer bound to your account. Attackers scan for dangling DNS and create the missing resource, gaining control of the subdomain and any web traffic routed to it.
How Subdomain Takeover Happens
A subdomain takeover typically follows this pattern:
- You create a DNS
CNAME/Arecord pointingapp.example.comto a provider-managed endpoint (such asapp.s3.amazonaws.com,app.azurewebsites.net,app.github.io, etc.). - You later delete or misconfigure the dependent resource on the provider side — maybe the storage bucket was removed, the CDN deployment expired, or the GitHub Pages repository was renamed — but you forget to remove the DNS record.
- An attacker notices the "dangling" DNS target, provisions a resource at the same provider endpoint (e.g., re-creates a bucket named
appin the same service), and the provider happily serves traffic from the attacker-controlled content because DNS still points there. Even if the adversary is just someone who gains access to those newly available resources (not necessarily a targeted attacker), they inherit whatever incoming requests and data are routed to the subdomain until the takeover is remediated.
Because TLS certificates and cookies are scoped by hostname, the attacker can impersonate the subdomain, overlay malicious content, deface static sites, host phishing pages, or intercept partner integrations.
Common Vectors
- Cloud storage services (Amazon S3, Azure Blob, Google Cloud Storage) when buckets are deleted but DNS still points to the storage endpoint.
- Middleware endpoints / VMs / egress gateways hosted in managed platforms (API gateways, proxy services, egress points) where the service is deprovisioned but DNS continues to resolve to its former address.
- Managed hosting platforms such as GitHub Pages, Netlify, Vercel, Azure Web Apps, Heroku, and an array of CI/CD preview environments.
- CDN distributions that retain DNS records pointing to inactive distributions or misconfigured origins.
- Email providers and verification services that require TXT records; mismanaging them can also expose takeover paths in specialized cases.
Detecting Subdomain Takeover on Your Domains : Manual reconnaissance
- Export or download your DNS zone data from the registrar/Cloud provider and manually review every subdomain (CNAME/A records) for targets you no longer control or don't recognize.
- Use
digornslookupto enumerate subdomains and inspect CNAME/A records for red flags (e.g., pointing to deleted resources or provider placeholders likena1n0pn7h0.execute-api.us). - Look for HTTP responses like "NoSuchBucket" (S3), "The specified blob does not exist" (Azure), or provider-provided error pages that hint the target resource is absent.
- Periodically reconfirm every DNS record you manage: run
dig/nslookupon the entire zone, verify each IP address or CNAME target still belongs to your infrastructure, and ensure the CNAME resolves to a service endpoint you actively control. This guards against forgotten entries and supply-chain reallocations.
Detecting Subdomain Takeover on your domains : Command-line tools
subfinder, amass, assetfinder, and similar OSINT tools: enumerate subdomains at scale by querying public records, certificates, or brute-forcing common names. Export their output and filter for unique hostnames before forwarding them to detection helpers.
Installing the enumeration tools
- Linux (Debian/Ubuntu)
Install Go if missing: sudo apt install golang-go or follow the official download for the latest Go release.
Install each tool via go install:
go install github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest
go install github.com/OWASP/Amass/v3/...@latest
go install github.com/tomnomnom/assetfinder@latestEnsure $HOME/go/bin is exported to your PATH (e.g., export PATH=$PATH:$HOME/go/bin), then run subfinder -version, etc. to confirm.
- macOS (Homebrew)
brew install subfinder amassgo install github.com/tomnomnom/assetfinder@latest(ensure$HOME/go/binis in yourPATH). Runassetfinder --helpafterward to verify the binary is available.- There are community releases (prebuilt binaries) under
https://github.com/tomnomnom/assetfinder/releases. Download the macOS or Linux build, unzip it (e.g.,tar -xzf assetfinder_*_darwin_amd64.tar.gz), and move theassetfinderbinary into/usr/local/binor another directory that's on yourPATH.
Homebrew places binaries in /opt/homebrew/bin (Apple silicon) or /usr/local/bin, so they work immediately.
Subfinder logic
Subfinder is a passive subdomain discovery tool: it queries public sources rather than brute-forcing names. It works by tying together dozens of APIs (certificate transparency logs, public DNS data, search engines, archive services, etc.) and extracting any hostname that ends with the target domain.
When you run subfinder -d example.com, it fans out requests to those providers (depending on enabled sources) and aggregates every subdomain it finds without touching your own DNS. This minimizes the risk of triggering rate limits or being blocked.
Subfinder also supports bruteforce dictionaries if you enable them, but its default behavior is passive enumeration plus filtering duplicates — so it surfaces historically or externally visible subdomains rather than guessing random hostnames.
The tool deduplicates the results, optionally tests their resolvability, and streams them (stdout or a file) so you can pipe them into scanners, the Python takeover script, or other analysis workflows.
subfinder -d example.comOWASP Amass
OWASP Amass is an attack-surface discovery / external asset mapping framework. In practice, it helps you build an inventory of internet-facing assets that belong to an organization — most commonly subdomains, plus related infrastructure (IPs, ASNs, CIDRs, certificates, reverse-DNS hints, etc.).
amass enum -d example.com -oA amass -dir ./amass-outputThe above command lets you gather DNS, certificate, and passive data
-oA stands for "output all" and writes the text (.txt), JSON (.json), and sqlite (.db) files using the base name you supply (amass). The files only appear once enumeration finishes and only if Amass found data to write.
amass enum -d example.com -oA amass -dir ./amass-output -v -src -ip -brute -min-for-recursive 2The above command adds verbose logging (-v), records which data sources were queried (-src), captures IP addresses (-ip), enables brute-force dictionary enumeration (-brute), and insists on recursive exploration beyond two levels (-min-for-recursive 2), giving you a more exhaustive asset feed to check against dangling DNS records.
To list the subdomains detected run the following command command
amass trackor
amass assoc -t1 '<fqdn:miko-robot.in> - <*:node> -> <fqdn:*>' | jq -r '.entity.edges.[].entity.asset.name'AssetFinder
Assetfinder is a lightweight Go utility that scrapes certificate logs, search indexes, and DNS data for names that match your domain. Because it defaults to passive sources, it gives you high-confidence subdomains without generating unnecessary DNS queries.
Use assetfinder --subs-only example.com when you just need the already-resolving hosts.
Automated scanning (Python script)
The following Python script looks for potentially dangling CNAME targets by resolving subdomains and comparing them to a list of known defunct patterns:
import sys
from typing import List
import dns.resolver
import requests
COMMON_TAKEOVER_TARGETS = [
"s3.amazonaws.com",
"azurewebsites.net",
"github.io",
"netlify.app",
"vercel.sh",
]
HTTP_ERROR_PATTERNS = [
"nosuchbucket",
"the specified blob does not exist",
"site not found",
"page not found",
"not found",
"404",
"503",
"repository not found",
"studio cannot find",
"no such host",
]
REQUEST_TIMEOUT = 6
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
def resolve_records(hostname: str, record_type: str) -> List[str]:
try:
answers = dns.resolver.resolve(hostname, record_type, raise_on_no_answer=False)
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers, dns.resolver.LifetimeTimeout):
return []
return [record.to_text().strip(".") for record in answers] if answers else []
def looks_like_takeover(target: str) -> bool:
return any(part in target for part in COMMON_TAKEOVER_TARGETS)
def check_http(hostname: str) -> bool:
try:
resp = requests.get(f"https://{hostname}", timeout=REQUEST_TIMEOUT, allow_redirects=True, verify=False)
except requests.RequestException as exc:
#print(f"[!] {hostname}: needs investigation )")
return False
body = resp.text.lower()
reasons = []
if resp.status_code >= 400:
reasons.append(f"status {resp.status_code}")
for keyword in HTTP_ERROR_PATTERNS:
if keyword in body:
reasons.append(keyword)
if reasons:
print(f"[!] {hostname}: needs investigation ")
return True
return False
def inspect(hostname: str) -> None:
cnames = resolve_records(hostname, "CNAME")
flagged = False
if not cnames:
if resolve_records(hostname, "A") or resolve_records(hostname, "AAAA"):
flagged |= check_http(hostname)
else:
print(f"[!] {hostname}: no DNS records found (possibly removed)")
flagged = True
else:
for target in cnames:
if looks_like_takeover(target):
print(f"[!] {hostname} -> {target} (possible dangling target)")
flagged = True
flagged |= check_http(hostname)
if not flagged:
print(f"{hostname}: no obvious takeover indicators detected")
if __name__ == "__main__":
for subdomain in sys.argv[1:]:
inspect(subdomain)- Install the Python requirements with
pip install dnspython requests. Those packages provide the DNS resolver (dns.resolver) and the HTTP client used in the script.
Run the script as python detect_takeover.py foo.example.com bar.example.com. Extend COMMON_TAKEOVER_TARGETS as you learn more about the services you use.
Example pipeline:
subfinder -d example.com | tee subdomains.txtamass track -d example.com | tee -a subdomains.txtassetfinder --subs-only example.com | tee -a subdomains.txtcat subdomains.txt | sort -u | xargs -n1 python3./detect_takeover.py- Review the flagged hosts, then pair each with
dig/curlfor final verification—dig/nslookupquickly shows their DNS targets and TTLs, whilecurl -Iexposes HTTP status, headers, and provider error text that confirms the endpoint is stale or misconfigured. - For bulk verification, pipe filtered
subdomains.txtthrough a small shell loop that runsdigandcurl, flagging failures:
while read host; do
dig +short "$host" | grep -q '.' || echo "missing DNS: $host"
curl -sI "https://$host" | head -n1 | grep -qE "404|NoSuchBucket" && echo "broken HTTP: $host"
done < subdomains.txtRecovering from a Subdomain Takeover
- Delete or correct the DNS record if the resource is gone. Remove dangling CNAME/A records or point them to a valid, actively managed service.
- Recreate the external resource under your control if the DNS must remain unchanged — e.g., re-create the Azure Web App that DNS references and redeploy legitimate content.
- Rotate certificates and secrets bound to the compromised subdomain (CSRF tokens, API keys, third-party integrations, and cookies) because the attacker might have intercepted requests during the takeover window.
- Monitor logs for anomalous traffic to
app.example.comand notify stakeholders of any data or session exposure.
Preventive Best Practices
- Maintain an inventory of all DNS entries and the services they map to; avoid one-off CNAMEs for short-lived experiments unless you document their lifecycle.
- Automate cleanup: tie domain records to provisioning/deprovisioning pipelines so DNS is removed when the service is destroyed.
- Use provider APIs (AWS Route53, Cloudflare, etc.) to cross-validate DNS entries and the presence of corresponding resources.
- Keep monitoring in place (via periodic scans or monitoring tools like SecurityScorecard, Detectify, or custom cron jobs) to flag new dangling references.
- Apply security hardening such as shortest TTLs during experimentation phases so stale records expire sooner.
Wrap-up
Subdomain takeover can be devastating because it looks normal from the outside until an attacker leverages it. Regular discovery, automated detection, careful DNS hygiene, and swift recovery steps ensure your domains remain under your control — plus the extra peace of mind that no forgotten CNAME is hosting someone else's payload.
Next steps
- Schedule the Python detector and your enumeration stack (
subfinder,amass,assetfinder) on a cadence (e.g., weekly) so every DNS change is evaluated before it becomes a takeover window. - Feed the flagged hosts into your incident response channel (alerts, tickets) so operators can triage misconfigurations and rotate any secrets bound to the affected subdomain.
If you found this helpful, consider following my profile and signing up for the newsletter. Have thoughts or questions? Share them in the comments below.
References
- OWASP Amass Asset DB Triples guide.
- tomnomnom/assetfinder release archives.