npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
NIST CSF 2.0
When to Use
Use this skill when:
- SOC teams suspect data exfiltration through DNS tunneling to bypass firewall/proxy controls
- Threat intelligence indicates adversaries using DNS-based C2 channels (e.g., Cobalt Strike DNS beacon)
- UEBA detects anomalous DNS query volumes from specific hosts
- Malware analysis reveals DNS-over-HTTPS (DoH) or DNS tunneling capabilities
Do not use for standard DNS troubleshooting or availability monitoring — this skill focuses on security-relevant DNS abuse detection.
Prerequisites
- DNS query logging enabled (Windows DNS Server, Bind, Infoblox, or Cisco Umbrella)
- DNS logs ingested into SIEM (Splunk with
Stream:DNS,dnssourcetype, or Zeek DNS logs) - Passive DNS data for historical domain resolution analysis
- Baseline of normal DNS behavior (query volume, domain distribution, TXT record frequency)
- Python with
mathandcollectionslibraries for entropy calculation
Workflow
Step 1: Detect DNS Tunneling via Subdomain Length Analysis
DNS tunneling encodes data in subdomain labels, creating unusually long queries:
index=dns sourcetype="stream:dns" query_type IN ("A", "AAAA", "TXT", "CNAME", "MX")
| eval domain_parts = split(query, ".")
| eval subdomain = mvindex(domain_parts, 0, mvcount(domain_parts)-3)
| eval subdomain_str = mvjoin(subdomain, ".")
| eval subdomain_len = len(subdomain_str)
| eval tld = mvindex(domain_parts, -1)
| eval registered_domain = mvindex(domain_parts, -2).".".tld
| where subdomain_len > 50
| stats count AS queries, dc(query) AS unique_queries,
avg(subdomain_len) AS avg_subdomain_len,
max(subdomain_len) AS max_subdomain_len,
values(src_ip) AS sources
by registered_domain
| where queries > 20
| sort - avg_subdomain_len
| table registered_domain, queries, unique_queries, avg_subdomain_len, max_subdomain_len, sourcesStep 2: Detect High-Entropy Domain Queries (DGA Detection)
Domain Generation Algorithms produce random-looking domains:
index=dns sourcetype="stream:dns"
| eval domain_parts = split(query, ".")
| eval sld = mvindex(domain_parts, -2)
| eval sld_len = len(sld)
| eval char_count = sld_len
| eval vowels = len(replace(sld, "[^aeiou]", ""))
| eval consonants = len(replace(sld, "[^bcdfghjklmnpqrstvwxyz]", ""))
| eval digits = len(replace(sld, "[^0-9]", ""))
| eval vowel_ratio = if(char_count > 0, vowels / char_count, 0)
| eval digit_ratio = if(char_count > 0, digits / char_count, 0)
| where sld_len > 12 AND (vowel_ratio < 0.2 OR digit_ratio > 0.3)
| stats count AS queries, dc(query) AS unique_domains, values(src_ip) AS sources
by query
| where unique_domains > 10
| sort - queriesPython-based Shannon Entropy Calculation for DNS queries:
import math
from collections import Counter
def shannon_entropy(text):
"""Calculate Shannon entropy of a string"""
if not text:
return 0
counter = Counter(text.lower())
length = len(text)
entropy = -sum(
(count / length) * math.log2(count / length)
for count in counter.values()
)
return round(entropy, 4)
# Test with examples
normal_domain = "google" # Low entropy
dga_domain = "x8kj2m9p4qw7n" # High entropy
tunnel_subdomain = "aGVsbG8gd29ybGQ.evil.com" # Base64 encoded data
print(f"Normal: {shannon_entropy(normal_domain)}") # ~2.25
print(f"DGA: {shannon_entropy(dga_domain)}") # ~3.70
print(f"Tunnel: {shannon_entropy(tunnel_subdomain)}") # ~3.50
# Threshold: entropy > 3.5 for subdomain = likely tunneling/DGASplunk implementation of entropy scoring:
index=dns sourcetype="stream:dns"
| eval domain_parts = split(query, ".")
| eval check_string = mvindex(domain_parts, 0)
| eval check_len = len(check_string)
| where check_len > 8
| eval chars = split(check_string, "")
| stats count AS total_chars, dc(chars) AS unique_chars by query, src_ip, check_string, check_len
| eval entropy_estimate = log(unique_chars, 2) * (unique_chars / check_len)
| where entropy_estimate > 3.5
| stats count AS high_entropy_queries, dc(query) AS unique_queries by src_ip
| where high_entropy_queries > 50
| sort - high_entropy_queriesStep 3: Detect Anomalous DNS Query Volume
Identify hosts generating abnormal DNS traffic:
index=dns sourcetype="stream:dns" earliest=-24h
| bin _time span=1h
| stats count AS queries, dc(query) AS unique_domains by src_ip, _time
| eventstats avg(queries) AS avg_queries, stdev(queries) AS stdev_queries by src_ip
| eval z_score = (queries - avg_queries) / stdev_queries
| where z_score > 3 OR queries > 5000
| sort - z_score
| table _time, src_ip, queries, unique_domains, avg_queries, z_scoreDetect TXT record abuse (common tunneling method):
index=dns sourcetype="stream:dns" query_type="TXT"
| stats count AS txt_queries, dc(query) AS unique_txt_domains,
values(query) AS domains by src_ip
| where txt_queries > 100
| eval suspicion = case(
txt_queries > 1000, "CRITICAL — Likely DNS tunneling",
txt_queries > 500, "HIGH — Possible DNS tunneling",
txt_queries > 100, "MEDIUM — Unusual TXT volume"
)
| sort - txt_queries
| table src_ip, txt_queries, unique_txt_domains, suspicionStep 4: Detect Known DNS Tunneling Tools
Search for signatures of common DNS tunneling tools:
index=dns sourcetype="stream:dns"
| eval query_lower = lower(query)
| where (
match(query_lower, "\.dnscat\.") OR
match(query_lower, "\.dns2tcp\.") OR
match(query_lower, "\.iodine\.") OR
match(query_lower, "\.dnscapy\.") OR
match(query_lower, "\.cobalt.*\.beacon") OR
query_type="NULL" OR
(query_type="TXT" AND len(query) > 100)
)
| stats count by src_ip, query, query_type
| sort - countDetect DNS over HTTPS (DoH) bypassing local DNS:
index=proxy OR index=firewall
dest IN ("1.1.1.1", "1.0.0.1", "8.8.8.8", "8.8.4.4",
"9.9.9.9", "149.112.112.112", "208.67.222.222")
dest_port=443
| stats sum(bytes_out) AS total_bytes, count AS connections by src_ip, dest
| where connections > 100 OR total_bytes > 10485760
| eval alert = "Possible DoH bypass — DNS queries sent over HTTPS to public resolver"
| sort - total_bytesStep 5: Correlate DNS Findings with Endpoint Data
Cross-reference suspicious DNS with process data:
index=dns src_ip="192.168.1.105" query="*.evil-tunnel.com" earliest=-24h
| stats count AS dns_queries, earliest(_time) AS first_query, latest(_time) AS last_query
by src_ip, query
| join src_ip [
search index=sysmon EventCode=3 DestinationPort=53 Computer="WORKSTATION-042"
| stats count AS connections, values(Image) AS processes by SourceIp
| rename SourceIp AS src_ip
]
| table src_ip, query, dns_queries, first_query, last_query, processesStep 6: Calculate Data Exfiltration Volume Estimate
Estimate data volume encoded in DNS queries:
index=dns src_ip="192.168.1.105" query="*.evil-tunnel.com" earliest=-24h
| eval domain_parts = split(query, ".")
| eval encoded_data = mvindex(domain_parts, 0)
| eval encoded_bytes = len(encoded_data)
| eval decoded_bytes = encoded_bytes * 0.75 -- Base64 decoding factor
| stats sum(decoded_bytes) AS total_bytes_estimated, count AS total_queries,
earliest(_time) AS first_seen, latest(_time) AS last_seen
| eval estimated_kb = round(total_bytes_estimated / 1024, 1)
| eval estimated_mb = round(total_bytes_estimated / 1048576, 2)
| eval duration_hours = round((last_seen - first_seen) / 3600, 1)
| eval rate_kbps = round(estimated_kb / (duration_hours * 3600) * 8, 2)
| table total_queries, estimated_mb, duration_hours, rate_kbps, first_seen, last_seenKey Concepts
| Term | Definition |
|---|---|
| DNS Tunneling | Technique encoding data within DNS queries/responses to exfiltrate data or establish C2 channels through DNS |
| DGA | Domain Generation Algorithm — malware technique generating pseudo-random domain names for C2 resilience |
| Shannon Entropy | Mathematical measure of randomness in a string — high entropy (>3.5) in domain names indicates DGA or tunneling |
| TXT Record Abuse | Using DNS TXT records (designed for text data) as a high-bandwidth channel for data tunneling |
| DNS over HTTPS (DoH) | DNS queries encrypted over HTTPS (port 443), bypassing traditional DNS monitoring |
| Passive DNS | Historical record of DNS resolutions showing which IPs a domain resolved to over time |
Tools & Systems
- Splunk Stream: Network traffic capture add-on providing parsed DNS query data for SIEM analysis
- Zeek (Bro): Network security monitor generating detailed DNS transaction logs for analysis
- Cisco Umbrella (OpenDNS): Cloud DNS security platform blocking malicious domains and logging query data
- Infoblox DNS Firewall: DNS-layer security providing RPZ-based blocking and detailed query logging
- Farsight DNSDB: Passive DNS database for historical domain resolution lookups and infrastructure mapping
Common Scenarios
- Cobalt Strike DNS Beacon: Detect periodic TXT queries with encoded payloads to C2 domain
- Data Exfiltration: Large volumes of unique subdomain queries encoding stolen data in Base64/hex
- DGA Malware: Detect DNS queries to algorithmically generated domains (high entropy, no web content)
- DNS-over-HTTPS Bypass: Employee using DoH to bypass corporate DNS filtering and monitoring
- Slow Drip Exfiltration: Low-volume DNS tunneling staying below threshold alerts (requires baseline comparison)
Output Format
DNS EXFILTRATION ANALYSIS — WORKSTATION-042
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Period: 2024-03-14 to 2024-03-15
Source: 192.168.1.105 (WORKSTATION-042, Finance Dept)
Findings:
[CRITICAL] DNS tunneling detected to evil-tunnel[.]com
Query Volume: 12,847 queries in 18 hours
Avg Subdomain Len: 63 characters (normal: <20)
Avg Entropy: 3.82 (threshold: 3.5)
Query Types: TXT (89%), A (11%)
Estimated Data: ~4.7 MB exfiltrated via DNS
Rate: 0.58 kbps (slow drip pattern)
[HIGH] DGA-like domains resolved
Unique DGA Domains: 247 domains resolved
Pattern: 15-char random alphanumeric.xyz TLD
Entropy Range: 3.6 - 4.1
Process Attribution:
Process: svchost_update.exe (masquerading — not legitimate svchost)
PID: 4892
Parent: explorer.exe
Hash: SHA256: a1b2c3d4... (VT: 34/72 malicious — Cobalt Strike beacon)
Containment:
[DONE] Host isolated via EDR
[DONE] Domain evil-tunnel[.]com added to DNS sinkhole
[DONE] Incident IR-2024-0448 createdReferences 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: DNS Exfiltration Detection Tools
Shannon Entropy Calculation
Python Implementation
import math
from collections import Counter
def shannon_entropy(text):
counter = Counter(text.lower())
length = len(text)
return -sum((c/length) * math.log2(c/length) for c in counter.values())Threshold Values
| Entropy | Classification |
|---|---|
| < 2.5 | Normal domain (e.g., "google") |
| 2.5 - 3.5 | Borderline (monitor) |
| > 3.5 | Suspicious (likely DGA/tunneling) |
| > 4.0 | High confidence malicious |
Splunk DNS Queries
Tunneling Detection
index=dns sourcetype="stream:dns"
| eval subdomain_len=len(mvindex(split(query,"."),0))
| where subdomain_len > 50
| stats count by registered_domain, src_ipDGA Detection
index=dns
| eval sld=mvindex(split(query,"."), -2)
| where len(sld) > 12
| stats count, dc(query) AS unique by src_ipVolume Anomaly
index=dns earliest=-24h
| bin _time span=1h
| stats count AS queries by src_ip, _time
| eventstats avg(queries) AS avg_q, stdev(queries) AS stdev_q by src_ip
| eval z_score=(queries - avg_q) / stdev_q
| where z_score > 3TXT Record Abuse
index=dns query_type="TXT"
| stats count AS txt_queries by src_ip
| where txt_queries > 100Zeek DNS Log Format
Log Fields (dns.log)
| Column | Field | Description |
|---|---|---|
| 0 | ts | Timestamp |
| 2 | id.orig_h | Source IP |
| 4 | id.resp_h | DNS server IP |
| 9 | query | Query domain name |
| 13 | qtype_name | Query type (A, TXT, CNAME) |
| 15 | rcode_name | Response code |
| 21 | answers | Response answers |
Zeek CLI Analysis
cat dns.log | zeek-cut query qtype_name id.orig_h | sort | uniq -c | sort -rnDNS Tunneling Tools (Detection Signatures)
| Tool | DNS Pattern |
|---|---|
| iodine | *.pirate.sea (TXT/NULL records) |
| dnscat2 | *.dnscat. prefix in queries |
| dns2tcp | *.dns2tcp. pattern |
| Cobalt Strike DNS | Periodic TXT queries with encoded payloads |
Passive DNS Lookup APIs
Farsight DNSDB
curl -H "X-API-Key: $KEY" \
"https://api.dnsdb.info/dnsdb/v2/lookup/rrset/name/evil.com/A"VirusTotal Domain Resolutions
curl -H "x-apikey: $KEY" \
"https://www.virustotal.com/api/v3/domains/evil.com/resolutions"Cisco Umbrella (OpenDNS) Investigate API
Domain Categorization
curl -H "Authorization: Bearer $TOKEN" \
"https://investigate.api.umbrella.com/domains/categorization/evil.com"Security Information
curl -H "Authorization: Bearer $TOKEN" \
"https://investigate.api.umbrella.com/security/name/evil.com"Scripts 1
agent.py8.5 KB
#!/usr/bin/env python3
"""DNS exfiltration detection agent using entropy analysis and query pattern detection."""
import math
from collections import Counter, defaultdict
def shannon_entropy(text):
"""Calculate Shannon entropy of a string."""
if not text:
return 0.0
counter = Counter(text.lower())
length = len(text)
entropy = -sum(
(count / length) * math.log2(count / length)
for count in counter.values()
)
return round(entropy, 4)
def extract_subdomain(fqdn):
"""Extract the subdomain portion from a fully qualified domain name."""
parts = fqdn.rstrip(".").split(".")
if len(parts) > 2:
return ".".join(parts[:-2])
return ""
def extract_registered_domain(fqdn):
"""Extract the registered domain (SLD + TLD) from an FQDN."""
parts = fqdn.rstrip(".").split(".")
if len(parts) >= 2:
return ".".join(parts[-2:])
return fqdn
def detect_tunneling(dns_records, subdomain_len_threshold=50, min_queries=20):
"""Detect DNS tunneling based on subdomain length anomalies."""
domain_stats = defaultdict(lambda: {"queries": 0, "unique_queries": set(),
"subdomain_lengths": [], "sources": set()})
for record in dns_records:
query = record.get("query", "")
src = record.get("src_ip", "unknown")
subdomain = extract_subdomain(query)
reg_domain = extract_registered_domain(query)
if len(subdomain) > subdomain_len_threshold:
stats = domain_stats[reg_domain]
stats["queries"] += 1
stats["unique_queries"].add(query)
stats["subdomain_lengths"].append(len(subdomain))
stats["sources"].add(src)
alerts = []
for domain, stats in domain_stats.items():
if stats["queries"] >= min_queries:
avg_len = sum(stats["subdomain_lengths"]) / len(stats["subdomain_lengths"])
max_len = max(stats["subdomain_lengths"])
alerts.append({
"domain": domain,
"queries": stats["queries"],
"unique_queries": len(stats["unique_queries"]),
"avg_subdomain_length": round(avg_len, 1),
"max_subdomain_length": max_len,
"sources": list(stats["sources"]),
"verdict": "CRITICAL - Likely DNS tunneling",
})
return sorted(alerts, key=lambda x: x["avg_subdomain_length"], reverse=True)
def detect_dga(dns_records, entropy_threshold=3.5, min_sld_length=12):
"""Detect Domain Generation Algorithm queries using entropy scoring."""
suspicious = defaultdict(lambda: {"count": 0, "sources": set(), "entropies": []})
for record in dns_records:
query = record.get("query", "").rstrip(".")
src = record.get("src_ip", "unknown")
parts = query.split(".")
if len(parts) < 2:
continue
sld = parts[-2]
if len(sld) < min_sld_length:
continue
ent = shannon_entropy(sld)
if ent > entropy_threshold:
suspicious[query]["count"] += 1
suspicious[query]["sources"].add(src)
suspicious[query]["entropies"].append(ent)
alerts = []
for domain, data in suspicious.items():
avg_entropy = sum(data["entropies"]) / len(data["entropies"])
alerts.append({
"domain": domain,
"queries": data["count"],
"avg_entropy": round(avg_entropy, 4),
"sources": list(data["sources"]),
"verdict": "HIGH - Possible DGA domain",
})
return sorted(alerts, key=lambda x: x["avg_entropy"], reverse=True)
def detect_volume_anomaly(dns_records, z_score_threshold=3.0):
"""Detect hosts with anomalously high DNS query volumes."""
host_counts = defaultdict(int)
for record in dns_records:
src = record.get("src_ip", "unknown")
host_counts[src] += 1
if not host_counts:
return []
values = list(host_counts.values())
mean_q = sum(values) / len(values)
if len(values) < 2:
return []
variance = sum((x - mean_q) ** 2 for x in values) / (len(values) - 1)
stdev_q = variance ** 0.5
if stdev_q == 0:
return []
anomalies = []
for host, count in host_counts.items():
z = (count - mean_q) / stdev_q
if z > z_score_threshold:
anomalies.append({
"src_ip": host,
"queries": count,
"z_score": round(z, 2),
"mean": round(mean_q, 1),
"verdict": "HIGH - Anomalous query volume",
})
return sorted(anomalies, key=lambda x: x["z_score"], reverse=True)
def detect_txt_abuse(dns_records, threshold=100):
"""Detect excessive TXT record queries (common tunneling method)."""
txt_counts = defaultdict(lambda: {"count": 0, "unique_domains": set()})
for record in dns_records:
qtype = str(record.get("query_type", "")).upper()
if qtype in ("TXT", "16"):
src = record.get("src_ip", "unknown")
txt_counts[src]["count"] += 1
txt_counts[src]["unique_domains"].add(record.get("query", ""))
alerts = []
for src, data in txt_counts.items():
if data["count"] > threshold:
level = "CRITICAL" if data["count"] > 1000 else "HIGH" if data["count"] > 500 else "MEDIUM"
alerts.append({
"src_ip": src,
"txt_queries": data["count"],
"unique_domains": len(data["unique_domains"]),
"verdict": f"{level} - Possible DNS tunneling via TXT records",
})
return sorted(alerts, key=lambda x: x["txt_queries"], reverse=True)
def estimate_exfil_volume(dns_records, target_domain):
"""Estimate data volume encoded in DNS queries to a specific domain."""
total_encoded_bytes = 0
query_count = 0
for record in dns_records:
query = record.get("query", "")
if target_domain in query:
subdomain = extract_subdomain(query)
total_encoded_bytes += len(subdomain)
query_count += 1
decoded_bytes = int(total_encoded_bytes * 0.75) # Base64 decode factor
return {
"target_domain": target_domain,
"total_queries": query_count,
"encoded_bytes": total_encoded_bytes,
"estimated_decoded_bytes": decoded_bytes,
"estimated_kb": round(decoded_bytes / 1024, 1),
"estimated_mb": round(decoded_bytes / (1024 * 1024), 3),
}
def parse_zeek_dns_log(log_path):
"""Parse a Zeek dns.log file into structured records."""
records = []
with open(log_path, "r") as f:
for line in f:
if line.startswith("#"):
continue
parts = line.strip().split("\t")
if len(parts) >= 10:
records.append({
"timestamp": parts[0],
"src_ip": parts[2],
"src_port": parts[3],
"dst_ip": parts[4],
"query": parts[9] if len(parts) > 9 else "",
"query_type": parts[13] if len(parts) > 13 else "",
})
return records
if __name__ == "__main__":
print("=" * 60)
print("DNS Exfiltration Detection Agent")
print("Tunneling, DGA, volume anomaly, and TXT abuse detection")
print("=" * 60)
# Demo with synthetic DNS records
demo_records = [
{"query": f"{'a' * 60}.evil-tunnel.com", "src_ip": "192.168.1.105",
"query_type": "TXT"} for _ in range(50)
] + [
{"query": "x8kj2m9p4qw7nz3.xyz", "src_ip": "192.168.1.110",
"query_type": "A"} for _ in range(5)
] + [
{"query": "google.com", "src_ip": "192.168.1.50", "query_type": "A"}
for _ in range(10)
]
print("\n--- DNS Tunneling Detection ---")
tunneling = detect_tunneling(demo_records, subdomain_len_threshold=30, min_queries=10)
for t in tunneling:
print(f"[!] {t['domain']}: {t['queries']} queries, "
f"avg subdomain len={t['avg_subdomain_length']}")
print("\n--- DGA Detection ---")
dga = detect_dga(demo_records, entropy_threshold=3.0, min_sld_length=10)
for d in dga[:5]:
print(f"[!] {d['domain']}: entropy={d['avg_entropy']}")
print("\n--- TXT Record Abuse ---")
txt = detect_txt_abuse(demo_records, threshold=10)
for t in txt:
print(f"[!] {t['src_ip']}: {t['txt_queries']} TXT queries")
print("\n--- Entropy Examples ---")
examples = ["google", "x8kj2m9p4qw7n", "aGVsbG8gd29ybGQ"]
for ex in examples:
print(f" '{ex}' -> entropy={shannon_entropy(ex)}")