Cybersecurity Home Lab: Build One That Generates Alerts
Build a cybersecurity home lab with Active Directory, Sysmon and a SIEM, then run 5 attacks and check the exact event IDs each one should leave.
13 min read

On this page
- What goes in a SOC home lab
- Hypervisor and network
- Build it in this order
- Five exercises and the event IDs they produce
- 1. Password spray, then brute force (4625, 4776, 4740)
- 2. Domain discovery from a shell (4688, Sysmon 1)
- 3. Request a service ticket for an SPN (4768, 4769)
- 4. Install a service that runs a command (7045, 4697)
- 5. Run encoded PowerShell (4104, 4688)
- What exercise 1 looks like on DC01
- Why your SIEM shows nothing after an exercise
- Turn the lab into something you can show
A cybersecurity home lab for SOC practice needs four pieces: a domain controller, a Windows endpoint joined to it, Sysmon, and a SIEM, all on an isolated virtual network. Three virtual machines fit on a 32 GB host. Then prove the lab works by running an attack and confirming the exact event IDs it should produce.
This guide builds the lab, turns on the logging that Windows leaves off by default, and walks through five exercises. Each one comes with the command to run, the event IDs it leaves, the machine they land on and a query to find them. Every exercise uses tools already on Windows, so you don't need an attacker VM or malware samples.
What goes in a SOC home lab
| Machine | Role | Operating system | vCPU | RAM | Disk |
|---|---|---|---|---|---|
| DC01 (10.0.10.10) | Domain controller and DNS for lab.test | Windows Server 2025, evaluation | 2 | 4 GB | 60 GB |
| WS01 (10.0.10.21) | Domain-joined workstation where you run the exercises | Windows 11 Enterprise, evaluation | 2 | 4 GB | 64 GB |
| SIEM01 (10.0.10.30) | Wazuh server, indexer and dashboard on one host | Ubuntu Server 24.04 | 4 | 8 GB | 50 GB |
| Total | 8 | 16 GB | ~175 GB |
The numbers come from the vendors. Microsoft's Windows Server hardware requirements list 2 GB of RAM as the minimum for Windows Server 2025, with 4 GB recommended for the Desktop Experience, and call 32 GB of disk an absolute minimum. Windows 11 needs 4 GB, 64 GB of storage and TPM 2.0, so give the VM a virtual TPM and Secure Boot. Wazuh's quickstart recommends 4 vCPU, 8 GiB and 50 GB for 1 to 25 agents, on Ubuntu 24.04 among other distributions.
Guests that need 16 GB leave nothing for the host, so 32 GB is the comfortable size. On a 16 GB laptop, run the SIEM on a second machine and keep the two Windows VMs local.
Hypervisor and network
Pick the hypervisor you will keep using. VirtualBox (7.2.20 at the time of writing) runs on Windows, macOS and Linux. Hyper-V is built into Windows 10 and 11 Pro or Enterprise on a CPU with SLAT. Proxmox VE (9.2) fits if you have a spare machine to dedicate. All three take snapshots, and snapshots matter more than the brand, because you will break this lab on purpose.
Microsoft's evaluation editions cover the Windows licenses. The Windows Server 2025 evaluation expires in 180 days and the Windows 11 Enterprise evaluation runs for 90.
The same Server evaluation page says the edition must be activated over the internet within the first 10 days to avoid automatic shutdown. A fully isolated host-only network means your domain controller turns itself off a week and a half in. Use a NAT network instead: the VMs reach the internet, nothing on your home LAN can reach them. Take a snapshot once Windows is activated.
Build it in this order
1. Promote the domain controller. Give DC01 a static IP, then:
Install-WindowsFeature AD-Domain-Services -IncludeManagementTools
Install-ADDSForest -DomainName "lab.test" -DomainNetbiosName "LAB" -InstallDns
# After the reboot: five users to attack
"alice","bob","carol","dave","erin" | ForEach-Object {
New-ADUser -Name $_ -SamAccountName $_ -Enabled $true `
-AccountPassword (Read-Host "Password for $_" -AsSecureString)
}2. Join the workstation. Point WS01's DNS at 10.0.10.10 and run Add-Computer -DomainName lab.test -Restart.
3. Turn on the logging. This step decides whether the exercises show anything. For a quick start, set the audit subcategories locally on both Windows machines, then move them into a GPO under Advanced Audit Policy Configuration once you know what you want. A GPO that configures advanced audit policy will overwrite local auditpol settings when it applies.
$subcategories = "Credential Validation","Kerberos Authentication Service",
"Kerberos Service Ticket Operations","Logon","Account Lockout",
"Process Creation","Security System Extension"
foreach ($s in $subcategories) {
auditpol /set /subcategory:"$s" /success:enable /failure:enable
}
auditpol /get /category:*Three more settings are off by default, and each one breaks an exercise if you skip it:
- Command line in 4688. Microsoft's command line process auditing page says the setting is disabled by default. Enable Administrative Templates\System\Audit Process Creation\Include command line in process creation events. Without it, the Process Command Line field in event 4688 stays empty.
- PowerShell script block logging. Enable Computer Configuration\Administrative Templates\Windows Components\Windows PowerShell\Turn on PowerShell Script Block Logging. Per Microsoft's Group Policy reference, it writes script input to the Microsoft-Windows-PowerShell/Operational log.
- Account lockout. The account lockout threshold defaults to 0 in the Default Domain Policy, which means accounts never lock. Microsoft's baselines suggest 10; in a lab, 5 makes the exercise faster:
Set-ADDefaultDomainPasswordPolicy -Identity lab.test -LockoutThreshold 5 `
-LockoutDuration 00:15:00 -LockoutObservationWindow 00:15:004. Install Sysmon on both Windows machines. Sysmon is at v15.22, published September 10, 2026. It needs a configuration file to be useful, so start from a community one such as sysmon-modular or SwiftOnSecurity's sysmon-config, then run sysmon64 -accepteula -i sysmonconfig.xml. Events land in Applications and Services Logs/Microsoft/Windows/Sysmon/Operational.
5. Install Wazuh and its agents. The quickstart is one script on SIEM01 (curl -sO https://packages.wazuh.com/4.14/wazuh-install.sh && sudo bash ./wazuh-install.sh -a), then an agent on DC01 and WS01. Wazuh's log collection docs say the agent reads the System, Application and Security channels by default. Sysmon and PowerShell need their own blocks in C:\Program Files (x86)\ossec-agent\ossec.conf:
<localfile>
<location>Microsoft-Windows-Sysmon/Operational</location>
<log_format>eventchannel</log_format>
</localfile>
<localfile>
<location>Microsoft-Windows-PowerShell/Operational</location>
<log_format>eventchannel</log_format>
</localfile>Restart the agent with Restart-Service -Name wazuh and snapshot all three VMs as your clean baseline.
Wazuh is used here because it is free, runs on one VM and has stock Windows rules. If you would rather use Security Onion, Elastic or another platform, compare them in our open source SIEM guide. The exercises don't change; only the query syntax does.
Five exercises and the event IDs they produce
| # | Exercise | Logged on | Event IDs | ATT&CK |
|---|---|---|---|---|
| 1 | Password spray, then brute force | DC01, Security | 4625, 4776, 4740 | T1110.003 |
| 2 | Domain discovery from a shell | WS01, Security and Sysmon | 4688, Sysmon 1 | T1087.002 |
| 3 | Request a service ticket for an SPN | DC01, Security | 4768, 4769 | T1558.003 |
| 4 | Install a service that runs a command | WS01, System and Security | 7045, 4697 | T1543.003 |
| 5 | Run encoded PowerShell | WS01, PowerShell/Operational | 4104, 4688 | T1059.001 |
Run every exercise from WS01 unless noted. The queries use Get-WinEvent on the machine where the events land, because checking the source first is how you tell a logging problem from a SIEM problem.
1. Password spray, then brute force (4625, 4776, 4740)
Target the domain controller by IP address. That forces NTLM, which makes the events predictable:
# Spray: one password against five accounts
"alice","bob","carol","dave","erin" | ForEach-Object {
net use \\10.0.10.10\IPC$ /user:LAB\$_ "Autumn2026!" 2>$null
net use \\10.0.10.10\IPC$ /delete 2>$null
}
# Brute force: six passwords against one account
1..6 | ForEach-Object { net use \\10.0.10.10\IPC$ /user:LAB\bob "Wrong-$_" 2>$null }DC01 writes a 4625 for each failure: Logon Type 3, authentication package NTLM, the workstation's IP in Source Network Address and Sub Status 0xC000006A, which Microsoft documents as a bad password. Because the DC validated the NTLM credential, it also writes a 4776 with error code 0xC000006A. On bob's fifth bad password you get a 4740, and further attempts return 4625 with Status 0xC0000234, the locked-out code.
The spray locked out nobody, and that is the attack working as intended. MITRE describes password spraying as trying one password across many accounts precisely to avoid lockouts. A 4740 is a brute-force signal. A spray shows up as one source failing against many distinct accounts in a short window, so count users per source:
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625; StartTime=(Get-Date).AddHours(-1)} |
ForEach-Object {
$d = @{}; ([xml]$_.ToXml()).Event.EventData.Data | ForEach-Object { $d[$_.Name] = $_.'#text' }
[pscustomobject]@{ Source = $d.IpAddress; User = $d.TargetUserName }
} |
Group-Object Source |
Select-Object Name, Count, @{n='DistinctUsers'; e={ ($_.Group.User | Sort-Object -Unique).Count }}In Wazuh, the same failures match rule 60122 and the lockout matches rule 60115, "User account locked out (multiple login errors)", at level 9 in the stock Windows ruleset. Search data.win.system.eventID:4625 and data.win.eventdata.ipAddress:10.0.10.21.
Run it a second time against \\DC01 by name. Kerberos takes over, and bad passwords show up on the DC as 4771 (Kerberos pre-authentication failed). The NTLM pair may or may not follow, depending on whether the client falls back. MITRE notes that Kerberos and LDAP spraying is less likely to trigger 4625, so a detection built only on 4625 can miss that version.
2. Domain discovery from a shell (4688, Sysmon 1)
As alice, in PowerShell on WS01:
whoami /groups
net group "Domain Admins" /domain
nltest /dclist:lab.testEach command produces a 4688 in WS01's Security log and a Sysmon event 1. The 4688 gives you New Process Name, Creator Process Name (powershell.exe here) and the command line, if you enabled it. Sysmon 1 adds file hashes and a ProcessGuid that, per the Sysmon docs, is unique across the domain, which makes correlation easier. Expect two process events for the net command: net.exe starts net1.exe to do the work.
Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Sysmon/Operational'; Id=1; StartTime=(Get-Date).AddMinutes(-30)} |
Where-Object { $_.Message -match '\\(net1?|nltest|whoami)\.exe' } |
ForEach-Object { ($_.Message -split "`n" | Select-String '^(Image|CommandLine|ParentImage):') -join ' | ' }If the 4688 CommandLine is empty, the Group Policy setting from step 3 has not applied. Run gpupdate /force and test again before you go looking for the event in the SIEM.
3. Request a service ticket for an SPN (4768, 4769)
Give a service account a service principal name on DC01. setspn -S checks for duplicates first, which is why Microsoft recommends it over -A:
New-ADUser -Name svc_sql -SamAccountName svc_sql -Enabled $true `
-AccountPassword (Read-Host "Password" -AsSecureString)
setspn -S MSSQLSvc/sql01.lab.test:1433 LAB\svc_sqlThen, as alice on WS01, drop your tickets and ask for one to that SPN with klist:
klist purge
klist get MSSQLSvc/sql01.lab.test:1433DC01 logs a 4768 when alice gets a fresh ticket-granting ticket and a 4769 for the service ticket. Both are generated only on domain controllers. In the 4769, Service Name holds the account the ticket was issued for (svc_sql), not the SPN string, and Client Address appears as ::ffff:10.0.10.21. Search for the SPN text and you will find nothing.
Expect Ticket Encryption Type 0x12 (AES256), not 0x17 (RC4). According to Microsoft's 4769 reference, AES keys exist once an account's password has been set at domain functional level 2008 or higher. MITRE's Kerberoasting analytic watches for RC4 tickets, so don't build your lab detection on etype alone. Watch for a user requesting tickets for services they never use, or many SPNs in seconds:
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4769; StartTime=(Get-Date).AddMinutes(-30)} |
ForEach-Object {
$d = @{}; ([xml]$_.ToXml()).Event.EventData.Data | ForEach-Object { $d[$_.Name] = $_.'#text' }
[pscustomobject]@{ Client = $d.TargetUserName; Service = $d.ServiceName; EncType = $d.TicketEncryptionType; From = $d.IpAddress }
} | Where-Object { $_.Service -notlike '*$' -and $_.Service -ne 'krbtgt' }The filter drops computer accounts (they end in $) and TGT renewals, which are most of the 4769 volume on a busy DC. Also note that Wazuh files 4769 under rule 60106, "Windows Logon Success", so the alert title never says Kerberos.
4. Install a service that runs a command (7045, 4697)
From an elevated prompt on WS01:
sc.exe create LabUpdater binPath= "C:\Windows\System32\cmd.exe /c whoami > C:\Windows\Temp\labupdater.txt" start= demand
sc.exe start LabUpdater
sc.exe delete LabUpdatersc.exe wants the space after binPath=. The start returns error 1053, because cmd.exe is not a real service, but the command has already run as SYSTEM. That is why attackers like this pattern.
The System log gets a 7045 from the Service Control Manager with the service name and the image path, including arguments. If you enabled Security System Extension, the Security log also gets a 4697. Sysmon event 1 shows cmd.exe with services.exe as its parent. Wazuh raises rule 61138, "New Windows Service Created", at level 5, mapped to T1543.003:
Get-WinEvent -FilterHashtable @{LogName='System'; Id=7045} -MaxEvents 5 | Format-List TimeCreated, MessageMicrosoft's 4697 guidance suggests alerting when the service file is not under %windir% or Program Files. Your test service lives in System32 and passes that check. The tell is cmd.exe /c inside a service path, which no installer writes.
5. Run encoded PowerShell (4104, 4688)
This is the encoding example from Microsoft's powershell.exe documentation, with a harmless command:
$cmd = 'Get-Service | Where-Object Status -eq "Running" | Select-Object -First 3'
$enc = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($cmd))
powershell.exe -NoProfile -EncodedCommand $encThe process events (4688 and Sysmon 1) show -EncodedCommand followed by a base64 blob. A 4104 in Microsoft-Windows-PowerShell/Operational shows the decoded script, Get-Service | Where-Object ..., in plain text. The process record tells you what was launched; script block logging tells you what ran.
Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104; StartTime=(Get-Date).AddMinutes(-30)} |
Where-Object Message -match 'Get-Service' | Select-Object TimeCreated, Message -First 3Wazuh's stock rule for base64 in PowerShell, 91809, looks for FromBase64String inside the script block text. PowerShell decodes -EncodedCommand before it logs, so the 4104 holds plain text and 91809 stays quiet. Catch this one on the process side, where the command line still carries the flag.
What exercise 1 looks like on DC01
Simulated example generated by SOCSimulator Research. Condensed view of the Security log after exercise 1, not a capture from a real network. Only the first 4776 is shown; DC01 writes one per failure.
Time (UTC) EventID Account Source Detail
09:14:02 4776 alice WS01 Error Code 0xC000006A
09:14:02 4625 alice 10.0.10.21 Logon Type 3, NTLM, Sub Status 0xC000006A
09:14:03 4625 bob 10.0.10.21 Logon Type 3, NTLM, Sub Status 0xC000006A
09:14:04 4625 carol 10.0.10.21 Logon Type 3, NTLM, Sub Status 0xC000006A
09:14:05 4625 dave 10.0.10.21 Logon Type 3, NTLM, Sub Status 0xC000006A
09:14:06 4625 erin 10.0.10.21 Logon Type 3, NTLM, Sub Status 0xC000006A
09:15:40 4625 bob 10.0.10.21 (x4) Sub Status 0xC000006A
09:15:44 4740 bob WS01 Caller Computer Name: WS01
09:15:45 4625 bob 10.0.10.21 (x2) Status 0xC0000234Read it in two blocks. Five accounts, one source, a second apart, no lockout: a spray. One account, a burst, then a 4740: brute force. The 4740 names WS01 rather than an IP address, and Microsoft's 4740 page warns this happens even when the caller is not in your domain.
If you query the raw XML, there is no field called CallerComputerName in the 4740. Microsoft's own sample stores the caller in TargetDomainName, so in Wazuh you search data.win.eventdata.targetDomainName.
Why your SIEM shows nothing after an exercise
When an exercise doesn't appear in the SIEM, work back from the source in this order.
- The event was never written. Run the
Get-WinEventquery on the Windows machine. If nothing comes back, it is the audit policy (auditpol /get /category:*), a GPO that hasn't applied, or one of the default-off settings: command line in 4688, script block logging, a lockout threshold of 0. - The agent doesn't read that channel. Wazuh collects Application, Security and System out of the box. Sysmon and PowerShell need the
localfileblocks from step 5. - The event arrived but was never indexed. Wazuh stores an event in the alerts index only if it matches a rule at or above
log_alert_level, which defaults to 3. The base rule for Sysmon process creation (61603) and the base rule for PowerShell script blocks (91802) are both level 0. Your exercise 2 and 5 events reach the manager and disappear.
There are two fixes. To see everything, turn on archives: set logall_json to yes in ossec.conf, set archives: enabled: true in Filebeat and create a wazuh-archives-* index pattern, as the event logging guide describes. To alert on what matters, write a rule in /var/ossec/etc/rules/local_rules.xml with an ID between 100000 and 120000, the range Wazuh reserves for custom rules, then restart wazuh-manager:
<group name="local,sysmon,">
<rule id="100100" level="6">
<if_sid>61603</if_sid>
<field name="win.eventdata.image" type="pcre2">(?i)\\(net1?|nltest)\.exe$</field>
<description>Lab: domain discovery binary executed</description>
<mitre>
<id>T1087.002</id>
</mitre>
</rule>
</group>That rule is a working detection, written against telemetry you generated and can explain line by line.
Turn the lab into something you can show
Write each exercise up as a short case note: the command, the events it produced, the query that finds them and the benign activity that would trigger the same query. Five of those make a stronger portfolio than screenshots of a dashboard.
Then make the detections portable. Rewrite each query as a Sigma rule so it is not tied to Wazuh. Keep the Windows event IDs cheat sheet open while you work, and when an event makes no sense, go through it field by field with the guide to reading Windows event logs.
A home lab is quiet. You run one attack and that attack is all there is. A SOC queue buries the same 4625 burst under hundreds of benign alerts, and picking it out is a separate skill. When you want to practice that part, the SOCSimulator SIEM console puts these event types in a noisy alert queue, so you are triaging instead of reproducing.
Frequently asked questions
How much RAM do I need for a cybersecurity home lab?
For the three-machine lab in this guide, plan on a 32 GB host. The guests alone add up to 16 GB: 4 GB for the domain controller, 4 GB for the Windows 11 endpoint (Microsoft's minimum) and 8 GiB for an all-in-one Wazuh server, which is what Wazuh recommends for up to 25 agents. A 16 GB machine works if the SIEM runs on a second box, such as an old desktop or a mini PC.
Can I build a cybersecurity home lab for free?
The software, yes. VirtualBox, Hyper-V on Windows Pro or Enterprise, and Proxmox VE cost nothing, and so do Sysmon and Wazuh. Microsoft's evaluation editions cover Windows: Windows Server 2025 runs for 180 days and Windows 11 Enterprise for 90. Script the build so you can rebuild when they expire. The only real cost is a host with enough memory.
Do I need Active Directory in a SOC home lab?
If you want to practice Windows detections, yes. Kerberos ticket events such as 4768 and 4769 are generated only on domain controllers, and account lockouts, NTLM validation and most credential attacks you will triage at work involve domain accounts. A lab without a domain controller can teach you Sysmon and process events, but not the identity half of a SOC queue.
Why don't my lab attacks show up in Wazuh?
Check three things in order. First, confirm the event exists on the Windows machine with Get-WinEvent; if it does not, the audit policy or a default-off setting is the cause. Second, confirm the agent collects that channel, because Sysmon and PowerShell logs need extra localfile blocks. Third, remember that the alerts index only stores events that match a rule at level 3 or higher, and the base rules for Sysmon process creation and PowerShell script blocks are level 0.
Should I put Kali Linux in my home lab?
You do not need it for the five exercises here, because built-in Windows tools generate every event. Add an attacker VM when you want to see attacks from a machine outside the domain. The logs change in useful ways: Microsoft notes that the 4740 lockout event reports a computer name rather than an IP address, even when that computer is not in your domain.
Nerea writes SOCSimulator's guides for people starting or growing a career in security operations: how Windows authentication shows up in the logs (NTLM vs Kerberos), and which tools a T1, T2 or T3 analyst opens first. Each guide is built around the questions a new analyst actually has on shift, with real event IDs and queries to practice on.


