threat hunting

Hunting for DNS-based Persistence

Hunt for DNS-based persistence mechanisms including DNS hijacking, dangling CNAME records, wildcard DNS abuse, and unauthorized zone modifications using passive DNS databases, SecurityTrails API, and DNS audit log analysis.

dnsdns-hijackingpassive-dnspersistencesecuritytrailssubdomain-takeoverthreat-hunting
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

Attackers establish DNS-based persistence by hijacking DNS records, creating unauthorized subdomains, abusing wildcard DNS entries, or modifying NS delegations to redirect traffic through attacker-controlled infrastructure. These techniques survive credential rotations, endpoint reimaging, and traditional remediation because DNS changes persist independently of compromised hosts. Detection requires passive DNS historical analysis, zone file auditing, and monitoring for unauthorized record modifications. This skill covers hunting methodologies using SecurityTrails passive DNS API, DNS audit logs from Route53/Azure DNS/Cloudflare, and zone transfer analysis.

When to Use

  • When investigating security incidents that require hunting for dns based persistence
  • When building detection rules or threat hunting queries for this domain
  • When SOC analysts need structured procedures for this analysis type
  • When validating security monitoring coverage for related attack techniques

Prerequisites

  • SecurityTrails API key (free tier provides 50 queries/month)
  • Access to DNS provider audit logs (Route53, Azure DNS, Cloudflare, or on-premises DNS)
  • Python 3.9+ with requests library
  • DNS zone file access or AXFR capability for internal zones
  • Historical DNS baseline for comparison

Steps

Step 1: Baseline DNS Records

Export current DNS zone records and establish baseline for all authorized A, AAAA, CNAME, MX, NS, and TXT records.

Step 2: Query Passive DNS History

Use SecurityTrails API to retrieve historical DNS records and identify unauthorized changes, new subdomains, and CNAME records pointing to decommissioned services (dangling CNAMEs).

Step 3: Detect Anomalies

Compare current records against baseline to identify unauthorized modifications, wildcard records that resolve all subdomains, NS delegation changes, and MX record hijacking.

Step 4: Investigate Findings

Correlate DNS anomalies with threat intelligence feeds, check resolution targets against known malicious infrastructure, and validate record ownership.

Expected Output

JSON report listing DNS anomalies with record type, historical changes, risk severity, and remediation recommendations for each finding.

Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md2.6 KB

API Reference: Hunting for DNS-based Persistence

SecurityTrails API

# Get current DNS records
curl -s "https://api.securitytrails.com/v1/domain/example.com" \
  -H "APIKEY: ${ST_API_KEY}"
 
# Get subdomains
curl -s "https://api.securitytrails.com/v1/domain/example.com/subdomains" \
  -H "APIKEY: ${ST_API_KEY}"
 
# Historical A records
curl -s "https://api.securitytrails.com/v1/history/example.com/dns/a" \
  -H "APIKEY: ${ST_API_KEY}"
 
# Historical NS records
curl -s "https://api.securitytrails.com/v1/history/example.com/dns/ns" \
  -H "APIKEY: ${ST_API_KEY}"
 
# WHOIS history
curl -s "https://api.securitytrails.com/v1/history/example.com/whois" \
  -H "APIKEY: ${ST_API_KEY}"

DNS Record Types to Hunt

Record Persistence Risk Detection
A/AAAA IP hijacking to attacker infra Compare against baseline
CNAME Subdomain takeover via dangling records Resolve target, check NXDOMAIN
NS Full zone delegation hijack Compare against registrar NS
MX Email interception Monitor for unauthorized MX
TXT C2 data exfiltration channel Check for encoded payloads
Wildcard (*) Catch-all subdomain resolution Test random subdomain resolution

Dangling CNAME Services (Subdomain Takeover)

Service CNAME Pattern Takeover Method
AWS S3 *.s3.amazonaws.com Create matching bucket
GitHub Pages *.github.io Create matching repo
Azure Web Apps *.azurewebsites.net Register app name
Heroku *.herokuapp.com Create matching app
Shopify *.myshopify.com Claim custom domain
CloudFront *.cloudfront.net Create matching distribution

dig Commands for Hunting

# Check all record types
dig ANY example.com +noall +answer
 
# Test for wildcard records
dig A randomtest123.example.com +short
 
# Check NS delegation chain
dig NS example.com +trace
 
# Verify DNSSEC
dig DNSKEY example.com +dnssec +short
 
# Check for zone transfer (authorized testing only)
dig AXFR example.com @ns1.example.com

MITRE ATT&CK DNS Techniques

Technique ID Description
DNS Hijacking T1584.001 Modify DNS records to redirect traffic
DNS Server T1583.002 Acquire DNS server for C2
Domain Fronting T1090.004 Use CDN to mask C2
DNS Tunneling T1572 Encode data in DNS queries

References

Scripts 1

