What I Found In A Zeek Dataset (And How I Found It)
- Jun 5
- 8 min read
What happens when a data analyst opens a Zeek conn.log for the first time — and starts asking questions?
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. That suggests it was trying to connect to a command and control server (a hacker's remote control center).
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 is a botnet that looks for Internet-connected devices like cameras and routers that still use factory default usernames and passwords. It breaks in, takes over, and uses them to launch huge DDoS attacks. The SSH attempts on port 22 are also there, which some Mirai variants use too. 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 told the entire story. You know — every infected host, every red flag, all in a single output. Something I could bring straight to a briefing and say, 'Here's what we're dealing with.'
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% | Suspected C2 server (AWS) |
The results tell the whole story. One host — 192.168.10.43 — is clearly the main problem. It made nearly 900,000 attempts. Most of the attempts are failed scans, and they only succeeded about 18% of the time. That's the primary threat actor. Another host, 192.168.10.50, made over 250,000 attempts, succeeded zero times, and sent a ton of ICMP traffic — that's a confirmed botnet node. A third internal IP showed suspicious but lighter activity. One host looked completely normal — almost all its connections succeeded. And lastly, an AWS IP had nearly perfect success connecting out, with no scanning behavior — that's our suspected command and control server. So in one result table, we have the attacker, the infected bots, the normal baseline, and the C2.
The Kill Chain — Reconstructed
Across four queries, I had reconstructed the complete attack sequence from start to finish, which I had AI help me to visualize:
// Step 1 — Reconnaissance (mapping the network)
192.168.10.43 sent out nearly 37,000 ICMP pings to see which devices were alive. 192.168.10.50 did the exact same thing — a sign it was already infected and copying the attacker's behavior.
// Step 2 — Scanning (looking for weak spots)
Then the main attacker, 192.168.10.43, made over half a million connection attempts that got no reply. It was hammering ports like Telnet (23), SSH (22), and alternate Telnet (2323) — the classic Mirai playbook.
// Step 3 — Lateral Movement (spreading to other devices)
The scanning wasn't stuck in one place. It spread across multiple subnets — .10.x to .49.x to .60.x to .61.x. And sure enough, a host on a completely separate subnet (10.200.200.80) came back as infected, proving the malware jumped across the network.
// Step 4 — C2 Communication (phoning home)
Finally, infected devices reached out to external IP addresses, including one on Amazon's AWS cloud. They sent tiny, fixed-sized packets — 999 bytes — over and over. That's a malware heartbeat: infected devices checking in with their controller.
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.
My complete analyst summary:
Host | Role | Action |
192.168.10.43 | Primary infected host | Isolate immediately |
192.168.10.50 | Confirmed botnet node | Isolate immediately |
10.200.200.80 | Likely third infected host | Isolate and investigate |
54.243.185.88 | Suspected C2 server | Block at firewall |
185.61.150.201 | Suspected C2 server | Block at firewall |
192.168.61.21 | Normal host | No action needed |
🔗 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 lateral movement); 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 had zero successful connections — only scanning. At what point does a SOC analyst isolate a host versus continuing to observe it to learn more about the attacker?
The external IPs included AWS infrastructure. How do security teams distinguish legitimate cloud traffic from C2 servers hosted on cloud providers?
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 — average byte ranges, expected protocols, typical connection durations — before hunting anomalies.
Cross-reference the DNS log. I downloaded the dns.log too. Joining it with conn.log would reveal which domains those external IPs resolved to — a key step in confirming C2 infrastructure by name.
There is a Part 2 coming. This investigation uncovered beaconing behavior, a detailed timeline proving automated attack scheduling, external SSH brute force attempts from multiple foreign IPs, and a suspicious host with unusually high data transfer that may represent a successful login. That story deserves 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 fancy security software, no dashboards, no ready-made alerts. From that alone, I spotted a Mirai botnet infection, traced how it spread across four different parts of the network, confirmed it was talking to a command and control server, and wrapped it up with a simple success-rate comparison that makes the evidence impossible to ignore.
The instincts I built over years of working 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.
→ Coming up in Part 2
Going Deeper — Beacons, Timelines, and What the Data Confessed — beaconing behavior, attack scheduling, external SSH brute force, and the one IP that may have actually gotten in.



Comments