top of page

Going Deeper — Beacons, a Second Host, and What the Data Confessed

  • Jul 10
  • 10 min read

"Part 1 told me something was wrong. Part 2 is where I learned to tell a real signal from a convincing one."


In Part 1, I loaded a Zeek conn.log into SQLite and identified a Mirai botnet infection: mass scanning, the telnet-family port signature, and a success-rate comparison that made the compromise undeniable. That felt like a complete story.


Then I kept querying. I want to be honest with you about what Part 2 actually became — because it turned into a lesson about the difference between a finding that looks right and one that reproduces. Some of what I first thought I'd found didn't survive a second, stricter look. The story below is the one the data actually supports, checked query by query against the raw logs.


💡 A note on method

Every figure in this post came off my own screen, from a query I can re-run. Where an earlier pass over-counted or mislabeled something, I've said so plainly and shown the corrected result. In threat hunting, an anomaly that dissolves under a tighter filter was noise. One that survives is a finding. Learning to tell them apart is the whole job.


Query 1: The Heartbeat — C2 Beaconing (and a false start)


The most important question after confirming an infection is whether any host is still taking orders — reaching out to a command-and-control server on a schedule. A beacon is a bot raising its hand at fixed intervals to ask "what next?" The instinct is the same one I used reading atmospheric data: a signal you can trust isn't the loudest reading, it's the one that keeps time.


My first attempt looked for destinations whose average gap between connections fell into a beacon-like window. That was a trap. Averaging is blind to burstiness — a host that fires ten connections in half a second and then goes quiet for a minute has the same average gap as one that connects steadily every six seconds. One is scan spray; the other is a beacon. When I pulled the candidates, the giveaway was in the spread: minimum gaps near zero, maximum gaps in the tens of seconds. Those weren't heartbeats. They were scan bursts that happened to average into range.


The fix was to filter on regularity, not average — the coefficient of variation (standard deviation ÷ mean). A true beacon clusters tightly around its period; bursty traffic scatters. I also constrained direction: beacons are outbound, internal host to external server. Without that, inbound scan traffic hitting internal machines gets counted as beaconing.


WITH ordered AS (
  SELECT orig_h, resp_h, CAST(ts AS REAL) AS t,
    LAG(CAST(ts AS REAL)) OVER (
      PARTITION BY orig_h, resp_h ORDER BY CAST(ts AS REAL)) AS prev_t
  FROM conn_logs
  WHERE orig_h LIKE '192.168.%'
    AND resp_h NOT LIKE '192.168.%'),
gaps AS (SELECT orig_h, resp_h, (t - prev_t) AS gap
       FROM ordered WHERE prev_t IS NOT NULL)
SELECT orig_h, resp_h, COUNT(*) AS conns,
  ROUND(AVG(gap),1) AS avg_gap
FROM gaps GROUP BY orig_h, resp_h
HAVING conns >= 50 AND cv < 0.35
ORDER BY cv ASC;

Under that filter, the inflated list collapsed to a single host with genuine periodic behavior:

Infected Host

C2 Server

Connections

Avg Interval

CV

192.168.60.22

69.43.168.214

216

757.8s

0.06

192.168.60.22

109.74.9.119

217

754.5s

0.09

192.168.60.22

192.188.58.163

217

754.3s

0.09

192.168.60.22

203.153.165.21

217

754.2s

0.09

192.168.60.22 beacons to four external C2 servers on a steady ~755-second cadence — roughly every 12.6 minutes — held so consistently that the variation stays under 10% across more than 200 callbacks to each. That regularity is the signature. This host wasn't scanning; it was checking in, on schedule, with a fixed set of controllers.


💡 Why regularity beats average

Legitimate traffic is irregular — humans and applications connect when they need to. A scheduler firing on a timer produces the opposite: the same interval, over and over, regardless of what the network is doing. The coefficient of variation captures exactly that. A low CV means "this repeats like a metronome," and metronomes in network logs are almost always automation, not people.


Query 2: A Second Infected Host — Internal Reconnaissance


When I profiled connection-success rates across every internal host, one machine stood out for the opposite reason .43 had: 192.168.10.50 completed barely 0.5% of its connections across a quarter-million attempts. Something on this host was knocking constantly and almost never getting an answer.


