What I Found In A Zeek Dataset (And How I Found It)
- Jun 5
- 9 min read
Updated: Jul 11
What happens when a data analyst opens a Zeek conn.log for the first time — and starts asking questions?
Updated: After the deeper analysis in Part 2, I revisited the host classifications and the kill-chain reconstruction in this post and tightened them to what the data strictly supports. The core discovery — a Mirai-infected host mass-scanning from inside the network — is unchanged and fully reproducible. What I corrected: two hosts I'd grouped as "infected" turned out to be doing different things (internal reconnaissance, ordinary HTTP), two IPs I'd flagged as command-and-control don't fit that role, and a fixed "heartbeat" packet size I cited didn't hold up on re-analysis. Re-auditing your own findings is the job — so I'm leaving the correction visible rather than quietly editing. The real command-and-control finding lives in Part 2.
What is Zeek — and Why Should Analysts Care?
Zeek (formerly Bro) is an open-source network analysis framework widely used in Security Operations Centers (SOCs). Unlike a firewall that blocks traffic, Zeek passively monitors network activity and produces detailed, structured logs — one for Domain Name System (DNS) queries, one for Hypertext Transfer Protocol (HTTP) activity, and one for connection metadata. The logs are a catalog of every network connection.
The dataset I used comes from Kaggle: DNS and Connections Log — Zeek/Corelight. It includes two log types: dns.log and conn.log. I focused on conn.log — the connection log.
💡 What is conn.log?
The connection log captures every network session Zeek observes — who talked to whom, for how long, how much data moved, and over which protocol. It's one of the first places you look when you suspect unusual behavior, for SOC Analysts.
The Dataset: What I Was Working With
I explored the schema before writing a single query. Here are the key fields in conn.log and why each one matters from a security perspective:
Field | What it contains | Why security analysts care |
ts | Timestamp of the connection | Timing patterns reveal automated or off-hours activity |
uid | Unique connection identifier | Links records across multiple log types |
orig_h | Originating (source) IP address | Who initiated the connection |
resp_h | Responding (destination) IP address | Where are they going — internal or external? |
proto | Protocol (TCP, UDP, ICMP) | Unusual protocols can indicate tunneling or scanning |
service | Application-layer service (HTTP, DNS, SSH) | Identifies what kind of traffic this really is |
duration | How long the connection lasted | Very long or very short sessions can both be red flags |
orig_bytes | Bytes sent by the source | Large outbound transfers = potential data exfiltration |
resp_bytes | Bytes sent by the destination | Large responses may signal data staging or C2 activity |
conn_state | State of the connection at close | Rejected or half-open connections suggest scanning |
Setting Up SQLite on My Mac
I want to be honest about how this actually went, because the polished "here are the steps" version leaves out the part where you learn the most.
In the terminal, I used Linux to navigate to the folder containing the downloaded log files. I ran the ls command to view the folder’s contents, then launched SQLite directly from the terminal. I launched SQLite and ran my import commands. I ran SELECT COUNT(*) FROM conn_logs; and there was no output. What actually happened: I had typed my folder name wrong in the terminal. Whoops, make sure you type the right folder name. This mishap nearly derailed the whole session. Learning to debug your environment is as important as knowing the queries.
The second challenge: Zeek's conn.log is not a clean CSV. It opens with metadata lines starting with # that SQLite imported as actual data rows, meaning my column names became timestamps and IP addresses. The fix was to strip those lines first:
# Strip Zeek metadata header lines
grep -v "^#" conn.log > conn_clean.csvThen I created the table with proper column names before importing. Schemas are defined before data is loaded.
-- Create the table first, then import
CREATE TABLE conn_logs (
ts TEXT, uid TEXT, orig_h TEXT, orig_p TEXT,
resp_h TEXT, resp_p TEXT, proto TEXT, service TEXT,
duration TEXT, orig_bytes TEXT, resp_bytes TEXT,
conn_state TEXT, local_orig TEXT, local_resp TEXT,
missed_bytes TEXT, history TEXT, orig_pkts TEXT,
orig_ip_bytes TEXT, resp_pkts TEXT, resp_ip_bytes TEXT,
tunnel_parents TEXT
);
.mode tabs
.import conn_clean.csv conn_logsI ran my SELECT statement, and it returned 1,319,960 log records.
Query 1: Where I Started — Connection States
Connection states tell you what happened at the end of every network session. This is the first query I ran on the network dataset. This query is like taking the pulse of the network.
SELECT conn_state, COUNT(*) AS total
FROM conn_logs
GROUP BY conn_state
ORDER BY total DESC;My results from the query and what they mean. I also used AI to help me digest the results:
State | Count | What it means |
S0 | 647,224 | Connection attempted — no response. Classic port scanning. |
SF | 259,992 | Normal completed connection. This is healthy traffic. |
REJ | 227,555 | Connection actively refused. Scanning hitting closed ports. |
OTH | 142,044 | No SYN seen — partial traffic, possible tunneling. |
RSTO | 13,639 | Reset by originator — abrupt termination. |
The results show that the failed connections (S0, REJ, OTH, RSTO) far outnumber successful ones (SF), this means over 60% of connections in the dataset never completed successfully.
Query 2: Finding the Source
With 647,224 S0 connections flagged, the next question was simple: who is responsible?
SELECT orig_h, COUNT(*) AS total
FROM conn_logs
WHERE conn_state = 'S0'
GROUP BY orig_h
ORDER BY total DESC
LIMIT 10;The top result: 192.168.10.43 — 513,865 S0 connections. This meant that one internal IP was responsible for 79% of all unanswered connection attempts. That's not a human user - it's an automated, malicious machine. When I checked what it was attacking, I found it was systematically scanning four internal subnets and also reaching out to external IPs. Scanning at that volume from an internal host is exactly how a botnet spreads. (What those external connections actually were — and whether any amount to command-and-control — is what Part 2 investigates. Part 1 establishes the scanning; it doesn't prove a controller.)
Query 3: Identifying the Attack — Port Analysis
Knowing who was scanning told me the what. Knowing which ports they were hitting told me the why.
SELECT resp_p, COUNT(*) AS attempts
FROM conn_logs
WHERE orig_h = '192.168.10.43'
AND conn_state = 'S0'
GROUP BY resp_p
ORDER BY attempts DESC
LIMIT 10;Port | Attempts | Service |
23 | 157,065 | Telnet — Mirai's primary target |
22 | 123,913 | SSH — brute force credential attempts |
443 | 51,713 | HTTPS — scanning web-facing devices |
2323 | 17,381 | Alternative Telnet — classic Mirai port |
2222 | 14,144 | Alternative SSH — same pattern |
The chart shows massive scanning on port 23 and port 2323 — that's classic Mirai behavior. Mirai hunts internet-connected devices — cameras, routers — that still use factory-default credentials, breaks in, and conscripts them to launch DDoS attacks. The SSH attempts on 22 fit too; some Mirai variants use it. So this isn't just random scanning — it's a known, named threat.
💡 What is Mirai?
Mirai is malware from 2016 that hunts for smart devices (cameras, routers, etc.) still using factory default passwords. It takes them over and turns them into a botnet — an army of hacked devices — which then floods websites with traffic until they crash. Mirai took down major parts of the internet, including the Dyn DNS service. When its code was released publicly, copycats created many versions that are still active today.
Query 4: The Complete Host Summary
I wanted one query that profiled the whole network at once — every host, its connections, failed scans, successes, ICMP, and success rate.
SELECT
orig_h,
COUNT(*) AS total_connections,
SUM(CASE WHEN conn_state = 'S0' THEN 1 ELSE 0 END) AS failed_scans,
SUM(CASE WHEN conn_state = 'SF' THEN 1 ELSE 0 END) AS successful,
SUM(CASE WHEN proto = 'icmp' THEN 1 ELSE 0 END) AS icmp_recon,
ROUND(SUM(CASE WHEN conn_state = 'SF' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 1) AS success_rate_pct
FROM conn_logs
GROUP BY orig_h
ORDER BY total_connections DESC
LIMIT 10;Host | Total | Failed Scans | Successful | ICMP | Success Rate | Verdict |
192.168.10.43 | 894,218 | 513,865 | 158,154 | 36,682 | 17.7% | Primary threat actor |
192.168.10.50 | 251,368 | 111,382 | 0 | 36,633 | 0% | Confirmed botnet node |
10.200.200.80 | 40,615 | 20,868 | 1,643 | 4,040 | 4% | Likely third infected host |
192.168.61.21 | 24,567 | 3 | 23,238 | 55 | 94.6% | Normal — baseline host |
54.243.185.88 | 10,564 | 0 | 10,562 | 0 | 99.9% | High-volume outbound HTTP — likely a normal client, not a threat |
Only .43 fits the Mirai signature cleanly — it's the one hammering telnet-family ports at scale, and it's the confirmed infection. The others are more nuanced than a single success-rate number suggests. 192.168.10.50 is scanning too, but on HTTPS and ICMP rather than telnet, which reads as internal reconnaissance — Part 2 digs into this. 10.200.200.80 is also sweeping, but again not on Mirai's ports; a scanner, yes, but a Mirai node isn't established. And 54.243.185.88, which at first glance looked like it could be a command-and-control server, turns out to only originate outbound HTTP connections — the opposite of what a C2 endpoint looks like (a controller is something infected hosts connect to). The honest read from this one query: one confirmed Mirai infection (.43), plus additional anomalous hosts whose exact roles need the deeper analysis in Part 2.
What Part 1 Reconstructed
From four queries, here's what the data supports — stated as what I could confirm, not beyond it:
Scanning (the confirmed core). 192.168.10.43 made over half a million connection attempts that got no reply, hammering Telnet (23), SSH (22), and alternate Telnet (2323) — the classic Mirai playbook. This is the confirmed infection.
Additional scanning hosts. 192.168.10.50 and 10.200.200.80 also show heavy scanning, but on HTTPS/ICMP rather than telnet — a different pattern (discovery/enumeration) that Part 2 examines. Whether these are additional infections or something else isn't settled by Part 1 alone.
Outbound external traffic. Several external IPs show high connection volume. Part 1 isn't enough to classify them — identifying genuine command-and-control requires the beaconing analysis in Part 2, which is where the actual C2 servers turn up.
An earlier version of this section described lateral movement across subnets, a fixed 999-byte "heartbeat" packet, and an AWS-hosted C2 server. On re-analysis those specifics didn't reproduce against the data, so I've removed them. The confirmed C2 finding — a host beaconing to four external servers on a fixed cadence — is in Part 2.
The Closing Number — 17.7% vs 94.6%
I ran one final comparison between the primary infected host and the cleanest host on the network:
Host | Total Connections | Success Rate | Verdict |
192.168.61.21 | 24,567 | 94.6% | Normal user — completing nearly every connection |
192.168.10.43 | 894,218 | 17.7% | Infected host — failing 82% of connections |
A healthy machine completes nearly every connection it attempts. An infected machine fails 82% of the time because it is not trying to communicate — it is trying to find unlocked doors. That single percentage point difference is the difference between a user browsing the internet and a machine conscripted into a botnet without anyone knowing.
Analyst summary - what the data supports:
Host | Role | Action |
192.168.10.43 | Confirmed Mirai Scanner | Isolate immediately |
192.168.10.50 | Heavy scanner — internal recon (HTTPS/ICMP) | Isolate and investigate |
10.200.200.80 | External scanner — non-Mirai discovery | Investigate |
192.168.61.21 | Normal host | No action needed |
(Two external IPs I'd initially listed as "suspected C2 servers" were removed — both only originate outbound traffic, which doesn't fit a command-and-control profile.)
🔗 ISC2 CC Domain Connections
This investigation maps directly to three exam domains: Domain 1 — Security Principles (CIA triad violation, availability threatened via DDoS recruitment); Domain 4 — Network Security (unauthorized scanning and internal reconnaissance); Domain 5 — Security Operations (incident identification and response).
🪞 Reflection questions I am sitting with
How would this investigation look different in a SIEM like Splunk? What would I catch faster — and what might the dashboard hide?
The dataset captures the attack in progress but not the origin. How would a real analyst trace back how 192.168.10.43 first became infected?
192.168.10.50 was scanned heavily with almost no successful connections. At what point does a SOC analyst isolate a host versus observing it longer to learn more?
Several external IPs showed high outbound volume. How do analysts distinguish normal cloud and web traffic from malicious command-and-control?
What I Would Do Differently Next Time
Establish a baseline deliberately. I found my clean host accidentally. A disciplined approach documents what normal looks like — byte ranges, expected protocols, typical durations — before hunting anomalies.
Separate the number from the verdict. My biggest lesson: a query result and the conclusion drawn from it are two different things. "Zero successful connections" is a fact; "confirmed botnet node" is a hypothesis that needs its own query to test. Several of my first-pass verdicts were right about something being off but wrong about what — and the fix was always another query, not a better guess.
Cross-reference the DNS log. I downloaded dns.log too. Joining it with conn.log would reveal which domains those external IPs resolved to — a key step in confirming infrastructure by name.
There is a Part 2. The deeper analysis uncovered C2 beaconing on a fixed cadence, a second host mapping the network from the inside, external SSH brute-force campaigns, and one external IP that held a multi-hour session inside the network — a confirmed intrusion. That story is its own entry.
💼 From the Analyst's Desk to the SOC Floor
I started with nothing but a raw, messy log file from Zeek — no security software, no dashboards, no ready-made alerts. From that alone, I identified a Mirai botnet infection, isolated the primary infected host, confirmed the malware family by its port signature, and closed with a success-rate comparison that makes the evidence hard to ignore. Just as important: where my first-pass classifications ran ahead of the data, I went back, re-ran the queries, and corrected them. Building a detection narrative from raw data — and knowing when your own conclusion doesn't hold — is the skill that matters most.
The instincts I built over years in data analytics — spotting outliers, finding patterns, questioning anomalies — didn't need to be thrown away for cybersecurity work. They just needed to be pointed in a new direction, and disciplined with one rule: run it before you believe it.
Keep reading & see the code
🔗 Read the follow-up: From Storm Signatures to Attack Signatures — Log Analysis for Beginners
💻 Every query in this investigation is on GitHub, documented and reproducible against the public dataset: github.com/Tash925/mirai-zeek-detection
→ Part 2
Going Deeper — Beacons, a Second Host, and What the Data Confessed — C2 beaconing on a fixed cadence, a second infected host mapping the network from the inside, external SSH brute force, and the one IP that actually got in.


Comments