agent.py6.6 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""DNS Persistence Hunting Agent - detects DNS hijacking, dangling CNAMEs, and unauthorized zone changes."""

import json
import argparse
import logging
import requests
import subprocess
from datetime import datetime

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)

DANGLING_CNAME_SERVICES = [
    "s3.amazonaws.com", "cloudfront.net", "azurewebsites.net", "herokuapp.com",
    "github.io", "pantheonsite.io", "readme.io", "surge.sh",
    "unbouncepages.com", "wordpress.com", "shopify.com",
]


def query_securitytrails(domain, api_key):
    """Query SecurityTrails API for passive DNS history."""
    headers = {"APIKEY": api_key, "Content-Type": "application/json"}
    url = f"https://api.securitytrails.com/v1/history/{domain}/dns/a"
    resp = requests.get(url, headers=headers, timeout=30)
    resp.raise_for_status()
    return resp.json()


def get_subdomains(domain, api_key):
    """Enumerate subdomains via SecurityTrails API."""
    headers = {"APIKEY": api_key}
    url = f"https://api.securitytrails.com/v1/domain/{domain}/subdomains"
    resp = requests.get(url, headers=headers, timeout=30)
    resp.raise_for_status()
    data = resp.json()
    return [f"{sub}.{domain}" for sub in data.get("subdomains", [])]


def resolve_record(hostname, record_type="A"):
    """Resolve DNS record using dig command."""
    cmd = ["dig", "+short", record_type, hostname]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
    return [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]


def detect_dangling_cnames(subdomains):
    """Detect CNAME records pointing to decommissioned services (subdomain takeover risk)."""
    findings = []
    for subdomain in subdomains:
        cname_records = resolve_record(subdomain, "CNAME")
        for cname in cname_records:
            for service in DANGLING_CNAME_SERVICES:
                if cname.rstrip(".").endswith(service):
                    a_records = resolve_record(cname, "A")
                    if not a_records:
                        findings.append({
                            "subdomain": subdomain, "cname": cname.rstrip("."),
                            "service": service, "resolves": False,
                            "severity": "critical",
                            "issue": "Dangling CNAME - subdomain takeover possible",
                            "mitre": "T1584.001",
                        })
    return findings


def detect_wildcard_abuse(domain):
    """Check for wildcard DNS records that resolve all subdomains."""
    findings = []
    random_sub = f"randomtestxyz123.{domain}"
    results = resolve_record(random_sub, "A")
    if results:
        findings.append({
            "domain": domain, "wildcard_target": results,
            "severity": "high",
            "issue": "Wildcard DNS record detected - all subdomains resolve",
            "detail": f"Random subdomain {random_sub} resolves to {results}",
        })
    return findings


def check_ns_delegation(domain):
    """Verify NS records haven't been modified to point to unauthorized servers."""
    findings = []
    ns_records = resolve_record(domain, "NS")
    known_providers = ["awsdns", "azure-dns", "cloudflare", "google", "route53", "ns1."]
    for ns in ns_records:
        ns_lower = ns.lower().rstrip(".")
        is_known = any(provider in ns_lower for provider in known_providers)
        if not is_known:
            findings.append({
                "domain": domain, "nameserver": ns_lower,
                "severity": "high",
                "issue": "NS record points to unrecognized nameserver",
                "recommendation": "Verify this is an authorized nameserver for the domain",
            })
    return findings


def analyze_dns_history(domain, api_key):
    """Analyze historical DNS changes for signs of hijacking."""
    findings = []
    try:
        history = query_securitytrails(domain, api_key)
        records = history.get("records", [])
        if len(records) >= 2:
            for i in range(1, len(records)):
                prev_ips = set(v.get("ip", "") for v in records[i - 1].get("values", []))
                curr_ips = set(v.get("ip", "") for v in records[i].get("values", []))
                changed = curr_ips - prev_ips
                if changed:
                    findings.append({
                        "domain": domain,
                        "first_seen": records[i].get("first_seen", ""),
                        "new_ips": list(changed),
                        "severity": "medium",
                        "issue": f"DNS A record changed: added {changed}",
                    })
    except requests.RequestException as e:
        logger.warning("SecurityTrails query failed: %s", e)
    return findings


def generate_report(domain, dangling, wildcard, ns_findings, history_findings):
    all_findings = dangling + wildcard + ns_findings + history_findings
    critical = sum(1 for f in all_findings if f.get("severity") == "critical")
    return {
        "timestamp": datetime.utcnow().isoformat(),
        "target_domain": domain,
        "dangling_cnames": dangling,
        "wildcard_abuse": wildcard,
        "ns_delegation_issues": ns_findings,
        "historical_anomalies": history_findings,
        "total_findings": len(all_findings),
        "critical_findings": critical,
        "risk_level": "critical" if critical > 0 else "high" if all_findings else "low",
    }


def main():
    parser = argparse.ArgumentParser(description="DNS Persistence Hunting Agent")
    parser.add_argument("--domain", required=True, help="Target domain to hunt")
    parser.add_argument("--api-key", required=True, help="SecurityTrails API key")
    parser.add_argument("--output", default="dns_persistence_report.json")
    args = parser.parse_args()

    subdomains = get_subdomains(args.domain, args.api_key)
    logger.info("Discovered %d subdomains for %s", len(subdomains), args.domain)
    dangling = detect_dangling_cnames(subdomains[:200])
    wildcard = detect_wildcard_abuse(args.domain)
    ns_findings = check_ns_delegation(args.domain)
    history_findings = analyze_dns_history(args.domain, args.api_key)
    report = generate_report(args.domain, dangling, wildcard, ns_findings, history_findings)
    with open(args.output, "w") as f:
        json.dump(report, f, indent=2, default=str)
    logger.info("DNS hunt: %s - %d findings (%d critical), %d subdomains checked",
                args.domain, report["total_findings"], report["critical_findings"], len(subdomains))
    print(json.dumps(report, indent=2, default=str))


if __name__ == "__main__":
    main()
Keep exploring