SELECT
  CASE WHEN resp_h LIKE '192.168.%' THEN 'internal'
       ELSE 'external' END AS target_type,
  COUNT(*) AS conns,
  COUNT(DISTINCT resp_h) AS distinct_targets
FROM conn_logs
WHERE orig_h = '192.168.10.50'
GROUP BY target_type;

Where .43 fired outward at 338,000 external IPs, .50 pointed inward: of its 251,368 connections, all but three targeted internal 192.168.x addresses — reaching 771 distinct internal hosts, effectively every machine on the network. And the shape of the probing told a two-phase story:


Activity

Connections

Targets

Meaning

443 (HTTPS)

51,686

768

Broad sweep — probing HTTPS on nearly every host

ICMP (type 14)

18,573

763

Timestamp sweep — host discovery

ICMP (type 0)

18,060

769

Echo sweep — "who's alive?"

445 (SMB)

3,439

8

Narrow — targeted at 8 specific hosts

21 (FTP) / 135 (RPC) / 139 (NetBIOS)

~1,360 combined

7 each

Targeted service enumeration

Two phases stand out. First a broad discovery sweep — HTTPS and ICMP probes fanning across ~770 internal hosts, the network equivalent of walking every hallway to see who answers. Then a narrow, targeted probe — SMB, RPC, NetBIOS, and FTP aimed at only 7–8 specific hosts, presumably the ones that responded. Wide reconnaissance, then selective enumeration. That selection is what makes it read like recon rather than noise.


The connection states confirm it: S0 (no response, 111,382) and REJ (actively refused, 101,972) together account for 85% of .50's traffic. That isn't a broken host — it's exactly what enumeration looks like from the log side. Most probes hit machines that don't offer the service, so they time out or get refused. The 0.5% success rate is the fingerprint of a host systematically testing every door in the building, most of them locked.


🔎 One honest limit

The logs show what .50 is doing — sweeping the network — not definitively why. A legitimate vulnerability scanner produces similar traffic. In a real environment the next step is to check whether .50 is a known, authorized asset. Given the surrounding compromise, malicious internal reconnaissance is the strongest hypothesis — but distinguishing it from a sanctioned scanner is exactly the kind of question a good analyst keeps open.


So the infection wasn't one host doing one thing. It was two, with different jobs:


192.168.10.43

192.168.10.50

Direction

Outward (external)

Inward (internal)

Targets

338,181 internet IPs

771 internal hosts

Ports

Telnet family (23/2323/2222)

HTTPS + ICMP sweep, then SMB/RPC/FTP

Role

Mirai propagation

Internal reconnaissance

Success rate

17.7%

0.5%


Query 3: External Attackers — SSH Brute Force from Outside


While the internal hosts were scanned, external IPs were hammering the network from outside. This query surfaced completed SSH sessions and their average bytes sent — small, repetitive transfers being the signature of automated credential attempts:


SELECT orig_h, COUNT(*) AS attempts,
  AVG(CAST(orig_bytes AS REAL)) AS avg_bytes_sent
FROM conn_logs
WHERE resp_p = '22'
AND conn_state = 'SF'
GROUP BY orig_h
ORDER BY attempts DESC
LIMIT 15;

Source IP

SSH Sessions

Avg Bytes

Assessment

18.191.216.176

4,168

225

Automated credential stuffing

218.65.30.30

2,875

1,659

Higher-byte brute force tool

58.218.199.133

1,519

1,370

Sequential IP pair — coordinated

182.100.67.4

1,377

1,663

External brute force

192.168.10.43

690

1,021

Infected internal host also brute-forcing SSH

58.218.199.134

84

1,290

Sequential to .133 — same attacker

5.45.85.158

62

20,114

⚠ Massive avg bytes — possible successful intrusion

5.45.86.133

20

738,330

⚠ Extreme transfer — flag for investigation

