[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71FEMHN0JYMST0BM0ZKX2PJ":3,"skill-same:01M71FEMHN0JYMST0BM0ZKX2PJ":54},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":22,"linkOnly":23,"markdownContent":24,"markdownBody":25,"frontMatter":26,"refName":27,"commitSha":28,"skillDirectory":29,"forksCount":14,"repositoryHtmlUrl":30,"repositoryHomepageUrl":6,"ownerHtmlUrl":31,"ownerBlogUrl":6,"ownerDescription":32,"updatedAt":33,"install":34,"resources":47},"01M71FEMHN0JYMST0BM0ZKX2PJ","net-layer7-dns-dnsexfil",null,"Net Layer7 Dns Dnsexfil.","DCx7C5\u002Fai-marketplace","DCx7C5","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F42419559?v=4","01M71C01G2AZV3NTNGWAWSHJPP","DC","01M71C01GQW6G50S2BYMS7PP3E",0,"MIT","https:\u002F\u002Fgithub.com\u002FDCx7C5\u002Fai-marketplace\u002Fblob\u002Fmain\u002Fskills\u002Fnet-layer7-dns-dnsexfil\u002FSKILL.md",[],[19,20,21],"net layer7 dns dnsexfil","dns","cybersecurity","2026-11-10 10:12:30",false,"---\nname: net-layer7-dns-dnsexfil\ndescription: \"Net Layer7 Dns Dnsexfil.\"\ndomain: cybersecurity\n---\n\n|\n| iodine | IP-over-DNS | Base32\u002FBase64\u002FRaw | Medium |\n| dnscat2 | TCP-over-DNS | Hex encoding | Medium |\n| dns2tcp | TCP-over-DNS | Base64 | Medium |\n| DNSExfiltrator | Custom | Base64 | Low |\n| Cobalt Strike DNS | C2 over DNS | Custom encoding | High |\n\n## Workflow\n\n### Step 1: Capture DNS Traffic\n\n**Using Zeek:**\n```bash\n# Live capture\nzeek -i eth0 -C base\u002Fprotocols\u002Fdns\n\n# Offline PCAP analysis\nzeek -r traffic.pcap base\u002Fprotocols\u002Fdns\n\n# Output: dns.log with query, qtype, answers, TTL\n```\n\n**Using tcpdump:**\n```bash\n# Capture all DNS traffic\ntcpdump -i eth0 -w dns_capture.pcap port 53\n\n# Capture with size filter (large DNS packets)\ntcpdump -i eth0 -w large_dns.pcap 'port 53 and greater 512'\n```\n\n**Using Suricata:**\n```yaml\n# In suricata.yaml, enable DNS logging\noutputs:\n  - eve-log:\n      types:\n        - dns:\n            query: yes\n            answer: yes\n            formats: [detailed]\n```\n\n### Step 2: Analyze Query Characteristics\n\nPython script for DNS exfiltration detection:\n\n```python\n#!\u002Fusr\u002Fbin\u002Fenv python3\n\"\"\"DNS Exfiltration Detector - Analyzes DNS logs for tunneling indicators.\"\"\"\n\nimport json\nimport math\nimport re\nimport sys\nfrom collections import defaultdict\nfrom datetime import datetime, timedelta\n\nimport pandas as pd\n\ndef calculate_entropy(domain: str) -> float:\n    \"\"\"Calculate Shannon entropy of a string.\"\"\"\n    if not domain:\n        return 0.0\n    freq = defaultdict(int)\n    for char in domain:\n        freq[char] += 1\n    length = len(domain)\n    entropy = -sum(\n        (count \u002F length) * math.log2(count \u002F length)\n        for count in freq.values()\n    )\n    return entropy\n\ndef extract_subdomain(query: str) -> str:\n    \"\"\"Extract subdomain portion from FQDN.\"\"\"\n    parts = query.rstrip('.').split('.')\n    if len(parts) > 2:\n        return '.'.join(parts[:-2])\n    return ''\n\ndef get_base_domain(query: str) -> str:\n    \"\"\"Extract registered domain from FQDN.\"\"\"\n    parts = query.rstrip('.').split('.')\n    if len(parts) >= 2:\n        return '.'.join(parts[-2:])\n    return query\n\ndef is_base64_like(s: str) -> bool:\n    \"\"\"Check if string resembles base64 encoding.\"\"\"\n    b64_chars = set('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+\u002F=')\n    if len(s) \u003C 10:\n        return False\n    char_ratio = sum(1 for c in s if c in b64_chars) \u002F len(s)\n    return char_ratio > 0.9 and calculate_entropy(s) > 4.0\n\ndef is_hex_encoded(s: str) -> bool:\n    \"\"\"Check if string appears hex-encoded.\"\"\"\n    hex_chars = set('0123456789abcdefABCDEF')\n    if len(s) \u003C 16:\n        return False\n    clean = s.replace('.', '').replace('-', '')\n    return all(c in hex_chars for c in clean) and len(clean) % 2 == 0\n\nclass DNSExfiltrationDetector:\n    def __init__(self):\n        self.domain_stats = defaultdict(lambda: {\n            'query_count': 0,\n            'unique_subdomains': set(),\n            'total_subdomain_length': 0,\n            'entropy_sum': 0.0,\n            'query_types': defaultdict(int),\n            'source_ips': set(),\n            'first_seen': None,\n            'last_seen': None,\n            'txt_response_sizes': [],\n        })\n\n        # Detection thresholds\n        self.thresholds = {\n            'min_query_count': 50,\n            'min_unique_subdomains': 30,\n            'avg_subdomain_length': 30,\n            'avg_entropy': 3.8,\n            'unique_ratio': 0.7,\n            'txt_query_ratio': 0.3,\n            'max_label_length': 63,\n            'max_subdomain_labels': 5,\n        }\n\n    def process_query(self, timestamp, src_ip, query, qtype, response_size=0):\n        \"\"\"Process a single DNS query and update statistics.\"\"\"\n        base_domain = get_base_domain(query)\n        subdomain = extract_subdomain(query)\n\n        stats = self.domain_stats[base_domain]\n        stats['query_count'] += 1\n        stats['unique_subdomains'].add(subdomain)\n        stats['total_subdomain_length'] += len(subdomain)\n        stats['entropy_sum'] += calculate_entropy(subdomain)\n        stats['query_types'][qtype] += 1\n        stats['source_ips'].add(src_ip)\n\n        if stats['first_seen'] is None:\n            stats['first_seen'] = timestamp\n        stats['last_seen'] = timestamp\n\n        if qtype in ('TXT', 'NULL') and response_size > 0:\n            stats['txt_response_sizes'].append(response_size)\n\n    def analyze(self):\n        \"\"\"Analyze accumulated statistics and return suspicious domains.\"\"\"\n        alerts = []\n\n        for domain, stats in self.domain_stats.items():\n            if stats['query_count'] \u003C self.thresholds['min_query_count']:\n                continue\n\n            unique_count = len(stats['unique_subdomains'])\n            avg_length = stats['total_subdomain_length'] \u002F stats['query_count']\n            avg_entropy = stats['entropy_sum'] \u002F stats['query_count']\n            unique_ratio = unique_count \u002F stats['query_count']\n\n            txt_queries = stats['query_types'].get('TXT', 0) + stats['query_types'].get('NULL', 0)\n            txt_ratio = txt_queries \u002F stats['query_count']\n\n            score = 0\n            indicators = []\n\n            if avg_length > self.thresholds['avg_subdomain_length']:\n                score += 25\n                indicators.append(f\"high_avg_subdomain_length={avg_length:.1f}\")\n\n            if avg_entropy > self.thresholds['avg_entropy']:\n                score += 25\n                indicators.append(f\"high_entropy={avg_entropy:.2f}\")\n\n            if unique_ratio > self.thresholds['unique_ratio']:\n                score += 20\n                indicators.append(f\"high_unique_ratio={unique_ratio:.2f}\")\n\n            if txt_ratio > self.thresholds['txt_query_ratio']:\n                score += 15\n                indicators.append(f\"high_txt_ratio={txt_ratio:.2f}\")\n\n            if unique_count > self.thresholds['min_unique_subdomains']:\n                score += 15\n                indicators.append(f\"unique_subdomains={unique_count}\")\n\n            # Check for encoding patterns\n            encoded_count = sum(\n                1 for sd in list(stats['unique_subdomains'])[:100]\n                if is_base64_like(sd) or is_hex_encoded(sd)\n            )\n            if encoded_count > 20:\n                score += 20\n                indicators.append(f\"encoded_subdomains={encoded_count}\")\n\n            if score >= 50:\n                duration = (stats['last_seen'] - stats['first_seen']).total_seconds() if stats['first_seen'] and stats['last_seen'] else 0\n                alerts.append({\n                    'domain': domain,\n                    'score': min(score, 100),\n                    'query_count': stats['query_count'],\n                    'unique_subdomains': unique_count,\n                    'avg_subdomain_length': round(avg_length, 1),\n                    'avg_entropy': round(avg_entropy, 2),\n                    'unique_ratio': round(unique_ratio, 2),\n                    'txt_ratio': round(txt_ratio, 2),\n                    'source_ips': list(stats['source_ips']),\n                    'duration_seconds': duration,\n                    'indicators': indicators,\n                })\n\n        return sorted(alerts, key=lambda x: x['score'], reverse=True)\n\n    def process_zeek_dns_log(self, log_path):\n        \"\"\"Process Zeek dns.log file.\"\"\"\n        with open(log_path, 'r') as f:\n            for line in f:\n                if line.startswith('#'):\n                    continue\n                fields = line.strip().split('\\t')\n                if len(fields) \u003C 22:\n                    continue\n                try:\n                    ts = datetime.fromtimestamp(float(fields[0]))\n                    src_ip = fields[2]\n                    query = fields[9]\n                    qtype = fields[11]\n                    self.process_query(ts, src_ip, query, qtype)\n                except (ValueError, IndexError):\n                    continue\n\n    def process_eve_json(self, log_path):\n        \"\"\"Process Suricata EVE JSON DNS log.\"\"\"\n        with open(log_path, 'r') as f:\n            for line in f:\n                try:\n                    event = json.loads(line)\n                    if event.get('event_type') != 'dns':\n                        continue\n                    dns = event.get('dns', {})\n                    ts = datetime.fromisoformat(event['timestamp'].replace('Z', '+00:00'))\n                    src_ip = event.get('src_ip', '')\n                    query = dns.get('rrname', '')\n                    qtype = dns.get('rrtype', '')\n                    self.process_query(ts, src_ip, query, qtype)\n                except (json.JSONDecodeError, KeyError, ValueError):\n                    continue\n\ndef main():\n    detector = DNSExfiltrationDetector()\n\n    log_file = sys.argv[1] if len(sys.argv) > 1 else '\u002Fopt\u002Fzeek\u002Flogs\u002Fcurrent\u002Fdns.log'\n\n    if log_file.endswith('.json'):\n        detector.process_eve_json(log_file)\n    else:\n        detector.process_zeek_dns_log(log_file)\n\n    alerts = detector.analyze()\n\n    if alerts:\n        print(f\"\\n{'='*80}\")\n        print(f\"DNS EXFILTRATION DETECTION RESULTS - {len(alerts)} suspicious domains found\")\n        print(f\"{'='*80}\\n\")\n\n        for alert in alerts:\n            severity = \"CRITICAL\" if alert['score'] >= 80 else \"HIGH\" if alert['score'] >= 60 else \"MEDIUM\"\n            print(f\"[{severity}] Domain: {alert['domain']}\")\n            print(f\"  Score: {alert['score']}\u002F100\")\n            print(f\"  Queries: {alert['query_count']}, Unique Subdomains: {alert['unique_subdomains']}\")\n            print(f\"  Avg Subdomain Length: {alert['avg_subdomain_length']}, Avg Entropy: {alert['avg_entropy']}\")\n            print(f\"  Source IPs: {', '.join(alert['source_ips'][:5])}\")\n            print(f\"  Indicators: {', '.join(alert['indicators'])}\")\n            print()\n    else:\n        print(\"No DNS exfiltration indicators detected.\")\n\nif __name__ == '__main__':\n    main()\n```\n\n### Step 3: Deploy Suricata Rules for DNS Exfiltration\n\n```\n# Detect long DNS queries (potential tunneling)\nalert dns $HOME_NET any -> any 53 (msg:\"DNS Exfiltration - Excessive query length\"; dns.query; content:\".\"; pcre:\"\u002F^.{60,}\u002F\"; threshold:type both,track by_src,count 20,seconds 60; classtype:bad-unknown; sid:3000001; rev:1;)\n\n# Detect high-entropy DNS subdomain\nalert dns $HOME_NET any -> any 53 (msg:\"DNS Exfiltration - High entropy subdomain\"; dns.query; pcre:\"\u002F^[a-zA-Z0-9+\\\u002F=]{30,}\\.\u002F\"; threshold:type both,track by_src,count 10,seconds 60; classtype:bad-unknown; sid:3000002; rev:1;)\n\n# Detect large TXT record responses\nalert dns any 53 -> $HOME_NET any (msg:\"DNS Exfiltration - Large TXT response\"; content:\"|00 10|\"; byte_test:2,>,400,0,relative; classtype:bad-unknown; sid:3000003; rev:1;)\n\n# Detect NULL record queries (used by iodine)\nalert dns $HOME_NET any -> any 53 (msg:\"DNS Exfiltration - NULL record query (iodine indicator)\"; content:\"|00 0a|\"; classtype:bad-unknown; sid:3000004; rev:1;)\n\n# Detect dnscat2 traffic pattern\nalert dns $HOME_NET any -> any 53 (msg:\"DNS Exfiltration - dnscat2 indicator\"; dns.query; content:\"dnscat\"; nocase; classtype:trojan-activity; sid:3000005; rev:1;)\n```\n\n### Step 4: SIEM Detection Rules\n\n**Splunk SPL query for DNS exfiltration:**\n\n```spl\nindex=dns sourcetype=zeek:dns\n| eval subdomain=mvindex(split(query,\".\"),0)\n| eval subdomain_len=len(subdomain)\n| eval label_count=mvcount(split(query,\".\"))\n| stats count as query_count,\n        dc(subdomain) as unique_subdomains,\n        avg(subdomain_len) as avg_sub_len,\n        values(src_ip) as source_ips\n        by query_domain\n| where query_count > 100 AND avg_sub_len > 30 AND unique_subdomains > 50\n| eval risk_score = case(\n    avg_sub_len > 50 AND unique_subdomains > 200, \"Critical\",\n    avg_sub_len > 40 AND unique_subdomains > 100, \"High\",\n    avg_sub_len > 30 AND unique_subdomains > 50, \"Medium\",\n    true(), \"Low\")\n| sort -query_count\n| table query_domain risk_score query_count unique_subdomains avg_sub_len source_ips\n```\n\n## Response Actions\n\n1. **Block the tunnel domain** at DNS resolver and firewall level\n2. **Isolate the source host** from the network for forensic investigation\n3. **Capture full PCAP** of the DNS traffic for evidence preservation\n4. **Identify exfiltrated data** by decoding captured DNS queries\n5. **Check for persistence** mechanisms on the compromised host\n6. **Update blocklists** with identified C2 domains and infrastructure\n\n## Best Practices\n\n- **DNS Logging** - Enable full DNS query and response logging at resolvers and network level\n- **Internal DNS Only** - Force all DNS through internal resolvers; block direct external DNS (port 53)\n- **Response Policy Zones** - Deploy RPZ feeds to block known tunneling domains\n- **Baseline First** - Establish normal DNS query patterns before setting detection thresholds\n- **TXT Record Monitoring** - Pay special attention to TXT and NULL record queries\n- **Encrypted DNS Awareness** - Monitor for DoH\u002FDoT usage that may bypass DNS inspection\n\n## References\n\n- [Splunk DNS Exfiltration Detection with Deep Learning](https:\u002F\u002Fwww.splunk.com\u002Fen_us\u002Fblog\u002Fsecurity\u002Fmachine-learning-in-security-detect-dns-data-exfiltration-using-deep-learning.html)\n- [Akamai DNS Data Exfiltration](https:\u002F\u002Fwww.akamai.com\u002Fglossary\u002Fwhat-is-dns-data-exfiltration)\n- [SANS Detecting DNS Tunneling](https:\u002F\u002Fwww.giac.org\u002Fpaper\u002Fgcia\u002F1116\u002Fdetecting-dns-tunneling\u002F108367)\n- [Fidelis DNS Tunneling Detection](https:\u002F\u002Ffidelissecurity.com\u002Fthreatgeek\u002Flearn\u002Fdns-tunneling-detection\u002F)\n","|\n| iodine | IP-over-DNS | Base32\u002FBase64\u002FRaw | Medium |\n| dnscat2 | TCP-over-DNS | Hex encoding | Medium |\n| dns2tcp | TCP-over-DNS | Base64 | Medium |\n| DNSExfiltrator | Custom | Base64 | Low |\n| Cobalt Strike DNS | C2 over DNS | Custom encoding | High |\n\n## Workflow\n\n### Step 1: Capture DNS Traffic\n\n**Using Zeek:**\n```bash\n# Live capture\nzeek -i eth0 -C base\u002Fprotocols\u002Fdns\n\n# Offline PCAP analysis\nzeek -r traffic.pcap base\u002Fprotocols\u002Fdns\n\n# Output: dns.log with query, qtype, answers, TTL\n```\n\n**Using tcpdump:**\n```bash\n# Capture all DNS traffic\ntcpdump -i eth0 -w dns_capture.pcap port 53\n\n# Capture with size filter (large DNS packets)\ntcpdump -i eth0 -w large_dns.pcap 'port 53 and greater 512'\n```\n\n**Using Suricata:**\n```yaml\n# In suricata.yaml, enable DNS logging\noutputs:\n  - eve-log:\n      types:\n        - dns:\n            query: yes\n            answer: yes\n            formats: [detailed]\n```\n\n### Step 2: Analyze Query Characteristics\n\nPython script for DNS exfiltration detection:\n\n```python\n#!\u002Fusr\u002Fbin\u002Fenv python3\n\"\"\"DNS Exfiltration Detector - Analyzes DNS logs for tunneling indicators.\"\"\"\n\nimport json\nimport math\nimport re\nimport sys\nfrom collections import defaultdict\nfrom datetime import datetime, timedelta\n\nimport pandas as pd\n\ndef calculate_entropy(domain: str) -> float:\n    \"\"\"Calculate Shannon entropy of a string.\"\"\"\n    if not domain:\n        return 0.0\n    freq = defaultdict(int)\n    for char in domain:\n        freq[char] += 1\n    length = len(domain)\n    entropy = -sum(\n        (count \u002F length) * math.log2(count \u002F length)\n        for count in freq.values()\n    )\n    return entropy\n\ndef extract_subdomain(query: str) -> str:\n    \"\"\"Extract subdomain portion from FQDN.\"\"\"\n    parts = query.rstrip('.').split('.')\n    if len(parts) > 2:\n        return '.'.join(parts[:-2])\n    return ''\n\ndef get_base_domain(query: str) -> str:\n    \"\"\"Extract registered domain from FQDN.\"\"\"\n    parts = query.rstrip('.').split('.')\n    if len(parts) >= 2:\n        return '.'.join(parts[-2:])\n    return query\n\ndef is_base64_like(s: str) -> bool:\n    \"\"\"Check if string resembles base64 encoding.\"\"\"\n    b64_chars = set('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+\u002F=')\n    if len(s) \u003C 10:\n        return False\n    char_ratio = sum(1 for c in s if c in b64_chars) \u002F len(s)\n    return char_ratio > 0.9 and calculate_entropy(s) > 4.0\n\ndef is_hex_encoded(s: str) -> bool:\n    \"\"\"Check if string appears hex-encoded.\"\"\"\n    hex_chars = set('0123456789abcdefABCDEF')\n    if len(s) \u003C 16:\n        return False\n    clean = s.replace('.', '').replace('-', '')\n    return all(c in hex_chars for c in clean) and len(clean) % 2 == 0\n\nclass DNSExfiltrationDetector:\n    def __init__(self):\n        self.domain_stats = defaultdict(lambda: {\n            'query_count': 0,\n            'unique_subdomains': set(),\n            'total_subdomain_length': 0,\n            'entropy_sum': 0.0,\n            'query_types': defaultdict(int),\n            'source_ips': set(),\n            'first_seen': None,\n            'last_seen': None,\n            'txt_response_sizes': [],\n        })\n\n        # Detection thresholds\n        self.thresholds = {\n            'min_query_count': 50,\n            'min_unique_subdomains': 30,\n            'avg_subdomain_length': 30,\n            'avg_entropy': 3.8,\n            'unique_ratio': 0.7,\n            'txt_query_ratio': 0.3,\n            'max_label_length': 63,\n            'max_subdomain_labels': 5,\n        }\n\n    def process_query(self, timestamp, src_ip, query, qtype, response_size=0):\n        \"\"\"Process a single DNS query and update statistics.\"\"\"\n        base_domain = get_base_domain(query)\n        subdomain = extract_subdomain(query)\n\n        stats = self.domain_stats[base_domain]\n        stats['query_count'] += 1\n        stats['unique_subdomains'].add(subdomain)\n        stats['total_subdomain_length'] += len(subdomain)\n        stats['entropy_sum'] += calculate_entropy(subdomain)\n        stats['query_types'][qtype] += 1\n        stats['source_ips'].add(src_ip)\n\n        if stats['first_seen'] is None:\n            stats['first_seen'] = timestamp\n        stats['last_seen'] = timestamp\n\n        if qtype in ('TXT', 'NULL') and response_size > 0:\n            stats['txt_response_sizes'].append(response_size)\n\n    def analyze(self):\n        \"\"\"Analyze accumulated statistics and return suspicious domains.\"\"\"\n        alerts = []\n\n        for domain, stats in self.domain_stats.items():\n            if stats['query_count'] \u003C self.thresholds['min_query_count']:\n                continue\n\n            unique_count = len(stats['unique_subdomains'])\n            avg_length = stats['total_subdomain_length'] \u002F stats['query_count']\n            avg_entropy = stats['entropy_sum'] \u002F stats['query_count']\n            unique_ratio = unique_count \u002F stats['query_count']\n\n            txt_queries = stats['query_types'].get('TXT', 0) + stats['query_types'].get('NULL', 0)\n            txt_ratio = txt_queries \u002F stats['query_count']\n\n            score = 0\n            indicators = []\n\n            if avg_length > self.thresholds['avg_subdomain_length']:\n                score += 25\n                indicators.append(f\"high_avg_subdomain_length={avg_length:.1f}\")\n\n            if avg_entropy > self.thresholds['avg_entropy']:\n                score += 25\n                indicators.append(f\"high_entropy={avg_entropy:.2f}\")\n\n            if unique_ratio > self.thresholds['unique_ratio']:\n                score += 20\n                indicators.append(f\"high_unique_ratio={unique_ratio:.2f}\")\n\n            if txt_ratio > self.thresholds['txt_query_ratio']:\n                score += 15\n                indicators.append(f\"high_txt_ratio={txt_ratio:.2f}\")\n\n            if unique_count > self.thresholds['min_unique_subdomains']:\n                score += 15\n                indicators.append(f\"unique_subdomains={unique_count}\")\n\n            # Check for encoding patterns\n            encoded_count = sum(\n                1 for sd in list(stats['unique_subdomains'])[:100]\n                if is_base64_like(sd) or is_hex_encoded(sd)\n            )\n            if encoded_count > 20:\n                score += 20\n                indicators.append(f\"encoded_subdomains={encoded_count}\")\n\n            if score >= 50:\n                duration = (stats['last_seen'] - stats['first_seen']).total_seconds() if stats['first_seen'] and stats['last_seen'] else 0\n                alerts.append({\n                    'domain': domain,\n                    'score': min(score, 100),\n                    'query_count': stats['query_count'],\n                    'unique_subdomains': unique_count,\n                    'avg_subdomain_length': round(avg_length, 1),\n                    'avg_entropy': round(avg_entropy, 2),\n                    'unique_ratio': round(unique_ratio, 2),\n                    'txt_ratio': round(txt_ratio, 2),\n                    'source_ips': list(stats['source_ips']),\n                    'duration_seconds': duration,\n                    'indicators': indicators,\n                })\n\n        return sorted(alerts, key=lambda x: x['score'], reverse=True)\n\n    def process_zeek_dns_log(self, log_path):\n        \"\"\"Process Zeek dns.log file.\"\"\"\n        with open(log_path, 'r') as f:\n            for line in f:\n                if line.startswith('#'):\n                    continue\n                fields = line.strip().split('\\t')\n                if len(fields) \u003C 22:\n                    continue\n                try:\n                    ts = datetime.fromtimestamp(float(fields[0]))\n                    src_ip = fields[2]\n                    query = fields[9]\n                    qtype = fields[11]\n                    self.process_query(ts, src_ip, query, qtype)\n                except (ValueError, IndexError):\n                    continue\n\n    def process_eve_json(self, log_path):\n        \"\"\"Process Suricata EVE JSON DNS log.\"\"\"\n        with open(log_path, 'r') as f:\n            for line in f:\n                try:\n                    event = json.loads(line)\n                    if event.get('event_type') != 'dns':\n                        continue\n                    dns = event.get('dns', {})\n                    ts = datetime.fromisoformat(event['timestamp'].replace('Z', '+00:00'))\n                    src_ip = event.get('src_ip', '')\n                    query = dns.get('rrname', '')\n                    qtype = dns.get('rrtype', '')\n                    self.process_query(ts, src_ip, query, qtype)\n                except (json.JSONDecodeError, KeyError, ValueError):\n                    continue\n\ndef main():\n    detector = DNSExfiltrationDetector()\n\n    log_file = sys.argv[1] if len(sys.argv) > 1 else '\u002Fopt\u002Fzeek\u002Flogs\u002Fcurrent\u002Fdns.log'\n\n    if log_file.endswith('.json'):\n        detector.process_eve_json(log_file)\n    else:\n        detector.process_zeek_dns_log(log_file)\n\n    alerts = detector.analyze()\n\n    if alerts:\n        print(f\"\\n{'='*80}\")\n        print(f\"DNS EXFILTRATION DETECTION RESULTS - {len(alerts)} suspicious domains found\")\n        print(f\"{'='*80}\\n\")\n\n        for alert in alerts:\n            severity = \"CRITICAL\" if alert['score'] >= 80 else \"HIGH\" if alert['score'] >= 60 else \"MEDIUM\"\n            print(f\"[{severity}] Domain: {alert['domain']}\")\n            print(f\"  Score: {alert['score']}\u002F100\")\n            print(f\"  Queries: {alert['query_count']}, Unique Subdomains: {alert['unique_subdomains']}\")\n            print(f\"  Avg Subdomain Length: {alert['avg_subdomain_length']}, Avg Entropy: {alert['avg_entropy']}\")\n            print(f\"  Source IPs: {', '.join(alert['source_ips'][:5])}\")\n            print(f\"  Indicators: {', '.join(alert['indicators'])}\")\n            print()\n    else:\n        print(\"No DNS exfiltration indicators detected.\")\n\nif __name__ == '__main__':\n    main()\n```\n\n### Step 3: Deploy Suricata Rules for DNS Exfiltration\n\n```\n# Detect long DNS queries (potential tunneling)\nalert dns $HOME_NET any -> any 53 (msg:\"DNS Exfiltration - Excessive query length\"; dns.query; content:\".\"; pcre:\"\u002F^.{60,}\u002F\"; threshold:type both,track by_src,count 20,seconds 60; classtype:bad-unknown; sid:3000001; rev:1;)\n\n# Detect high-entropy DNS subdomain\nalert dns $HOME_NET any -> any 53 (msg:\"DNS Exfiltration - High entropy subdomain\"; dns.query; pcre:\"\u002F^[a-zA-Z0-9+\\\u002F=]{30,}\\.\u002F\"; threshold:type both,track by_src,count 10,seconds 60; classtype:bad-unknown; sid:3000002; rev:1;)\n\n# Detect large TXT record responses\nalert dns any 53 -> $HOME_NET any (msg:\"DNS Exfiltration - Large TXT response\"; content:\"|00 10|\"; byte_test:2,>,400,0,relative; classtype:bad-unknown; sid:3000003; rev:1;)\n\n# Detect NULL record queries (used by iodine)\nalert dns $HOME_NET any -> any 53 (msg:\"DNS Exfiltration - NULL record query (iodine indicator)\"; content:\"|00 0a|\"; classtype:bad-unknown; sid:3000004; rev:1;)\n\n# Detect dnscat2 traffic pattern\nalert dns $HOME_NET any -> any 53 (msg:\"DNS Exfiltration - dnscat2 indicator\"; dns.query; content:\"dnscat\"; nocase; classtype:trojan-activity; sid:3000005; rev:1;)\n```\n\n### Step 4: SIEM Detection Rules\n\n**Splunk SPL query for DNS exfiltration:**\n\n```spl\nindex=dns sourcetype=zeek:dns\n| eval subdomain=mvindex(split(query,\".\"),0)\n| eval subdomain_len=len(subdomain)\n| eval label_count=mvcount(split(query,\".\"))\n| stats count as query_count,\n        dc(subdomain) as unique_subdomains,\n        avg(subdomain_len) as avg_sub_len,\n        values(src_ip) as source_ips\n        by query_domain\n| where query_count > 100 AND avg_sub_len > 30 AND unique_subdomains > 50\n| eval risk_score = case(\n    avg_sub_len > 50 AND unique_subdomains > 200, \"Critical\",\n    avg_sub_len > 40 AND unique_subdomains > 100, \"High\",\n    avg_sub_len > 30 AND unique_subdomains > 50, \"Medium\",\n    true(), \"Low\")\n| sort -query_count\n| table query_domain risk_score query_count unique_subdomains avg_sub_len source_ips\n```\n\n## Response Actions\n\n1. **Block the tunnel domain** at DNS resolver and firewall level\n2. **Isolate the source host** from the network for forensic investigation\n3. **Capture full PCAP** of the DNS traffic for evidence preservation\n4. **Identify exfiltrated data** by decoding captured DNS queries\n5. **Check for persistence** mechanisms on the compromised host\n6. **Update blocklists** with identified C2 domains and infrastructure\n\n## Best Practices\n\n- **DNS Logging** - Enable full DNS query and response logging at resolvers and network level\n- **Internal DNS Only** - Force all DNS through internal resolvers; block direct external DNS (port 53)\n- **Response Policy Zones** - Deploy RPZ feeds to block known tunneling domains\n- **Baseline First** - Establish normal DNS query patterns before setting detection thresholds\n- **TXT Record Monitoring** - Pay special attention to TXT and NULL record queries\n- **Encrypted DNS Awareness** - Monitor for DoH\u002FDoT usage that may bypass DNS inspection\n\n## References\n\n- [Splunk DNS Exfiltration Detection with Deep Learning](https:\u002F\u002Fwww.splunk.com\u002Fen_us\u002Fblog\u002Fsecurity\u002Fmachine-learning-in-security-detect-dns-data-exfiltration-using-deep-learning.html)\n- [Akamai DNS Data Exfiltration](https:\u002F\u002Fwww.akamai.com\u002Fglossary\u002Fwhat-is-dns-data-exfiltration)\n- [SANS Detecting DNS Tunneling](https:\u002F\u002Fwww.giac.org\u002Fpaper\u002Fgcia\u002F1116\u002Fdetecting-dns-tunneling\u002F108367)\n- [Fidelis DNS Tunneling Detection](https:\u002F\u002Ffidelissecurity.com\u002Fthreatgeek\u002Flearn\u002Fdns-tunneling-detection\u002F)\n",{"name":5,"domain":21,"description":7},"main","a91f1293287ffa9f4f1b8f486cd6e835a1568c84","skills\u002Fnet-layer7-dns-dnsexfil","https:\u002F\u002Fgithub.com\u002FDCx7C5\u002Fai-marketplace","https:\u002F\u002Fgithub.com\u002FDCx7C5","#Python","2026-09-01 11:21:00",{"source":8,"skillName":5,"commands":35,"prompt":45,"zipAvailable":46},[36,39,42],{"tool":37,"command":38},"npx","npx skills add DCx7C5\u002Fai-marketplace --skill net-layer7-dns-dnsexfil",{"tool":40,"command":41},"bunx","bunx skills add DCx7C5\u002Fai-marketplace --skill net-layer7-dns-dnsexfil",{"tool":43,"command":44},"pnpm","pnpm dlx skills add DCx7C5\u002Fai-marketplace --skill net-layer7-dns-dnsexfil","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FDCx7C5\u002Fai-marketplace\u002Fblob\u002Fmain\u002Fskills\u002Fnet-layer7-dns-dnsexfil\u002FSKILL.md",true,[48],{"path":49,"resourceType":50,"resourceTypeDesc":51,"sizeBytes":52,"sourceHtmlUrl":53},"index.json","REFERENCE","参考文档",492,"https:\u002F\u002Fgithub.com\u002FDCx7C5\u002Fai-marketplace\u002Fblob\u002Fmain\u002Fskills\u002Fnet-layer7-dns-dnsexfil\u002Findex.json",[55,80,103,125,147,163],{"skillNo":56,"name":57,"nameZh":6,"description":58,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":59,"categories":60,"keywords":61,"publishedAt":22},"01M71HAPZ01KQ30RKTB0FFSQ05","services-wmi-persistence-detect","Detect WMI event subscription persistence by enumerating ActiveScriptEventConsumer, CommandLineEventConsumer, and permanent subscriptions in the WMI repository.","https:\u002F\u002Fgithub.com\u002FDCx7C5\u002Fai-marketplace\u002Fblob\u002Fmain\u002Fskills\u002Fwindows\u002Fservices\u002Fwmi\u002Fpersistence\u002Fdetect\u002FSKILL.md",[],[62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,21,79],"services","detect","services wmi persistence detect","wmi","persistence","de","cm","process","activescript","event","subscriptions","commandline-consumer","repository","subscription","event-subscription","t1546.003","forensics","process-forensics",{"skillNo":81,"name":82,"nameZh":6,"description":83,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":84,"categories":85,"keywords":86,"publishedAt":22},"01M71HAK6JTTTS229T17WJMMCG","services-wmi-exec-exploit","Perform lateral movement across Windows networks using WMI-based remote execution techniques including Impacket wmiexec.py, CrackMapExec, and native WMI commands for stealthy post-exploitation during red team engagements.","https:\u002F\u002Fgithub.com\u002FDCx7C5\u002Fai-marketplace\u002Fblob\u002Fmain\u002Fskills\u002Fwindows\u002Fservices\u002Fwmi\u002Fexec\u002Fexploit\u002FSKILL.md",[],[87,62,88,65,89,90,91,21,92,93,94,95,96,97,67,98,99,100,101,102],"exploit","services wmi exec exploit","exec","remote","traffic","Bash","analysis","team","across","crackmapexec","native","command","lateral movement","read","lateral-movement","Impacket",{"skillNo":104,"name":105,"nameZh":6,"description":106,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":107,"categories":108,"keywords":109,"publishedAt":22},"01M71HAG6MHKNQV8KDZKMT4886","registry-artifact-extract","Extract and analyze Windows Registry hives to uncover user activity, installed software, autostart entries, and evidence of system compromise.","https:\u002F\u002Fgithub.com\u002FDCx7C5\u002Fai-marketplace\u002Fblob\u002Fmain\u002Fskills\u002Fwindows\u002Fregistry\u002Fartifact\u002Fextract\u002FSKILL.md",[],[110,111,112,113,114,115,21,92,116,93,117,118,67,100,119,120,121,122,123,124],"artifact","registry","registry artifact extract","extract","evidence-collection","artifact-analysis","collection","analyze","uncover","software","activity","digital-forensics","windows-registry","regripper","sonnet",{"skillNo":126,"name":127,"nameZh":6,"description":128,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":129,"categories":130,"keywords":131,"publishedAt":22},"01M71HACR4A8XTWTTSXK4124AA","processes-token-impersonate-detect","Detect Windows token impersonation and privilege abuse including SeImpersonatePrivilege exploitation via Potato attacks and token stealing techniques.","https:\u002F\u002Fgithub.com\u002FDCx7C5\u002Fai-marketplace\u002Fblob\u002Fmain\u002Fskills\u002Fwindows\u002Fprocesses\u002Ftoken\u002Fimpersonate\u002Fdetect\u002FSKILL.md",[],[63,132,133,134,135,136,137,68,138,21,79,139,140,141,142,143,144,145,146,67],"processes token impersonate detect","token","processes","attacks","windows","escalation","techniques","privilege-escalation","seimpersonate","token-impersonation","printspoofer","exploitation","potato","impersonation","via",{"skillNo":148,"name":149,"nameZh":6,"description":150,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":151,"categories":152,"keywords":153,"publishedAt":22},"01M71HAABSB4Z6QFSXEG00R4RK","processes-thread-injection-detect","Detect Windows thread injection techniques including CreateRemoteThread, QueueUserAPC, NtCreateThreadEx, and thread hijacking via process memory analysis.","https:\u002F\u002Fgithub.com\u002FDCx7C5\u002Fai-marketplace\u002Fblob\u002Fmain\u002Fskills\u002Fwindows\u002Fprocesses\u002Fthread\u002Finjection\u002Fdetect\u002FSKILL.md",[],[154,155,63,156,134,136,157,68,158,138,21,79,93,159,160,161,162,146,67,69],"thread","processes thread injection detect","injection","memory","process-injection","api","apc-injection","thread-injection","nt-api",{"skillNo":164,"name":165,"nameZh":6,"description":166,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":167,"categories":168,"keywords":169,"publishedAt":22},"01M71HA7H311V5G14WMX58HQSS","processes-hollow-technique-detect","Detect process hollowing (RunPE) by identifying processes with mismatched image paths between PEB and VAD tree, or unmapped PE headers in memory.","https:\u002F\u002Fgithub.com\u002FDCx7C5\u002Fai-marketplace\u002Fblob\u002Fmain\u002Fskills\u002Fwindows\u002Fprocesses\u002Fhollow\u002Ftechnique\u002Fdetect\u002FSKILL.md",[],[170,63,134,171,157,68,172,173,21,79,174,175,176,177,178,179,67,69,180,181],"processes hollow technique detect","tree","memory-forensics","process-hollow","peb","runpe","image","pe-header","in memory","with","mismatched","headers"]