{"id":183,"date":"2026-02-10T09:00:00","date_gmt":"2026-02-10T09:00:00","guid":{"rendered":"http:\/\/justruss.tech\/index.php\/2024\/03\/14\/getting-started-with-zeek-for-network-threat-hunting\/"},"modified":"2026-05-15T10:34:55","modified_gmt":"2026-05-15T10:34:55","slug":"getting-started-with-zeek-for-network-threat-hunting","status":"publish","type":"post","link":"https:\/\/justruss.tech\/index.php\/2026\/02\/10\/getting-started-with-zeek-for-network-threat-hunting\/","title":{"rendered":"Getting Started with Zeek for Network Threat Hunting"},"content":{"rendered":"<p>Zeek is a passive network analysis framework. Where Wireshark gives you packets, Zeek gives you structured logs. It parses protocols, extracts fields, and writes them to JSON log files you can query like a database. For hunting across hours or days of traffic it is significantly faster than working with raw packet captures because you are querying structured data rather than filtering hex.<\/p>\n<h3>Installation on Ubuntu 22.04<\/h3>\n<pre>echo \"deb http:\/\/download.opensuse.org\/repositories\/security:\/zeek\/xUbuntu_22.04\/ \/\" | \\\n    sudo tee \/etc\/apt\/sources.list.d\/security:zeek.list\ncurl -fsSL https:\/\/download.opensuse.org\/repositories\/security:zeek\/xUbuntu_22.04\/Release.key | \\\n    gpg --dearmor | sudo tee \/etc\/apt\/trusted.gpg.d\/security_zeek.gpg &gt; \/dev\/null\nsudo apt update &amp;&amp; sudo apt install zeek -y\n\n\/\/ Configure the monitored interface\nsudo nano \/etc\/zeek\/node.cfg\n\/\/ Change interface=eth0 to your actual span\/tap interface\n\n\/\/ Enable JSON output (much better for SIEM ingestion and jq parsing)\necho \"redef LogAscii::use_json = T;\" | sudo tee -a \/etc\/zeek\/local.zeek\necho \"redef LogAscii::json_timestamps = JSON::TS_ISO8601;\" | sudo tee -a \/etc\/zeek\/local.zeek\n\n\/\/ Start Zeek\nsudo zeekctl deploy\nsudo zeekctl status\n\n\/\/ Verify logs are writing\ntail -f \/opt\/zeek\/logs\/current\/conn.log | python3 -m json.tool | head -50<\/pre>\n<h3>Log files: what they contain and when to use them<\/h3>\n<pre>\/\/ conn.log: every network connection\n\/\/ Fields: ts, id.orig_h, id.orig_p, id.resp_h, id.resp_p, proto, duration,\n\/\/         orig_bytes, resp_bytes, conn_state, history\n\/\/ Use for: beaconing detection, data exfiltration volume, unusual destinations\n\n\/\/ dns.log: every DNS query\n\/\/ Fields: ts, id.orig_h, query, qtype_name, answers, TTLs\n\/\/ Use for: DNS tunnelling, C2 domain detection, DGA detection\n\n\/\/ ssl.log: TLS sessions\n\/\/ Fields: ts, id.orig_h, id.resp_h, version, cipher, ja3, ja3s, server_name,\n\/\/         subject, issuer, validation_status\n\/\/ Use for: C2 TLS fingerprinting (JA3), expired\/self-signed cert detection\n\n\/\/ http.log: HTTP requests\n\/\/ Fields: ts, id.orig_h, method, host, uri, user_agent, status_code,\n\/\/         request_body_len, response_body_len\n\/\/ Use for: malicious user agents, suspicious URIs, large exfil in POST bodies\n\n\/\/ files.log: file transfers across any protocol\n\/\/ Fields: ts, id.orig_h, fuid, md5, sha1, sha256, mime_type, filename, source\n\/\/ Use for: malware download detection, file hash reputation checking\n\n\/\/ weird.log: protocol anomalies Zeek could not parse cleanly\n\/\/ Use for: protocol-based evasion, malformed packets<\/pre>\n<h3>Hunting beaconing in conn.log<\/h3>\n<pre>\/\/ Method 1: zeek-cut + awk interval calculation\ncat conn.log | zeek-cut -d ts id.orig_h id.resp_h id.resp_p | \\\n    sort -k1,1n -k2,2 -k3,3 -k4,4 | \\\n    awk '\n        {key = $2\" \"$3\" \"$4}\n        key == prev_key {\n            diff = $1 - prev_ts\n            total[key] += diff\n            count[key]++\n            sq_diff = (diff - (total[key]\/count[key]))^2\n            variance[key] += sq_diff\n        }\n        {prev_key = key; prev_ts = $1}\n        END {\n            for (k in count) {\n                if (count[k] &gt;= 10) {\n                    mean = total[k] \/ count[k]\n                    stdev = sqrt(variance[k] \/ count[k])\n                    cv = stdev \/ mean\n                    if (cv  30 &amp;&amp; mean &lt; 600)\n                        printf &quot;BEACON CANDIDATE: %s mean=%.1fs stdev=%.1fs cv=%.3f samples=%d\\n&quot;,\n                            k, mean, stdev, cv, count[k]\n                }\n            }\n        }\n    &#039;\n\n\/\/ Method 2: Python script for more sophisticated analysis\npython3 &lt;&lt; EOF\nimport json, statistics\nfrom collections import defaultdict\n\nconnections = defaultdict(list)\nwith open(&quot;\/opt\/zeek\/logs\/current\/conn.log&quot;) as f:\n    for line in f:\n        try:\n            rec = json.loads(line)\n            if rec.get(&quot;proto&quot;) in [&quot;tcp&quot;, &quot;udp&quot;]:\n                key = (rec[&quot;id.orig_h&quot;], rec[&quot;id.resp_h&quot;], str(rec[&quot;id.resp_p&quot;]))\n                connections[key].append(float(rec[&quot;ts&quot;]))\n        except:\n            pass\n\nprint(f&quot;Analysing {len(connections)} unique connections...&quot;)\nfor key, timestamps in sorted(connections.items()):\n    if len(timestamps) &lt; 10:\n        continue\n    timestamps.sort()\n    intervals = [timestamps[i+1]-timestamps[i] for i in range(len(timestamps)-1)]\n    if len(intervals)  0 else 999\n    # Flag: very regular timing, 30s to 10min interval range\n    if cv &lt; 0.1 and 30 &lt; mean_i  {dst}:{port}\")\n        print(f\"  mean={mean_i:.1f}s  stdev={stdev_i:.1f}s  cv={cv:.3f}  samples={len(intervals)}\")\nEOF<\/pre>\n<h3>DNS tunnelling detection<\/h3>\n<pre>\/\/ Long query names (DNS tunnelling uses subdomains to encode data)\ncat dns.log | zeek-cut query id.orig_h | \\\n    awk '{if (length($1) &gt; 50) print length($1), $0}' | \\\n    sort -rn | head -20\n\n\/\/ High query rate per base domain (rapid polling from tunnelling tools)\ncat dns.log | zeek-cut query | \\\n    awk -F. 'NF &gt; 2 {print $(NF-1)\".\"$NF}' | \\\n    sort | uniq -c | sort -rn | head -20\n\n\/\/ Queries with high entropy subdomains (encoded data looks random)\npython3 &lt; 2 else \"\"\n                if len(subdomain) &gt; 15 and entropy(subdomain) &gt; 3.5:\n                    print(f\"HIGH ENTROPY SUBDOMAIN: {query}\")\n                    print(f\"  subdomain={subdomain} length={len(subdomain)} entropy={entropy(subdomain):.2f}\")\n        except:\n            pass\nEOF<\/pre>\n<h3>JA3 fingerprinting for C2 detection<\/h3>\n<pre>\/\/ Check all TLS connections against known malware JA3 hashes\n\/\/ Known Cobalt Strike default: a0e9f5d64349fb13191bc781f81f42e1\n\/\/ Known Metasploit Meterpreter: 51c64c77e60f3980eea90869b68c58a8\n\ncat ssl.log | zeek-cut ja3 ja3s id.orig_h id.resp_h server_name | \\\n    grep -E \"a0e9f5d64349fb13191bc781f81f42e1|51c64c77e60f3980eea90869b68c58a8\"\n\n\/\/ More comprehensive check against a JA3 blocklist\npython3 &lt;&lt; EOF\nimport json\n\n# Known malicious JA3 hashes (expand from threat intel feeds)\nblocklist = {\n    &quot;a0e9f5d64349fb13191bc781f81f42e1&quot;: &quot;Cobalt Strike default&quot;,\n    &quot;51c64c77e60f3980eea90869b68c58a8&quot;: &quot;Metasploit Meterpreter&quot;,\n    &quot;6734f37431670b3ab4292b8f60f29984&quot;: &quot;Cobalt Strike malleable&quot;,\n    &quot;b386946a5a44d1ddcc843bc75336dfce&quot;: &quot;Possible malware&quot;,\n}\n\nwith open(&quot;\/opt\/zeek\/logs\/current\/ssl.log&quot;) as f:\n    for line in f:\n        try:\n            rec = json.loads(line)\n            ja3 = rec.get(&quot;ja3&quot;, &quot;&quot;)\n            if ja3 in blocklist:\n                print(f&quot;BLOCKED JA3: {blocklist[ja3]}&quot;)\n                print(f&quot;  src={rec[&#039;id.orig_h&#039;]} dst={rec[&#039;id.resp_h&#039;]}&quot;)\n                print(f&quot;  sni={rec.get(&#039;server_name&#039;,&#039;unknown&#039;)} ja3={ja3}&quot;)\n        except:\n            pass\nEOF\n\n\/\/ Also check for self-signed certificates (common in C2 infrastructure)\ncat ssl.log | zeek-cut id.orig_h id.resp_h server_name subject issuer validation_status | \\\n    awk &#039;$6 == &quot;self signed certificate&quot; {print &quot;SELF-SIGNED:&quot;, $0}&#039;<\/pre>\n<h3>Feeding Zeek logs into Elastic<\/h3>\n<pre>\/\/ Filebeat configuration for Zeek log ingestion\n\/\/ \/etc\/filebeat\/modules.d\/zeek.yml\n\n- module: zeek\n  connection:\n    enabled: true\n    var.paths: [\"\/opt\/zeek\/logs\/current\/conn.log\"]\n  dns:\n    enabled: true\n    var.paths: [\"\/opt\/zeek\/logs\/current\/dns.log\"]\n  ssl:\n    enabled: true\n    var.paths: [\"\/opt\/zeek\/logs\/current\/ssl.log\"]\n  http:\n    enabled: true\n    var.paths: [\"\/opt\/zeek\/logs\/current\/http.log\"]\n  files:\n    enabled: true\n    var.paths: [\"\/opt\/zeek\/logs\/current\/files.log\"]\n\n\/\/ Restart Filebeat\nsystemctl restart filebeat\n\n\/\/ Verify data is arriving in Kibana:\n\/\/ Index: filebeat-*\n\/\/ Filter: event.module: zeek<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Zeek is one of those tools that looks overwhelming at first.<br \/>\nAfter a few weeks of using it in a lab environment, here is how I actually got it to a useful state.<\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[13],"tags":[],"class_list":["post-183","post","type-post","status-publish","format-standard","hentry","category-threat-hunting"],"_links":{"self":[{"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/posts\/183","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/comments?post=183"}],"version-history":[{"count":4,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/posts\/183\/revisions"}],"predecessor-version":[{"id":280,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/posts\/183\/revisions\/280"}],"wp:attachment":[{"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/media?parent=183"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/categories?post=183"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/tags?post=183"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}