Three things stand out. First, 58.218.199.133 and .134 are sequential IPs running the same attack — one operator across a small block of machines, not one person at one keyboard. Second, 192.168.10.43 appears here too: the infected internal host wasn't only scanning for telnet victims, it was attempting SSH logins where it found the port open. Third, and most alarming: 5.45.85.158 logged only 62 completed sessions but averaged over 20,000 bytes each — every other brute-forcer averaged under 2,000. And an adjacent IP, 5.45.86.133, averaged over 700,000 bytes across 20 sessions. Those outliers demanded their own look.


Query 4: The Intruder Inside — 5.45.85.158


Brute-force attempts are noise until one succeeds. To separate a failed attacker from a successful one, I pulled everything 5.45.85.158 did — session count, average and total bytes, and the longest single session:


SELECT orig_h, resp_h,
  COUNT(*) AS sessions,
  AVG(CAST(orig_bytes AS REAL)) AS avg_bytes,
  MAX(CAST(duration AS REAL)) AS longest_session,
  SUM(CAST(orig_bytes AS REAL)) AS total_bytes
FROM conn_logs
WHERE orig_h = '5.45.85.158'
GROUP BY orig_h, resp_h;

Metric

Value

What it means

Target

192.168.10.43

Single victim — the same host Mirai infected

Sessions

85

Sustained access, not a one-off

Avg bytes/session

47,034

Interactive activity, not credential-guessing

Longest session

11,888s (3h 18m)

Hands-on-keyboard presence inside the network

Total transferred

~4.0 MB

Real data movement into the host

⚠ Confirmed intrusion

An external IP holding a 3-hour-18-minute session with an internal host, moving roughly 4 MB across 85 sessions, is not a brute-force attempt that failed. It's an attacker who got in and stayed. And the victim, 192.168.10.43, is the same host running the Mirai scanner — the scanning begins just 29 minutes after this intrusion, which points to it as the trigger for .43's compromise. (Notably, the timestamps show two other hosts were already active more than a day earlier — see the timeline below — so this intrusion explains .43, but the network was already compromised before it.)


The Timeline — What Order Did This Actually Happen In?


I assumed the SSH intrusion was patient zero — the first event, from which everything else cascaded. The timestamps said otherwise. Pulling the first-seen time for each activity in the capture reordered the whole story:


Time (UTC)

Event

Host

May 1, 12:34

Internal recon begins

192.168.10.50 — already active at capture start

May 1, 17:38

C2 beaconing begins

192.168.60.22 → 4 external servers

May 2, 16:36

SSH intrusion begins (~28h later)

5.45.85.158 → 192.168.10.43

May 2, 17:05

Mirai scanning begins (+29 min)

192.168.10.43 → 338,181 external IPs

⚠ The assumption that didn't survive

Internal recon and C2 beaconing were running more than a day before the SSH intrusion I could see. The intrusion cleanly explains one host — .43 starts scanning just 29 minutes after the attacker logs in — but it does not explain .50 or .60.22, which were already active. The network was compromised before the intrusion in the logs. (One caveat: these are first-seen-in-capture times, not necessarily true patient-zero — an earlier compromise could predate the logged window entirely.)


Query 5: The Network Looked Perfectly Normal


The most unsettling query was the simplest. While all of this was happening, what did the "healthy" traffic look like?


SELECT service, COUNT(*) AS total
FROM conn_logs
WHERE service != '-'
AND conn_state = 'SF'
GROUP BY service
ORDER BY total DESC
LIMIT 15;

Service

Completed Sessions

Note

http

74,614

Normal web browsing — users had no idea

dns

33,230

Name resolution — the next log to cross-reference

ssl

30,355

Encrypted traffic — C2 can hide here

smtp

9,614

Email — Mirai variants weaponize this for spam

ssh

7,786

Includes the attacker's active sessions

ntp

234

Time synchronization — normal

Seventy-four thousand HTTP sessions completed normally. DNS kept resolving. Email kept flowing. From the outside, nothing looked wrong — while two internal machines were compromised, an attacker held multi-hour sessions inside the network, and a third host beaconed to external controllers on a timer.


That gap — between what the traffic looked like and what was actually happening — is exactly why threat hunting exists. Alerts tell you something fired. Hunting tells you what was already there before anything fired at all.


The Complete Investigation — Both Parts

Part

Query

Finding

Part 1

Connection state breakdown

Mass S0 scanning confirmed

