Following a Failed Logon Through a SIEM
A Wazuh lab built to understand how an alert is born, and how an analyst decides what it means.
Wazuh 4.14.8 · Windows 10 · Windows Security Events · VirtualBox · Kali Linux
Status: proof of concept, Day 2. Built and tested by me on an isolated host-only network. This page grows as the lab does.
The question behind the project
Most of my practice so far has been offensive. I know how a password attack, a broken authentication flaw or a stolen token works from the attacker’s side. What I hadn’t done was look at the same activity from the other chair: what does it look like to the person watching the screen, and how do they decide what it means?
So I avoided the usual route of installing a SIEM and clicking through its dashboards. I picked one small, common event, a failed Windows logon, and followed it end to end, from the moment Windows records it to the decision an analyst makes about it.
The workflow I wanted to practice:
Collect telemetry → Detect → Triage → Investigate → Document a disposition
The goal was never just “generate an alert”. It was to verify the whole path, read the event correctly, test a custom rule honestly, and investigate what came out of it.
Where this stands
| Done through Day 2 | Still to do |
|---|---|
| Working Windows-to-Wazuh telemetry pipeline | Frequency-based detection |
| Reading Event 4625 fields correctly | Automated failed-then-successful correlation (rule) |
| Custom rule creation and validation | Sysmon, PowerShell and privilege-related detection |
| Alert triage and evidence-based disposition | |
| Remote authentication attack from Kali | |
| Failed-then-successful login correlation, by hand | |
| Catching a SIEM mislabeling its own alert |
The lab
Two virtual machines on a host-only network, isolated from everything else. Kali Linux is the analyst and attack host and joins the action in the next stage.
Kali Linux host
192.168.56.1
analyst / attack host (active since AUTH-002)
|
vboxnet0 host-only
192.168.56.0/24
/ \
▼ ▼
Windows 10 VM Wazuh server VM
DESKTOP-IN042K6 wazuh-server
192.168.56.104 192.168.56.105
(Agent 001)
|
| TCP/1514
▼
Wazuh Manager → Indexer → Dashboard → Analyst
| Attribute | Value |
|---|---|
| Endpoint | DESKTOP-IN042K6, Windows 10, 192.168.56.104 |
| Wazuh manager | 192.168.56.105, version 4.14.8 |
| Agent | ID 001, transport TCP/1514 |
| Network | VirtualBox host-only, 192.168.56.0/24 |
Decisions that shaped it
Sizing. The physical host has about 15 GiB of RAM, so I gave the Wazuh VM 2 vCPU and 4 GB rather than the appliance default. That left room for the Windows endpoint and Kali. It also pushed me toward one well-understood endpoint instead of a thin imitation of an enterprise network. I’d rather size a lab to its purpose than run every default.
Isolation. I moved the environment onto VirtualBox’s host-only network so the attack testing in the next stage stays contained and can’t reach anything outside the lab.
Verify the pipeline before trusting a detection. Before writing a single rule, I checked the agent logs: the endpoint had received its key, connected to the manager and reported online. A detection is only worth trusting if the telemetry beneath it works. An alert that never fires because events never arrived looks exactly like a quiet network.
The endpoint was reporting several sources: Windows Security, Application and System events, Syscollector, Security Configuration Assessment, File Integrity Monitoring, Rootcheck and agent health. For this case, Windows Security events were the one that mattered.
How the event travels
Before this, “a SIEM collects logs” was a sentence I could repeat. Following one event made it concrete.
Windows Security log → Wazuh agent → Manager (decode → rules) → Indexer → Dashboard
- Windows writes Event 4625. Every failed logon is recorded in the Security log, with fields describing who, where from, how and why.
- The Wazuh agent ships it to the manager over TCP/1514.
- The manager decodes it. The raw event is split into named fields (target account, logon type, source address, status codes), so rules can test each one directly instead of searching text.
- Rules are evaluated. If a rule’s conditions match, the event becomes an alert with a rule ID and a severity level.
- The indexer stores it and the dashboard displays it, where an analyst can search, filter and investigate.
Steps 3 and 4 changed how I think. A SIEM isn’t magic pattern recognition. It’s structured fields plus rules someone wrote. That means detection quality depends on two things: what the rules look at, and how well they’re tuned. A SIEM with weak rules is a very expensive log viewer.
Reading the event
Here is one of my failed logons:
Event ID: 4625 (an account failed to log on)
Target user: Eddie
Logon type: 2 (interactive, at the keyboard)
Source: 127.0.0.1 / ::1 (loopback, the machine itself)
Status: 0xC000006D (logon failed)
Sub-status: 0xC000006A (account exists, wrong password)
An analyst reads this like a short sentence: someone tried to log in locally to a real account and got the password wrong. The event also carries the hostname, process information and authentication package, but three fields do most of the work.
| Field | Value | What it tells the analyst |
|---|---|---|
| Logon type | 2 | Interactive, at the machine. Consistent with local activity |
| Logon type | 3 | Network. Relevant when investigating remote authentication |
| Logon type | 10 | Remote desktop. Relevant when investigating RDP |
| Sub-status | 0xC000006A |
The account is real and the password was wrong |
| Sub-status | 0xC0000064 |
The account doesn’t exist, which points toward username guessing |
| Source address | loopback here | Where it came from. Here, the endpoint itself |
Why a single 4625 proves very little
People mistype passwords. One failure, or even a handful, is ordinary noise. What turns failures into something worth an analyst’s time is context: how many, how fast, from where, against which accounts, and what happened afterwards. These values shouldn’t be treated as standalone indicators of compromise. They have to be read together.
In my case, the interactive logon type and the loopback source mattered because together they showed the activity came from the endpoint itself, not from another system. This is where my offensive background helps. I know a real attack would show a network or remote logon type, an outside source and a pattern of repeats, so I know which fields to check first.
What different attacks look like in this data
| Pattern | Shape in the telemetry | What the analyst looks for next |
|---|---|---|
| Brute force | Many failures against one account | Rate of attempts, source, whether one eventually succeeds |
| Password spraying | A few failures against many accounts from one source | Spread of accounts, same source across hosts, slow timing |
| Mistyped password | A few failures, then a success, from the user’s usual source | Usual source and hours, no spread, no follow-on activity |
From a native rule to mine
Wazuh already detects this event with a built-in rule, 60122. So my first decision was not to write a rule from scratch, but to check what the platform already gave me and build on it. I then wrote a rule that inherits from it:
<rule id="100101" level="7">
<if_sid>60122</if_sid>
<description>Windows Authentication Failure - SOC Lab Detection</description>
<group>windows,authentication_failed,custom_detection,</group>
</rule>
Reading the rule
| Element | What it does here |
|---|---|
id="100101" |
A custom rule ID, in the range Wazuh reserves for user-written rules |
level="7" |
Severity on Wazuh’s 0 to 15 scale, which decides how prominent the alert is |
if_sid |
Makes this a child rule: it only fires when parent rule 60122 has already matched |
description |
The text an analyst sees on the alert |
group |
Tags that let me filter these lab alerts separately |
| It fired 14 times on real events, which confirmed that a custom rule can load, match and alert in my pipeline. |
What this rule is not
I want to be straightforward: this is a labelling layer, not a mature brute-force detection. It inherits the native rule, adds a custom group and assigns level 7. That’s all. It does not:
- count failures within a time window
- correlate events by source
- tell an isolated failure from a repeated attack
- link failures to a later successful login
- reduce noise through thresholds
So it alerts on every single failure and raises the severity each time. On a real network, that would bury an analyst in noise, and alert fatigue is how real attacks get missed. I’m keeping it for two reasons. It proved the mechanics of loading and triggering custom rules against endpoint telemetry, and its weakness showed me exactly what a good rule needs. Relabelling an existing alert is not the same as building a detection, and improving it is the next thing I’m doing.
Case AUTH-001: repeated failed logons
The 14 alerts became a case. I worked through it the way an L1 analyst would: what happened, to whom, from where, why, and is it still happening?
| Question | Finding |
|---|---|
| What happened? | 14 failed interactive logons |
| Which account? | Eddie, a known local account |
| Which host? | DESKTOP-IN042K6 |
| From where? | Loopback: the machine itself |
| How? | Logon type 2, interactive |
| Why did it fail? | Sub-status 0xC000006A: valid account, wrong password |
| Repeated? | Yes, 14 failures |
| Any success? | No. Zero Event 4624 events in the examined window |
| Malicious? | Not established by the evidence |
Disposition: benign, expected lab activity
Several things support that call:
- The source was the machine itself, not a remote host.
- The logon type was interactive.
- The account was a known local account.
- No successful authentication was observed.
- No relevant follow-on activity appeared in the telemetry I examined.
The limit of this case
I generated these failures myself, so I knew the answer before I started. That matters. This case shows I can read the evidence and structure an investigation, but it doesn’t yet test judgement when the answer is unknown. That is deliberate, and it’s the reason the next case exists.
What would change my mind
If the same pattern appeared outside a lab, I’d want more evidence before calling it benign, and I’d escalate on any of these:
- a source address other than loopback
- logon type 3 (network) or 10 (remote desktop)
- a successful logon (4624) right after a run of failures, which could mean a password was guessed
- multiple accounts targeted from one source, or the same source hitting several hosts
- privileged-account involvement
- follow-on activity such as account creation or privileged actions
- process and command-line telemetry showing what ran afterwards
- the timing and frequency of the attempts
The analyst’s path through an alert
Alert fires
├─ Is the source local or remote? (source address, logon type)
├─ Is the account real and expected? (sub-status, account name)
├─ Is it repeated, and how fast? (count, timing)
├─ Did anything succeed? (4624 after the failures)
├─ Did anything happen afterwards? (privileges, new accounts, processes)
└─ Decide: benign / escalate / need more evidence, and write down why
Escalation note
The handover an L1 analyst would write for this case:
Ticket: AUTH-001
Severity: Low (lab)
Summary: 14 failed interactive logons against account "Eddie"
on DESKTOP-IN042K6
Source: Loopback (127.0.0.1 / ::1)
Evidence: Event 4625, logon type 2, sub-status 0xC000006A
0 successful Event 4624 events
Assessment: Expected lab activity, self-generated
Disposition: Benign
Escalate: No
Would change: Remote source address, network or remote-interactive
logon type, successful authentication following
repeated failures, or relevant follow-on activity
An alert means an event needs analysis. It does not, by itself, establish malicious activity.
Case AUTH-002: a real source, an unknown answer
AUTH-001 proved I could read the evidence and structure an investigation. It didn’t prove I could do it when I didn’t already know the answer — I generated those failures myself, on the machine itself, and knew going in that nothing had succeeded. AUTH-002 was built to close that gap.
The setup
From Kali, 192.168.56.1, I ran a credential attack against the Windows endpoint over SMB using NetExec and a 10-password wordlist against the account Eddie. Nine of the passwords were wrong. One, buried in the middle of the list rather than placed first or last, was correct. I deliberately didn’t track which position it was in while the attack ran, so the moment of truth happened in the Wazuh dashboard, not in my terminal.
nxc smb 192.168.56.104 -u Eddie -p auth002_wordlist.txt
Port scanning beforehand showed SMB (445) open and RDP (3389) filtered by the Windows Firewall, which decided the attack vector for me — SMB over RDP was the honest choice here, not a preference.
Reading the result
Filtering the dashboard to the relevant rules and sorting oldest-first gave a complete sequence:
| # | Time | Event |
|---|---|---|
| 1 | 12:56:23.883 | 4625 failure |
| 2 | 12:56:33.926 | 4625 failure |
| 3 | 12:59:15.807 | 4625 failure |
| 4 | 12:59:15.867 | 4625 failure |
| 5 | 12:59:15.882 | 4625 failure |
| 6 | 12:59:15.898 | 4625 failure |
| 7 | 12:59:15.917 | 4625 failure |
| 8 | 12:59:15.945 | 4625 failure |
| 9 | 12:59:15.945 | 4625 failure |
| — | 12:59:15.992 | 4624 success |
Nine failures, one success, and the success is the last event in the sequence — nothing failed afterward. That alone is a cleaner correlation than I expected to get on a first attempt.
The field that made it a different case from AUTH-001
| Field | AUTH-001 | AUTH-002 |
|---|---|---|
| Logon type | 2 (interactive) | 3 (network) |
| Source | 127.0.0.1 / ::1 (loopback) | 192.168.56.1 (external host) |
| Sub-status | 0xC000006A (wrong password) | 0xC000006A (wrong password) |
| Outcome | No success observed | Success observed, 47ms after the last failure |
The sub-status is identical in both cases — a wrong password looks the same on its own either time. What changes the read entirely is the logon type and source: AUTH-001 was the machine talking to itself, AUTH-002 was a remote host guessing a password and eventually getting it right.
A pattern inside the pattern
The nine failures aren’t evenly spaced. The first two land about ten seconds apart — consistent with a tool establishing its connection — then there’s a gap of roughly two and a half minutes, then the remaining seven failures and the success all land within about 150 milliseconds of each other. That’s not how a person typing wrong passwords behaves. A human re-entering a password shows irregular, multi-second gaps throughout. A sub-150ms cluster across seven attempts is a machine-speed signature.
I’m not turning this into a formal detection yet — I don’t have a rule that measures inter-event timing — but it’s worth stating as evidence I noticed, because the ability to read tool behavior from timestamp patterns is part of what separates triage from pattern-matching.
Where the SIEM’s own label was wrong
Wazuh’s native rule for this successful logon (rule 92652) describes it as:
“Successful Remote Logon Detected… NTLM authentication, possible pass-the-hash attack,” mapped to MITRE T1550.002 (Pass the Hash).
That description doesn’t match what happened. Pass-the-hash means authenticating with a stolen password hash, without ever knowing the plaintext. What the evidence actually shows is nine sequential plaintext password guesses culminating in a correct one — that’s brute force (T1110) leading to valid account access (T1078), not pass-the-hash. Wazuh’s rule appears to label any successful NTLM network logon this way, regardless of how the credential was obtained, which makes it a generic flag rather than a specific finding.
This is the same lesson from AUTH-001 — an alert is a question, not a verdict — showing up in a harder form. In AUTH-001 I had to question whether a failure meant anything. Here I had to question a label the SIEM had already assigned with a MITRE technique attached, which is easy to accept at face value precisely because it looks authoritative. Checking raw evidence against a rule’s own description, not just against raw telemetry, is the addition this case makes to how I read an alert.
Disposition: escalate — confirmed account compromise via credential guessing (lab-controlled)
Supporting evidence:
- The source was external to the endpoint (
192.168.56.1), not local — unlike AUTH-001 - The logon type was network (3), not interactive
- A clear pattern of repeated failures against one account, immediately followed by a success
- The success and the final failures share the same source and sit within 50 milliseconds of each other
- The native rule’s “possible pass-the-hash” label does not match the evidence; the case is better described as brute force leading to valid account access, which I’ve recorded as a correction to the automated classification rather than an agreement with it
Escalation note
Ticket: AUTH-002
Severity: High (lab)
Summary: 9 failed network logons against account "Eddie" on
DESKTOP-IN042K6, followed by 1 successful network logon
from the same external source
Source: 192.168.56.1 (external, non-loopback)
Evidence: Event 4625 x9, logon type 3, sub-status 0xC000006A
Event 4624 x1, logon type 3, same source, 47ms after
the final failure
Assessment: Successful brute-force / credential-guessing attack —
account Eddie compromised from an external host.
Native rule 92652 labeled this "possible pass-the-hash"
(T1550.002); the evidence instead supports brute force
(T1110) -> valid accounts (T1078). Recorded as an
analyst correction to the automated classification.
Disposition: Malicious — confirmed compromise (lab-controlled)
Escalate: Yes. In a live environment: force a password reset,
lock the account pending review, and check for
follow-on activity from this session (new processes,
new accounts, persistence)
What AUTH-002 adds that AUTH-001 couldn’t
AUTH-001 showed I can read a single event’s fields correctly and structure an investigation around them. AUTH-002 required something AUTH-001 didn’t: making a call when I hadn’t pre-decided the answer, correlating two different event types (4625 and 4624) by time and source rather than reading them in isolation, and catching a case where the platform’s own labeling was misleading rather than just absent. Read together, the two cases form a pair — one proves process, the other proves judgment under uncertainty.
What I took from it
- An alert is a question, not a verdict. Context decides the answer.
- Detection quality comes from what a rule examines, not from owning a SIEM.
- The most useful evidence is often not the failure itself but what happens next.
- Knowing how an attack works tells me which fields to check first. It doesn’t replace checking the evidence.
- A detection is only as reliable as the telemetry pipeline under it, so verify the pipeline first.
- Being honest about what a project does not prove makes the parts it does prove more believable.
- A rule’s description can be wrong even when its alert is right. Checking a label against the evidence matters as much as checking a raw event against the evidence.
What’s next
AUTH-002 closed the “unknown answer” gap. Each remaining step answers a weakness that’s still open.
1. A rule that looks at more than one event
I’ll replace or extend rule 100101 with logic that:
- counts repeated failures from one source within a defined time window
- flags a successful login that follows those failures
- uses severity levels that match the risk
- is compared against Wazuh’s existing rules, so I can state what mine adds
Wazuh’s rule engine supports frequency and time-window conditions and chaining rules together, which is the route I’ll explore. I’ll test the result with Wazuh’s rule-testing tool before treating it as validated. This rule should catch, automatically, what I had to notice by hand in AUTH-002.
2. MITRE ATT&CK mapping
I’ll map the relevant authentication techniques based on what AUTH-002 actually shows and what Wazuh actually tags — including documenting where Wazuh’s own tagging was wrong, as it was with rule 92652 — not to make the project look more advanced.
3. Richer telemetry
Once authentication detection is solid, the lab grows one concrete step at a time:
Authentication → Correlation rule → Sysmon
→ Process execution → PowerShell → Privilege-related activity
→ Multi-stage investigation → Detection tuning → Incident report
Logon events only show the front door. Sysmon and PowerShell telemetry will show what happens once someone is inside, which is what turns an authentication alert into an incident.
Current limits
For clarity, this project does not yet demonstrate frequency-based detection, automated failed-then-successful correlation, Sysmon or PowerShell detection, privilege-related detection, multi-stage correlation, practical ATT&CK mapping, a full incident-response lifecycle or production-scale SIEM engineering. These are treated as planned work, not implied capabilities.
Outcome so far
Day 1 produced a working Windows-to-Wazuh telemetry pipeline, a complete first investigation from event to disposition, and a clear view of the gap between a custom rule that fires and a detection that helps. Day 2 added a real remote attack with an unknown outcome, a correct correlation of failures to a success, and a documented case where the SIEM’s own alert label was wrong and I caught it. The lab is being built incrementally so that every capability I claim is backed by real telemetry and real investigation evidence.
Next milestone: a custom detection rule that counts repeated failures in a time window and automatically flags a success that follows them.