Part 1

S0 by source host

One host responsible for the bulk of scans

Part 1

Port analysis

Mirai signature — ports 23, 22, 2323

Part 1

Clean host comparison

17.7% vs 94.6% success rate (SF-only)

Part 2

Beacon detection (CV)

192.168.60.22 → 4 C2 servers, ~755s cadence

Part 2

Internal recon

192.168.10.50 swept 771 internal hosts

Part 2

SSH brute force

Multiple external IPs attacking simultaneously

Part 2

Intruder deep-dive

5.45.85.158 — 3h18m session, ~4MB, confirmed inside

Part 2

Services query

Network appeared normal while breach was active

Nine queries, re-audited against the raw logs. One complete breach — external intrusion, two infected hosts, and a controller on a timer — built on a Mac, in SQLite, from a single CSV.


🔗 ISC2 CC Domain Connections

This investigation reinforces Domain 1 (Security Principles — confidentiality and availability both at risk), Domain 4 (Network Security — internal reconnaissance, unauthorized access, C2 communication), and Domain 5 (Security Operations — incident identification, evidence collection, escalation). The 5.45.85.158 finding maps directly to IR concepts around containment and forensic preservation.


🪞 Reflection questions I am sitting with


  1. 5.45.85.158 held a session over three hours. In a real environment, at what point should behavioral analytics flag session duration as anomalous?

  2. My first beacon pass over-counted badly. What automated guardrails would keep a production detection rule from making the same mistake at scale?

  3. .50 swept the whole internal network. How would I confirm whether it was a compromised host or an authorized scanner before escalating?

  4. 74,000 HTTP sessions completed normally while the breach was live. How do defenders isolate compromised hosts without disrupting legitimate traffic?

  5. The dns.log is still untouched. Given the four C2 IPs from .60.22, what would I expect those lookups to reveal?


What I Would Do Next


Analyze the DNS log. 33,230 completed DNS sessions sit in dns.log untouched. Cross-referencing those lookups with the four C2 IPs from 192.168.60.22 and the 5.45.85.158 sessions would likely name the botnet infrastructure.


Confirm the status of 192.168.10.50. Its network-wide sweep is either lateral movement or an authorized scanner. A single asset-inventory check would resolve which — and that distinction changes the whole containment decision.


Trace the earlier compromise. The timeline shows internal recon and C2 beaconing active before the SSH intrusion in the logs. Pulling earlier captures — or correlating with DNS and auth logs from before this window — would help find how .50 and .60.22 were first compromised, since the SSH intrusion only explains .43.


🔧 Run it yourself

Every query in this two-part investigation is on GitHub — documented in order, each with its purpose and finding, and reproducible against the public dataset. Clone it, load the log, and run the same hunt: github.com/Tash925/mirai-zeek-detection


💼 From the Analyst's Desk to the SOC Floor


This two-part investigation demonstrates end-to-end incident analysis from a single raw log file — no SIEM, no pre-built alerts, no guided lab. I identified a Mirai infection and verified every finding against the raw data, isolated genuine C2 beaconing from bursty false positives using statistical regularity, mapped internal reconnaissance across the network, surfaced simultaneous external brute-force campaigns, and confirmed a persistent external intruder holding multi-hour SSH sessions inside the network. Just as important: where an early pass over-counted, I caught it, corrected it, and documented why. For SOC Analyst and Cyber Analyst roles, building a detection narrative from raw data — and knowing when your own finding doesn't hold — is the skill that matters most.


Read next

Log Analysis for Beginners — TryHackMe's Intro to Log Analysis, and how structured lab work reframed everything I found in this two-part investigation.

Recent Posts

See All
What I Found In A Zeek Dataset (And How I Found It)

Started with a raw Zeek log — no SIEM, no alerts. Uncovered a Mirai botnet within hours: one internal host behind 500K+ failed Telnet/SSH scans, confirmed by its port signature. A complete threat prof

 
 
 

Comments


Let's learn this together. Have a question, a better query, or just want to say hi? Drop a line below.

© 2026 by DataSec Chronicles. Data-Inspired, Instinct-Driven.    Privacy Policy    Terms & Conditions

bottom of page