Master Checklists

windowsServer Windows Server Master Checklist

AD DS, DNS, DHCP, GPO, IIS, and server-role hardening for Domain Controllers and member servers.

0 / 0 checked

A comprehensive, competition-ready checklist for hardening a Windows Server image (2008/2008 R2/2012/2012 R2/2016/2019) in CyberPatriot โ€” covering Active Directory Domain Services, DNS, DHCP, File/Print, IIS, and general member-server hardening. This document assumes the image may be a Domain Controller, a domain-joined member server, or a standalone server, and calls out where the correct approach differs.

Golden rule: Read the README on the Desktop FIRST. It tells you the scenario, the server's required roles (AD DS/DNS/DHCP/File/Print/Web), which users/groups must exist, and any instructions that override the generic advice below. If the README says a role or account is required, do not remove/disable it just because a checklist says "unnecessary." Scoring engines reward fixing vulnerabilities AND penalize breaking required functionality โ€” breaking AD, DNS, DHCP, or a required share can cost far more than any single hardening item is worth.

About the CIS references in this document: Where relevant, "Things to try / extra points" items below are cross-referenced against the CIS Microsoft Windows Server Benchmark. CIS publishes separate benchmark profiles for a Domain Controller and a Member Server โ€” a tip is flagged as DC-specific or Member-Server-specific where that distinction matters. Controls are also tagged Level 1 (baseline hardening, low risk of breaking functionality โ€” safe to apply broadly) or Level 2 (stricter/defense-in-depth, higher chance of affecting required functionality โ€” apply carefully and test). Controls are referenced by name/category rather than exact control-ID numbers, since numbering shifts between benchmark versions and OS releases โ€” treat these as directional guidance, not a substitute for the actual benchmark document if your team has access to it.


Table of Contents#

1. Initial Recon & README#

  • Read the Desktop README completely before making any changes. Screenshot or copy it somewhere safe (it sometimes disappears or the scoring engine resets desktop contents).
  • Identify the server's role(s) from the README AND from the OS itself โ€” a server can hold multiple roles simultaneously (e.g., DC + DNS + DHCP, or File + Print):
    • Active Directory Domain Services (Domain Controller)
    • DNS Server
    • DHCP Server
    • File and Storage Services / File Server
    • Print and Document Services
    • Web Server (IIS)
    • Remote Desktop Services (RDS)
  • Note the required users, groups, and OUs mentioned in the README โ€” these must survive your cleanup.
  • Note any specific software required to remain installed (this is common on server images tied to a "business scenario").
  • Identify the OS version (Server 2008/2008 R2/2012/2012 R2/2016/2019) since UI paths differ โ€” see Section 21.
  • Check the hostname, domain name, and IP configuration match what the README expects โ€” don't break networking by "fixing" a static IP the DHCP/DNS role depends on.

Things to try / extra points#

  • Run Get-WindowsFeature | Where-Object Installed immediately โ€” this is the fastest way to see every installed role/feature before you form an opinion about what's "supposed" to be there.
  • Run systeminfo and hostname to confirm OS build/edition and name match the README.
  • Check ipconfig /all and compare DNS server, gateway, and DHCP-assigned vs. static addressing against what a DC/DNS/DHCP box should have (DCs and DNS/DHCP servers almost always need a static IP pointing at themselves for DNS).
  • If unsure whether the box is a DC, run whoami /groups (look for Enterprise Domain Controllers) or Get-ADDomainController โ€” if that cmdlet works, ActiveDirectory module is present and it's likely a DC or has RSAT tools installed.
  • One-stop alternative: Get-ComputerInfo bundles OS build, edition, domain role, and boot time into a single object โ€” faster than running systeminfo + hostname + ipconfig separately when you just need a quick sanity check.
  • Fallback if Get-WindowsFeature errors out (mainly a 2008 R2 issue where the module isn't auto-loaded): Import-Module ServerManager first, or fall back entirely to the Server Manager GUI's Roles/Features summary if PowerShell tooling isn't cooperating.
  • Verification habit: whatever you learn in this recon pass (roles, hostname, IP config), save it to a text file outside the Desktop (e.g., C:\Users\Public\recon_baseline.txt) โ€” you'll want to diff against this baseline in the Final Sweep to confirm nothing required was accidentally broken.

Tip: Do the recon pass with a notepad file open (not saved on the Desktop where a reset might wipe it) tracking: server roles found, accounts found, current firewall/policy state. You will reference this constantly.

๐Ÿ“ธ SNAPSHOT CHECKPOINT โ€” take one right now, before you touch anything#

Once you've read the README and identified the server's role(s), take a VM snapshot in the hypervisor (VirtualBox: Machine โ†’ Take Snapshot; VMware: VM โ†’ Snapshot โ†’ Take Snapshot; Hyper-V Manager: right-click the VM โ†’ Checkpoint) before making a single configuration change. This is a host-level safety net, separate from Windows' own System Restore โ€” it's the fastest way to recover if a later change breaks AD, DNS, or networking beyond repair. Do this now; don't wait.


2. Forensics Questions Strategy#

Server images frequently include forensics questions (text files asking you to identify a misconfiguration, an unauthorized account, a malicious file, etc.) that award points independently of system hardening.

  • Find and read all forensics question files before changing anything they might be asking about โ€” changing state can make the answer harder to find or invalidate it.
  • Answer directly in the file/format requested (usually a .txt on the Desktop) โ€” save it before moving on.
  • Common forensics themes on server images: unauthorized AD accounts, suspicious scheduled tasks, a stale/rogue DHCP or DNS entry, a suspicious share, a misconfigured GPO, or a specific log entry (Event ID) showing an intrusion.

Things to try / extra points#

  • Before making destructive changes, take note of ("snapshot") current state so you can answer forensics questions accurately:
    powershell
    Get-ADUser -Filter * -Properties * | Export-Csv C:\Users\Public\ad_users_snapshot.csv -NoTypeInformation
    Get-LocalGroupMember Administrators | Export-Csv C:\Users\Public\local_admins_snapshot.csv -NoTypeInformation
    Get-WindowsFeature | Where-Object Installed | Export-Csv C:\Users\Public\roles_snapshot.csv -NoTypeInformation
    
    What this pipeline does
    Get-ADUser -Filter * -Properties * means "get every AD user account, with every available property field" (by default PowerShell only shows a handful of common fields). The | (pipe) sends that full list into Export-Csv, which writes it out to a spreadsheet-readable file โ€” this is essentially "dump the entire user database to a file I can review or open in Excel."
    Expected result: Three CSV files appear in C:\Users\Public\. No console output if run without -Verbose; each CSV, opened in Notepad or Excel, shows one row per user/local admin/installed feature with the properties named. **If it fails:** Get-ADUser errors with "The term 'Get-ADUser' is not recognized" โ€” the ActiveDirectory module isn't loaded; run Import-Module ActiveDirectory first, and if that also fails, install RSAT with Add-WindowsFeature RSAT-AD-PowerShell (this cmdlet only exists/works on a Domain Controller or a member server with RSAT tools added โ€” it will never work on a workgroup box with no AD). Get-LocalGroupMember can throw on very old builds where the cmdlet doesn't exist yet โ€” fall back to net localgroup Administrators. "Access denied" on the CSV path means you're not running PowerShell as Administrator.
  • If a forensics question asks "who is the unauthorized user," cross-reference Get-ADUser -Filter * -Properties WhenCreated,LastLogonDate sorted by WhenCreated โ€” recently created accounts are prime suspects.
  • Check Get-EventLog Security -InstanceId 4720 -Newest 20 (or Get-WinEvent -FilterHashtable @{LogName='Security';Id=4720}) for recent "user account created" events โ€” gives you exact timestamps and the account that created the suspicious user.
  • Fallback if Get-EventLog isn't available or feels slow: Get-WinEvent is the modern replacement and works identically across all supported Windows Server versions โ€” prefer it going forward; Get-EventLog is deprecated in newer PowerShell versions and may not exist at all on non-Windows PowerShell hosts.
  • Edge case โ€” anchor suspicious activity to the boot time: (Get-CimInstance Win32_OperatingSystem).LastBootUpTime tells you when the machine last rebooted. Anything timestamped before that (in Run keys, scheduled tasks, or recently modified files) survived a reboot and is more likely to be deliberate persistence rather than something you or a teammate did during this session.
  • Cross-check recently modified files, not just recently created ones โ€” an attacker editing an existing legitimate script is easy to miss if you only look for new files:
    powershell
    Get-ChildItem C:\ -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -gt (Get-Date).AddDays(-3) } | Select-Object FullName, LastWriteTime | Sort-Object LastWriteTime -Descending
    
    Expected result: A table of FullName/LastWriteTime pairs, newest first โ€” expect it to be long and noisy (Windows Update, temp files, log rotation all touch files constantly), so scan for anything in an unusual location (C:\Users\Public, C:\ProgramData, C:\Windows\Temp, a user's AppData\Roaming) rather than expecting a short clean list. If it fails: Runs very slowly or appears to hang โ€” a full C:\ recursive scan on a server with large data volumes or many files can take minutes; narrow the path (e.g., C:\Users, C:\ProgramData) if you have a hunch where to look. Individual "Access denied" errors on system folders are expected and suppressed by -ErrorAction SilentlyContinue โ€” don't mistake a slow-but-running scan for a frozen one.
  • Remember: server changes are more disruptive than client changes. Document your baseline before you start disabling/removing things, in case you need to prove what was "before" vs "after" for a forensics answer.

Encoded / Obfuscated Data#

Forensics questions sometimes hide their real content behind an encoding rather than plain text โ€” a real CyberPatriot practice round included a base64-encoded message on the desktop. Recognize the pattern, then decode:

Looks like Likely encoding Tell
SGVsbG8gV29ybGQh Base64 Only letters/digits/+//, often padded with =/==, length a multiple of 4
48656c6c6f20576f726c6421 Hex Only 0-9 and a-f, always an even number of characters
Uryyb Jbeyq ROT13 / Caesar shift Garbled but word lengths/spacing/punctuation match real text
Hello%20World%21 URL encoding % followed by two hex digits
powershell
# Base64 decode โ€” offline, no internet needed
[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("SGVsbG8gV29ybGQh"))

# Base64 decode a file with certutil (no PowerShell needed)
certutil -decode input.b64 output.txt

# Hex decode
-join (("48656c6c6f20576f726c6421") -split '(?<=\G..)(?!$)' | ForEach-Object { [char][Convert]::ToInt32($_,16) })

# ROT13 decode (self-inverse)
function Decode-Rot13($s) {
    -join ($s.ToCharArray() | ForEach-Object {
        if ($_ -cmatch '[a-z]') { [char]((([int][char]$_ - 97 + 13) % 26) + 97) }
        elseif ($_ -cmatch '[A-Z]') { [char]((([int][char]$_ - 65 + 13) % 26) + 65) }
        else { $_ }
    })
}
Decode-Rot13 "Uryyb Jbeyq"
Expected result:
  • The Base64 example prints Hello World!; the hex example prints Hello World!; the ROT13 function call prints Hello World.
  • All run with zero internet access, using only built-in .NET types (System.Convert, System.Text.Encoding) and PowerShell string operators.
If it fails:
  • FromBase64String throws "Invalid length for a Base64 char array" โ€” the string is missing padding = characters or has line breaks/whitespace copied in from the source file; strip whitespace with .Trim() or -replace '\s','' before decoding.
  • The hex one-liner throws if the string has an odd number of characters or non-hex characters โ€” verify with $s.Length % 2 -eq 0 first.
  • certutil -decode requires the input to be pure standard Base64 (no line breaks longer than certutil expects is actually fine, but a non-Base64 file will produce "CertUtil: -decode command completed successfully" and still write garbage output โ€” open the result and sanity-check it rather than trusting the exit message alone).
  • Check likely hiding spots beyond the obvious desktop .txt file: SYSVOL scripts, a GPO description field, a Scheduled Task's description, an AD user's Description/Notes attribute โ€” all of these can hold an encoded string on a server-flavored forensics question.
  • Try base64 first if unsure โ€” it's by far the most common encoding used, and a failed decode is instant confirmation to try something else.

3. Server Roles & Features Audit (Do This Before Touching Services)#

Order of operations matters. Server roles depend on specific underlying services. If you go straight to "Services Hardening" and start disabling things that look unfamiliar, you may cripple AD DS, DNS, DHCP, or File Services and lose far more points than you gain. Always inventory roles/features FIRST via Server Manager or Get-WindowsFeature, understand what's actually required by the README, and only then touch individual services.

  • Open Server Manager โ†’ Manage โ†’ Add/Remove Roles and Features (or Roles/Features panes on 2008 R2).
  • Compare the installed roles list against the README's stated required roles.

๐Ÿ“ธ Snapshot checkpoint: Removing a role can be disruptive and isn't always cleanly reversible without a restart or reinstall. If you haven't already taken a snapshot in this session, take one now โ€” right before you start removing roles/role services โ€” so a wrong removal is a one-click revert instead of a rebuild.

  • Remove roles/role services that are NOT required and NOT mentioned in the README (this is almost always worth points โ€” "Rogue Roles").
  • Under Features, remove anything non-mission-critical. A default roleless install typically has zero extra features โ€” anything installed beyond defaults is suspect.
  • For roles you ARE keeping, expand Role Services and remove unnecessary sub-components (e.g., keep DNS but not other bundled extras; keep IIS but remove FTP/CGI/ASP if not used).
  • Run the Best Practices Analyzer (BPA) for each installed role (Server Manager โ†’ select role โ†’ Summary pane โ†’ Best Practices Analyzer โ†’ Start BPA Scan). Review items marked Noncompliant; understand each setting before changing it โ€” don't blindly "fix" everything BPA flags, since some Noncompliant items are intentional to the scenario.
  • Review Configure IE Enhanced Security Configuration (IE ESC) โ€” set both Administrators and Users to On. Do this AFTER any internet-based downloads/updates you need, since ESC aggressively blocks browsing.
  • Review Server Manager Remote Management โ€” leave enabled unless the README says otherwise; disabling it can break scoring engine connectivity in some images. Test carefully.

Things to try / extra points#

  • Get-WindowsFeature | Where-Object Installed โ€” full inventory, works on 2012+ without opening the GUI.
  • Get-WindowsFeature | Where-Object {$_.InstallState -eq "Available"} โ€” see what's NOT installed (sanity check nothing sneaky was added outside Server Manager, e.g., via DISM/PowerShell directly, that Server Manager's role list might not surface obviously).
  • Remove a feature safely once confirmed unneeded:
    powershell
    Remove-WindowsFeature -Name <FeatureName>
    
    Expected result: Output object with Success : True, RestartNeeded : No (or Yes), and FeatureResult listing the feature(s) removed. If RestartNeeded is Yes, the feature isn't fully gone until you reboot. If it fails: "The term 'Remove-WindowsFeature' is not recognized" happens on non-server SKUs or very old builds without the ServerManager module โ€” try Import-Module ServerManager first, or use the DOS/DISM fallback below. "Invalid feature name" means the <FeatureName> string doesn't match โ€” get exact names with Get-WindowsFeature | Where-Object Installed | Select-Object Name. Removing a feature that another installed role depends on can prompt a dependency warning or silently break that role โ€” read the confirmation text before proceeding, and add -Restart only if you're sure you're ready for the box to reboot. Or in DOS:
    shell
    dism /online /disable-feature /featurename:<FeatureName>
    
    Expected result: Deployment Image Servicing and Management tool banner followed by a progress percentage and The operation completed successfully. (a restart may be flagged as required). If it fails: "Error: 87" or "Error: 0x800f080c" usually means the /featurename: value is wrong โ€” DISM feature names often differ from the Get-WindowsFeature display name (e.g., use Get-WindowsOptionalFeature -Online to list DISM-style names). "Error: 5" is access denied โ€” the Command Prompt must be run as Administrator.
  • IE ESC toggle from command line (registry method, works when GUI toggle is finicky):
    shell
    REG ADD "HKLM\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A7-37EF-4b3f-8CFC-4F3A74704073}" /v IsInstalled /t REG_DWORD /d 00000001 /f
    REG ADD "HKLM\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A8-37EF-4b3f-8CFC-4F3A74704073}" /v IsInstalled /t REG_DWORD /d 00000001 /f
    
    Expected result: Both commands print The operation completed successfully.; the first GUID (...A7-...) controls Administrators ESC, the second (...A8-...) controls Users ESC โ€” IsInstalled = 1 means ESC is ON for that group. If it fails: "ERROR: Access is denied" โ€” Command Prompt must be elevated (Run as Administrator); writing under HKLM always requires admin rights. If the change doesn't visibly reflect in Server Manager's dashboard until you reopen it, that's normal โ€” Server Manager caches the ESC state and only re-reads it on refresh/reopen. Double-check you didn't transpose the two GUIDs (Administrators vs. Users) โ€” it's an easy copy-paste mistake that toggles the wrong group.
  • If a GUI setting can't be found or a command line change "silently fails" with no visible effect, verify with a read-back command rather than assuming success โ€” this is a classic way teams think they scored a point but didn't.
  • Verify a removal actually completed: Remove-WindowsFeature can return Success = True with RestartNeeded = Yes โ€” the role isn't fully gone until that restart happens. Re-run Get-WindowsFeature -Name <FeatureName> afterward; if InstallState still shows Removed pending a restart, note that a reboot is required before the change is considered complete (tie this to a snapshot/reboot checkpoint).
  • Edge case โ€” feature installed outside Server Manager: a role or optional component enabled purely via DISM/PowerShell can sometimes be invisible or oddly labeled in the Server Manager GUI. If Get-WindowsFeature shows something installed that the GUI doesn't clearly surface, trust the PowerShell output and investigate with Get-WindowsOptionalFeature -Online as a second opinion (covers optional features that sit outside the Roles/Features model, like SMB1Protocol).
  • 2008 R2 fallback: if Get-WindowsFeature/Remove-WindowsFeature aren't available at all, the legacy tool is ServerManagerCmd.exe -query / ServerManagerCmd.exe -remove <FeatureID> โ€” slower and less commonly needed, but useful if the newer cmdlets throw errors on an older image.

Tip: BPA has no reliable command-line equivalent worth relying on in competition โ€” do it through the GUI, one role at a time, and actually read what each Noncompliant item means before "fixing" it.

A note on Microsoft Baseline Security Analyzer (MBSA): older CyberPatriot checklists (including some of the source material this document was built from) list MBSA as a scan-and-patch tool worth running on a server. Don't bother โ€” Microsoft discontinued MBSA in 2018, it has no official support for Windows Server 2012 R2 and later, and it will generally fail to install or run correctly on any modern (2016+) server image. BPA above is Microsoft's actual current-generation replacement for that same "is this role configured per best practice" role. If you see MBSA mentioned in an older resource, treat it as outdated advice.

Third-party AV/cleaner GUI tools (Malwarebytes, CCleaner, Spybot โ€“ Search & Destroy) are documented with full install-and-use steps in the Windows Client Master Checklist, Section 8 โ€” they work identically on Server if a desktop experience is installed and internet access is available/permitted. The install steps, exact menu options, and post-scan scoring-report check-in are the same; the only server-specific caveat is to never let a cleaner tool touch anything under C:\Windows\NTDS, SYSVOL, or an active role's data directories โ€” if a scan flags something there, investigate manually rather than letting the tool auto-remove it, since role-critical files can superficially resemble what these tools are designed to catch.


4. Active Directory Users & Computers Auditing#

Applies only if the box is a Domain Controller (has AD DS installed). Open Active Directory Users and Computers: Win + R โ†’ dsa.msc.

This section bundles several distinct audit passes โ€” work through them as separate numbered steps rather than trying to do everything in one sweep of the console:

  1. [ ] Open the console and locate the accounts. Win + R โ†’ dsa.msc โ†’ expand the domain tree โ†’ click into Users, Computers, and any custom Organizational Units (OUs). Accomplishes: gets every account in front of you before you start judging any of them.
  2. [ ] Pass 1 โ€” remove unauthorized users. Go through every account in the right-hand pane and compare against the README's roster. Right-click anything unauthorized โ†’ Disable Account (prefer Disable over Delete unless you're confident). Accomplishes: closes unauthorized entry points first, before you get distracted by finer-grained settings.
  3. [ ] Pass 2 โ€” audit privileged group membership. Double-click each of: Administrators, Domain Admins, Enterprise Admins, Schema Admins, Account Operators, Backup Operators, Server Operators โ†’ Members tab โ†’ remove anyone not authorized by the README. Accomplishes: limits who can do domain-wide damage, independent of whether their account itself looks "authorized."
  4. [ ] Pass 3 โ€” per-account security flags. For each remaining authorized user, open Properties โ†’ Account tab and confirm:
    • Guest and any other default/unused built-in accounts are disabled.
    • krbtgt account exists, is NOT disabled, and is NOT deleted (critical AD account โ€” deleting it breaks the domain).
    • Password never expires is unchecked for regular users (unless the README explicitly requires it for a service account).
    • Store password using reversible encryption is unchecked.
    • Consider checking Account is sensitive and cannot be delegated for high-privilege accounts. Accomplishes: catches weaker per-account settings that survive even after the roster and group-membership passes are clean.
  5. [ ] Pass 4 โ€” structural checks. Check for stale/duplicate computer accounts in the Computers container that don't correspond to real machines in the scenario; verify the OU structure hasn't been tampered with (unexpected OUs, users moved out of expected OUs to dodge GPO application); check delegation on OUs to ensure delegated control doesn't grant excessive rights to non-admin groups. Accomplishes: catches structural manipulation that individual account checks won't surface.

Things to try / extra points#

  • Full account + attribute dump for review:
    powershell
    Get-ADUser -Filter * -Properties * | Select-Object Name, SamAccountName, Enabled, LastLogonDate, PasswordNeverExpires, PasswordLastSet | Format-Table -AutoSize
    
    Expected result: A wide console table, one row per AD user, with Enabled as True/False, LastLogonDate as a datetime (or blank if never logged on), and PasswordNeverExpires/PasswordLastSet populated. Table wraps or truncates in a narrow console โ€” widen the window or pipe to Out-GridView (if available) or Format-Table -Wrap for readability. If it fails: "Get-ADUser is not recognized" means the ActiveDirectory module isn't loaded โ€” this cmdlet only works on a DC or a member server with RSAT's RSAT-AD-PowerShell feature installed (Add-WindowsFeature RSAT-AD-PowerShell, or Import-Module ActiveDirectory if it's installed but not auto-loaded). Running this on a non-domain-joined standalone server will always fail since there's no AD to query.
  • Find disabled-but-still-privileged or otherwise inconsistent accounts:
    powershell
    Get-ADUser -Filter {Enabled -eq $false} | Select-Object Name, SamAccountName
    
    Expected result: A short list of Name/SamAccountName pairs for every disabled account โ€” should include Guest and any account you've already disabled; cross-check it doesn't unexpectedly include an account that's supposed to be active. If it fails: Empty output (no table at all, cmdlet just returns) genuinely means zero disabled accounts exist โ€” that's valid, not a bug; disable Guest if it's still shown as enabled elsewhere. Filter syntax errors ("Variable is not defined" pointing at Enabled) usually mean a typo in the property name or missing {} around the filter โ€” property names in -Filter are case-insensitive but must be spelled exactly right.
  • Find accounts with dangerous flags set:
    powershell
    Get-ADUser -Filter {PasswordNeverExpires -eq $true} | Select-Object Name, SamAccountName, Enabled
    Get-ADUser -Filter {AllowReversiblePasswordEncryption -eq $true} | Select-Object Name, SamAccountName
    Get-ADUser -Filter {SmartcardLogonRequired -eq $false -and Enabled -eq $true} | Select-Object Name, SamAccountName
    
    Expected result: Each query returns only the accounts matching that risky flag โ€” ideally the second (reversible encryption) is empty, and the first (password never expires) is empty or limited to a documented service account. The third query is almost always non-empty (most environments don't require smartcard logon) โ€” it's informational, not necessarily a finding to fix. If it fails: All three queries return nothing at all (not even for Guest/krbtgt) โ€” double check the ActiveDirectory module actually loaded (Get-Module ActiveDirectory) rather than assuming a clean result; a silently failed module import can make every -Filter query return empty instead of erroring. If a flag you fixed in the GUI still shows up here, you may be looking at a cached AD replication delay on a multi-DC setup โ€” re-run after a minute, or run directly against the PDC emulator.
  • Audit high-privilege group membership quickly:
    powershell
    Get-ADGroupMember -Identity "Domain Admins" | Select-Object Name, SamAccountName
    Get-ADGroupMember -Identity "Enterprise Admins" | Select-Object Name, SamAccountName
    Get-ADGroupMember -Identity "Schema Admins" | Select-Object Name, SamAccountName
    Get-ADGroupMember -Identity "Account Operators" | Select-Object Name, SamAccountName
    
    Expected result: A short Name/SamAccountName list per group โ€” Enterprise Admins and Schema Admins should typically be empty or contain only the built-in Administrator, since those groups only matter at forest-root-domain level and granting them broadly is a red flag. If it fails: "Cannot find an object with identity: 'Enterprise Admins'" โ€” this group only exists in the forest root domain; if you're on a child-domain DC, that query legitimately has nothing to find there (query it from the forest root DC instead, or accept it's out of scope for this box). This cmdlet requires the ActiveDirectory module/RSAT and only makes sense on a DC or an RSAT-equipped member server โ€” it will error on a plain member server with no AD tools installed.
  • Check OU delegation for over-broad permissions:
    powershell
    Get-Acl -Path "AD:\OU=Employees,DC=domain,DC=local" | Select-Object -ExpandProperty Access
    
    Expected result: A list of access control entries (IdentityReference, ActiveDirectoryRights, AccessControlType) showing who has what delegated rights on that OU. If it fails: "Cannot find path 'AD:...'" means either the AD: PSDrive isn't mounted (it auto-mounts when the ActiveDirectory module loads โ€” re-import it: Import-Module ActiveDirectory) or the DN (OU=Employees,DC=domain,DC=local) doesn't match this domain's actual structure โ€” get the real OU path with Get-ADOrganizationalUnit -Filter * first rather than guessing the DN.
  • Check who created new accounts recently (helps forensics + spotting rogue admin-added users):
    powershell
    Get-ADUser -Filter * -Properties WhenCreated | Sort-Object WhenCreated -Descending | Select-Object -First 15 Name, WhenCreated
    
    Expected result: The 15 most recently created AD accounts, newest first. If it fails: Same ActiveDirectory module/RSAT dependency as every AD cmdlet in this section โ€” see the recurring note above if it's not recognized. A very large/slow domain can make -Filter * take a while; that's expected on real environments, not a hang.
  • Critical edge case โ€” nested group membership: Get-ADGroupMember -Identity "Domain Admins" only shows direct members. If an unauthorized user was added to a smaller group that is itself nested inside Domain Admins, a plain query misses them entirely. Always add -Recursive:
    powershell
    Get-ADGroupMember -Identity "Domain Admins" -Recursive | Select-Object Name, SamAccountName
    
    Expected result: Every effective member of Domain Admins, including those added via nested group membership, not just direct additions. If it fails: "Cannot find an object with identity 'Domain Admins'" on a non-DC or a domain with a renamed built-in group is unusual but possible on a heavily customized image โ€” confirm the exact group name with Get-ADGroup -Filter * first. Comparing this recursive result against the earlier non-recursive check from Pass 2 is exactly how you catch the nested-membership trick โ€” if the counts differ, investigate the gap.
  • Verification โ€” confirm a disable actually took effect: after disabling an account (GUI or PowerShell), read it back rather than trusting the click:
    powershell
    Get-ADUser -Identity <SamAccountName> -Properties Enabled | Select-Object Name, Enabled
    
    Expected result: Enabled : False for an account you just disabled. If it fails: Still shows Enabled : True after disabling via the GUI โ€” you likely edited the wrong OU/found a same-named account in a different container, or the GUI change didn't actually save; re-open ADUC and confirm directly. "Cannot find an object with identity" means a typo in <SamAccountName> โ€” get the exact value with Get-ADUser -Filter * | Select SamAccountName.
  • Fallback if the ActiveDirectory PowerShell module isn't loaded/available (more common on 2008 R2 without RSAT tools installed): the legacy dsquery/dsget command-line tools ship with every DC and don't require the module:
    shell
    dsquery user -disabled
    dsquery group -name "Domain Admins" | dsget group -members -expand
    
    Expected result: dsquery user -disabled prints the distinguished names of disabled accounts, one per line. The piped dsquery/dsget combo prints the expanded (including nested) member list of Domain Admins. If it fails: These are older, DN-based tools โ€” output is distinguished names (CN=...,OU=...,DC=...), not friendly SamAccountNames, which can be harder to read at a glance; that's normal for this tool, not an error. "The directory service was unable to allocate a relative identifier" or similar odd errors here usually indicate a deeper AD health problem outside the scope of this specific command โ€” see Section 7 for AD health checks.

Tip: Never delete krbtgt, Domain Admins itself, or the account you're currently logged in as. Disabling the wrong account can lock you out of the box entirely โ€” if unsure, disable rather than delete, and test login again immediately after.


5. Group Policy Management (Domain-Level Policy)#

Critical DC-specific concept: On a Domain Controller, Local Security Policy (secpol.msc) is mostly grayed out or ineffective โ€” domain-linked Group Policy Objects (especially the Default Domain Policy and Default Domain Controllers Policy) override local policy. A very common beginner mistake is spending time editing secpol.msc on a DC and being confused why nothing changes or why it reverts on the next gpupdate. Use gpmc.msc (Group Policy Management Console) instead.

๐Ÿ“ธ Snapshot checkpoint: A bad edit to the Default Domain Policy or Default Domain Controllers Policy applies domain-wide and can lock out every account or machine in the domain (e.g., an overly strict lockout policy or a broken logon-rights change) โ€” that's a much bigger blast radius than a single-machine mistake. Take a snapshot before you start editing GPOs in this section.

This walkthrough bundles a lot of distinct steps โ€” follow it as a numbered sequence rather than trying to remember it as one block:

  1. [ ] Open GPMC. Win + R โ†’ gpmc.msc. Accomplishes: gets you into the console where domain-wide policy actually lives (remember, this replaces secpol.msc on a DC โ€” see the callout above).
  2. [ ] Navigate to the policy objects. Expand Forest โ†’ Domains โ†’ [Your Domain] โ†’ Group Policy Objects. Accomplishes: shows you every GPO that exists in the domain, linked or not โ€” this is your inventory.
  3. [ ] Open the Default Domain Policy for editing. Right-click Default Domain Policy โ†’ Edit to open the Group Policy Management Editor. Accomplishes: this GPO is linked at the domain root, so anything you set here applies to every computer and user in the domain by default.
  4. [ ] Configure Account Policies. Navigate to Computer Configuration โ†’ Policies โ†’ Windows Settings โ†’ Security Settings โ†’ Account Policies, and set Password Policy and Account Lockout Policy to the values in Section 6. Accomplishes: this is the domain-wide password/lockout baseline โ€” see Section 6 for exact values.
  5. [ ] Configure Security Options. In the same Security Settings node, under Local Policies โ†’ Security Options, set:
    • Accounts: Rename administrator account โ†’ set a custom name.
    • Accounts: Rename guest account โ†’ set a custom name.
    • Network access: Do not allow anonymous enumeration of SAM accounts and shares โ†’ Enabled.
    • Interactive logon: Do not display last signed-in โ†’ Enabled. Accomplishes: closes off several classic low-effort recon/enumeration paths that don't depend on password strength at all.
  6. [ ] Review the Default Domain Controllers Policy separately. This is a different GPO from the one you just edited โ€” it's where DC-specific user rights assignments (e.g., who can log on locally to a DC) live. Accomplishes: DC-specific settings won't be in the Default Domain Policy at all; skipping this step means you only did half the job.
  7. [ ] Hunt for malicious or orphaned GPOs. Review the Group Policy Objects list for anything unfamiliar (a GPO pushing a backdoor script, disabling the firewall, or adding a user to local admins), checking its Scope (linked OUs) and Settings (report view); separately check for unlinked/orphaned GPOs that aren't currently applied but could be relinked later. Accomplishes: catches policy-based attacks that don't show up if you only look at the two Default policies.
  8. [ ] Apply and verify. Force policy refresh, then confirm the changes actually landed:
    cmd
    gpupdate /force
    gpresult /r
    
    Expected result: gpupdate /force prints "Updating policy..." then "Computer Policy update has completed successfully." (and the same for User Policy). gpresult /r prints a summary showing which GPOs applied to this computer/user and which were filtered out, plus the resultant set of policy. If it fails: gpupdate /force reporting success doesn't guarantee your specific setting took effect if a HIGHER-PRECEDENCE GPO (linked closer to the OU, or with "Enforced" set) overrides it โ€” that's exactly what gpresult /r is for: check the "Denied" section of its output for any of your settings, which shows precedence conflicts. If gpresult shows your GPO wasn't applied at all, confirm it's actually linked to the right OU (an unlinked GPO, however well-configured, does nothing) and that neither the OU nor the GPO has Block Inheritance or a security-filtering setting excluding this computer/user. Accomplishes: gpupdate /force pushes your edits out immediately instead of waiting for the normal refresh interval; gpresult /r gives you a quick sanity check that policy is applying at all.

Things to try / extra points#

  • Enumerate all GPOs and their link status:
    powershell
    Import-Module GroupPolicy
    Get-GPO -All | Select-Object DisplayName, Id, ModificationTime
    
    Expected result: A table of every GPO in the domain with its display name, GUID, and last-modified timestamp. If it fails: "The term 'Get-GPO' is not recognized" โ€” the GroupPolicy module needs RSAT Group Policy Management tools installed (Install-WindowsFeature GPMC on a server missing it); on most DCs it's present by default, missing more often on a plain member server.
  • Find unlinked GPOs (a common place to hide unauthorized settings โ€” they don't show up as "active" but could be relinked):
    powershell
    Get-GPO -All | Where-Object { (Get-GPOReport -Guid $_.Id -ReportType Xml) -notlike "*<LinksTo>*" } | Select-Object DisplayName, Id
    
    Expected result: A (hopefully short) list of GPOs that exist but aren't currently linked anywhere. If it fails: This one-liner calls Get-GPOReport once per GPO, so it can be genuinely slow on a domain with many GPOs โ€” that's expected, not a hang. A GPO showing up here isn't automatically malicious (unlinked "template" or "staging" GPOs are a normal admin practice) โ€” the point is just to know it exists and could be relinked, not to delete every hit automatically.
  • Search SYSVOL for legacy Group Policy Preferences credentials (cpassword) โ€” a classic, easily-reversible credential leak left in old GPP XML files:
    powershell
    Get-ChildItem -Path "\\localhost\SYSVOL" -Recurse -Include *.xml,*.bat,*.ps1,*.vbs | Select-String -Pattern "cpassword","password"
    
    If found, remove the offending preference/GPO and rotate the exposed credential. Expected result: Matching lines with filename/path if any GPP XML file contains a cpassword attribute (these are trivially reversible โ€” Microsoft published the decryption key years ago) or the plaintext string "password". If it fails: No error mode; a hit here is a real, serious finding โ€” cpassword values are not a theoretical risk, they're a one-line decrypt away from a plaintext credential. If found, don't just delete the XML file โ€” identify and remove the actual GPO/GPP setting that created it (deleting the file alone may not stop the GPO from recreating it), and treat the exposed credential as compromised (rotate it).
  • Generate a full HTML report of a GPO for review instead of clicking through every node:
    powershell
    Get-GPOReport -Name "Default Domain Policy" -ReportType Html -Path C:\Users\Public\DDP_Report.html
    
    Expected result: An HTML file written to the given path; open it in a browser for a full readable breakdown of every setting the GPO configures. If it fails: "Cannot find a GPO with the display name 'X'" โ€” GPO display names are case-sensitive-looking but usually match-insensitive in practice; more likely a typo โ€” get exact names from Get-GPO -All | Select DisplayName.
  • Confirm policy is actually landing on the DC/member servers:
    cmd
    gpresult /r
    gpresult /h C:\Users\Public\gpresult.html
    
    Expected result: Same console summary as before; the /h variant writes a browsable HTML report instead (often easier to read than the console dump for a complex resultant policy). If it fails: No new failure modes beyond the earlier gpresult /r note โ€” if the HTML file doesn't open cleanly, confirm the path is writable and try opening it directly rather than double-clicking through Explorer if that behaves oddly on a locked-down image.
  • For the full effective-policy picture, use gpresult /z instead of /r. On a server, the Default Domain Policy, any local policy, and every OU-linked GPO all combine into one resultant set of policy (RSoP) โ€” /r gives you a summary, but /z dumps the complete, verbose resultant policy including which specific GPO won each setting and why. This is the only fully reliable way to see what's actually in effect, as opposed to what you think you set:
    cmd
    gpresult /z > C:\Users\Public\gpresult_full.txt
    
    Expected result: A much longer text dump than /r, including every individual setting and exactly which GPO (by name) set it โ€” this is what you grep/search through when you need to know "why is this specific setting what it is." If it fails: No special failure mode beyond what's already noted for /r โ€” if the output is too large to search comfortably in Notepad, open it in PowerShell ISE or less-equivalent, or Select-String for the specific setting name you're chasing (Select-String -Path C:\Users\Public\gpresult_full.txt -Pattern "PasswordComplexity"). GUI equivalent: Win + R โ†’ rsop.msc opens a live Resultant Set of Policy snap-in you can browse interactively, which is often easier to skim than the text dump.
  • Verification fallback if gpupdate /force seems to silently do nothing: check the Group Policy operational log (Event Viewer โ†’ Applications and Services Logs โ†’ Microsoft โ†’ Windows โ†’ GroupPolicy โ†’ Operational) for processing errors โ€” a GPO with a broken permission or a corrupted setting can fail to apply without any obvious error on screen.
  • The Security Options items already listed above (rename Administrator/Guest, disallow anonymous SAM/share enumeration, hide last signed-in user) align directly with CIS Benchmark Level 1 "Security Options" guidance for both the Domain Controller and Member Server profiles โ€” these are considered baseline, low-risk wins. A few more Level 1 Security Options worth adding while you're in this same GPO node:
    • Interactive logon: Do not require CTRL+ALT+DEL โ†’ Disabled.
    • Network security: Do not store LAN Manager hash value on next password change โ†’ Enabled.
    • Network security: LAN Manager authentication level โ†’ Send NTLMv2 response only. Refuse LM & NTLM.
    • Microsoft network server: Digitally sign communications (always) โ†’ Enabled.
    • Domain member: Digitally encrypt or sign secure channel data (always) โ†’ Enabled.
  • DC-specific (Default Domain Controllers Policy โ†’ User Rights Assignment): CIS's Domain Controller profile includes several User Rights Assignment checks worth reviewing here โ€” confirm Add workstations to domain, Allow log on locally, and Allow log on through Remote Desktop Services are restricted to Administrators/authorized groups only, and that Deny access to this computer from the network includes appropriate accounts (e.g., built-in local accounts) per the benchmark's guidance. This is a good companion check to the account audits in Section 4.
  • CIS Level 2 (worth trying if time allows and it won't break required roles): CIS's stricter profile includes tighter interactive-logon and network-access restrictions (e.g., further restricting anonymous access and remote registry paths) beyond the Level 1 items above โ€” these carry more risk of breaking legitimate tooling, so validate required functionality immediately after applying.

Tip: If you edit something in secpol.msc on a DC and it doesn't stick, that's not a bug โ€” it's domain GPO overriding you. Go find the equivalent setting in gpmc.msc under Default Domain Policy or Default Domain Controllers Policy instead.


6. Password & Account Lockout Policy#

On a Domain Controller, password/lockout policy is set at the domain level (Default Domain Policy via GPMC or PowerShell AD cmdlets) โ€” NOT via local secpol. On a standalone/member server, local policy via secpol.msc or secedit still applies normally.

Setting Recommended Value
Minimum password length 14 characters
Password must meet complexity requirements Enabled
Enforce password history 24 passwords remembered
Maximum password age ~30โ€“90 days (not 0/never)
Minimum password age 1 day
Account lockout threshold 5 invalid attempts
Account lockout duration 30 minutes
Reset account lockout counter after 15 minutes
  • On a DC: set these via GPMC โ†’ Default Domain Policy (see Section 5) or via Set-ADDefaultDomainPasswordPolicy.
  • On a standalone/member server: set via secpol.msc โ†’ Account Policies, or secedit/net accounts.
  • Check for Fine-Grained Password Policies (FGPP) that might override the domain default for specific users/groups โ€” verify they aren't weaker than the domain policy for privileged accounts.

Things to try / extra points#

  • Set domain password/lockout policy directly from PowerShell (fast, scriptable, avoids GUI round-trips):
    powershell
    Set-ADDefaultDomainPasswordPolicy -MinPasswordLength 14 -ComplexityEnabled $true -PasswordHistoryCount 24 -LockoutThreshold 5 -LockoutDuration (New-TimeSpan -Minutes 30) -LockoutObservationWindow (New-TimeSpan -Minutes 15)
    
    Expected result: Silent on success. Verify with Get-ADDefaultDomainPasswordPolicy (next command) rather than assuming it worked. If it fails: "Access is denied" means your account isn't a Domain Admin (or equivalent delegated right) โ€” this cmdlet modifies domain-wide policy and needs real privilege. This only works run against a DC (or targeting one via -Server) โ€” running it on a plain member server with no domain context will error.
  • Check current domain policy quickly:
    powershell
    Get-ADDefaultDomainPasswordPolicy
    
    Expected result: An object listing every current password/lockout policy value for the domain โ€” confirm your new values landed. If it fails: Same module/RSAT/DC-context caveats as every other AD cmdlet in this document.
  • Check for Fine-Grained Password Policies that could be silently weaker than the domain default:
    powershell
    Get-ADFineGrainedPasswordPolicy -Filter *
    
    Expected result: A list of any FGPP objects defined, or nothing if none exist (most environments don't use FGPPs at all โ€” empty is a normal, common outcome). If it fails: No error mode beyond the usual module/permission checks. If an FGPP DOES exist, check which users/groups it applies to (AppliesTo property) and whether its values are actually weaker than the domain default you just set โ€” an FGPP always wins over the domain default for the accounts it targets, regardless of how strong you make the domain-wide policy.
  • On a standalone server (no domain), use secedit for a bulk policy push. This is an export โ†’ edit โ†’ re-import pattern โ€” three distinct steps, don't try to do it in one motion:
    1. Export the current policy to a text file:
      shell
      secedit /export /cfg C:\secpol.cfg
      
      Accomplishes: gives you a plain-text starting point instead of writing a policy file from scratch. Expected result: The task has completed successfully.; C:\secpol.cfg now exists. If it fails: "Access is denied" writing to C:\ root on a locked-down image โ€” export to C:\Users\Public\secpol.cfg instead and use that path in every later step.
    2. Edit the exported file (Notepad or PowerShell) and change the relevant lines under [System Access] โ€” e.g. MinimumPasswordLength = 14, PasswordComplexity = 1, LockoutBadCount = 5. Accomplishes: this is where your actual policy values get set โ€” the export/import steps are just plumbing around this edit. Expected result: File saves normally; still valid .ini-style text. If it fails: A key you expect (e.g. PasswordHistorySize) missing from the exported file entirely โ€” just add it as a new line under [System Access], secedit accepts appended keys as long as they're valid ones for that section.
    3. Re-import the edited file to apply it:
      shell
      secedit /configure /db C:\Windows\security\local.sdb /cfg C:\secpol.cfg /areas SECURITYPOLICY
      
      Accomplishes: pushes the edited values from the text file back into the actual local security database โ€” nothing changes on the system until this step runs. Expected result: The task has completed successfully. If it fails: "The task has completed with one or more errors" here (unlike the export step) usually means a malformed line in your edited file โ€” check %windir%\security\logs\scesrv.log for the specific line it choked on, fix it in secpol.cfg, and re-run; this command is safe to re-run.
    4. Verify it actually applied โ€” re-export and check the values landed, or spot-check with net accounts:
      shell
      secedit /export /cfg C:\secpol_verify.cfg
      net accounts
      
      Expected result: secpol_verify.cfg's values match what you set; net accounts prints the new numbers. If it fails: Values didn't change despite a "successful" import โ€” on a DC, remember the callout at the top of this section: local secedit-based policy is overridden by domain GPO for a Domain Controller. If this is a DC, use the GPMC/Set-ADDefaultDomainPasswordPolicy method instead โ€” secedit here only genuinely applies on a standalone/member server. Accomplishes: secedit /configure fails silently on some malformed edits โ€” always confirm rather than assuming step 3 worked.
  • GUI alternative to the whole secedit dance (same effect, no risk of a malformed .cfg file): secpol.msc โ†’ Account Policies โ†’ set the values directly in the UI. Slower for bulk changes, faster and safer for editing 2-3 values.
  • This whole section aligns with CIS Benchmark Level 1 "Account Policies" guidance (Password Policy + Account Lockout Policy), and the same values apply to both the Domain Controller and Member Server CIS profiles โ€” this is one of the most universally-agreed-upon baseline items in the benchmark, so treat the table above as safe to apply everywhere without much risk.

Tip: Don't set Maximum Password Age to 0 (never expires) thinking it's "safer" because it avoids lockouts โ€” a non-expiring password policy is itself commonly flagged as a vulnerability.


7. Active Directory Health, Replication & Kerberos Hardening#

AD Health & Replication#

  • Run diagnostics to confirm the DC is healthy before and after making changes โ€” a broken DC can silently fail scoring checks for AD-dependent services.
  • Confirm SYSVOL/NETLOGON shares are present and shared (net share should list SYSVOL and NETLOGON).
  • Confirm time sync is correct โ€” Kerberos fails hard with clock drift beyond ~5 minutes.

Kerberos & Authentication Hardening#

  • Audit accounts with Service Principal Names (SPNs) โ€” these are targets for Kerberoasting (offline cracking of service account password hashes).
  • Audit accounts with Kerberos pre-authentication disabled โ€” targets for AS-REP Roasting.
  • Check for accounts/computers configured with unconstrained delegation โ€” a high-value target since compromising them can yield cached admin TGTs.
  • Consider enrolling high-privilege accounts into the Protected Users group to enforce strict Kerberos-only auth (blocks NTLM/digest auth and LSASS credential caching for those accounts) โ€” only do this if it won't break required functionality for a service account.

Things to try / extra points#

  • Full DC diagnostics (run early to baseline, run again after changes to confirm nothing broke):

    shell
    dcdiag /v
    
    What dcdiag actually tests
    dcdiag runs a whole battery of built-in health checks against a Domain Controller โ€” DNS, replication, time sync, trust relationships, and more โ€” all in one command. /v (verbose) makes it print full detail on every test instead of just pass/fail, which is why it's slower but far more useful when something's actually wrong.
    Expected result:A long verbose report ending each test with passed test <TestName> โ€” scroll for any failed test line.
    If it fails:
    • A slow/long run (several minutes) is normal on /v, not a hang.
    • Any failed test needs individual investigation โ€” dcdiag output usually names the specific problem clearly enough to search for that exact test name plus "dcdiag" if you're unsure what it means; common early-competition failures are DNS-related (a DC that can't resolve its own SRV records) or time-sync related, both covered by other bullets in this section.
  • Replication health (only meaningful with multiple DCs, but harmless to run and check for errors otherwise):

    shell
    repadmin /replsummary
    repadmin /showrepl
    
    What these repadmin commands check
    Active Directory Domain Controllers constantly copy directory changes to each other ("replication") so every DC has the same data. /replsummary gives a quick one-screen health overview of that replication across all DCs; /showrepl drills into the specific errors for one DC if the summary shows a problem โ€” think of it as the summary dashboard vs. the detailed error log.
    Expected result:
    • On a single-DC domain, both commands report no partners / nothing to replicate โ€” that's a normal, expected outcome, not an error.
    • With multiple DCs, /replsummary shows a per-DC summary with failure counts; 0 failures across the board is the goal.
    If it fails:Non-zero replication failures need investigating with repadmin /showrepl for the specific error code โ€” common causes are DNS resolution problems between DCs or a firewall blocking AD replication ports; don't just re-run the command expecting it to self-heal.
  • Confirm domain controller discoverability:

    shell
    nltest /dsgetdc:domain.local
    nltest /dclist:domain.local
    
    Expected result:/dsgetdc prints the discovered DC's name, IP, and site info; /dclist lists every DC in the domain.
    If it fails:"The RPC server is unavailable" or a DC-not-found error suggests DNS misconfiguration (the box can't find SRV records pointing at a DC) or the domain name is wrong โ€” double check the actual domain name with Get-ADDomain or systeminfo first rather than guessing domain.local.
  • Force time resync against a reliable source to prevent Kerberos failures from clock drift:

    shell
    w32tm /config /manualpeerlist:"0.pool.ntp.org 1.pool.ntp.org" /syncfromflags:manual /reliable:YES /update
    w32tm /resync
    
    Expected result:The command completed successfully. for the config line; /resync prints Sending resync command to local computer then a success confirmation.
    If it fails:
    • "no response from server" on /resync most often means no internet access (pool.ntp.org unreachable) โ€” if this image is offline, external NTP won't work at all; instead sync against the DC itself if this is a member server (w32tm /config /syncfromflags:domhier /update) or accept the clock as-is if this IS the DC and there's no external reference available.
    • A DC being the domain's own time source is normal โ€” don't point the PDC emulator DC at itself in a loop; check netdom query fsmo first to confirm whether this box holds the PDC emulator role, which changes what "correct" time sync looks like for it specifically.
  • List SPNs to find Kerberoasting targets:

    powershell
    Get-ADUser -Filter {ServicePrincipalName -ne "$null"} | Select-Object Name, ServicePrincipalName, UserPrincipalName
    

    Remove unnecessary SPNs:

    shell
    setspn -D <SPN> <AccountName>
    
    Expected result:
    • The Get-ADUser query lists user accounts (not computer accounts โ€” those normally have SPNs legitimately) that have an SPN registered; each is a theoretical Kerberoasting target since anyone can request a service ticket for it and attempt offline cracking.
    • setspn -D prints confirmation it removed the SPN.
    If it fails:
    • An SPN legitimately tied to a running service (e.g., a SQL Server service account) will break that service if removed โ€” don't strip SPNs reflexively; cross-check against the README/required services first.
    • setspn -D failing with "cannot find SPN" means the exact <SPN> string didn't match โ€” copy it verbatim from the query output above rather than retyping it.
  • Find AS-REP roastable accounts (pre-auth disabled):

    powershell
    Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true} | Select-Object Name, SamAccountName
    

    Fix:

    powershell
    Set-ADUser -Identity <User> -DoesNotRequirePreAuth $false
    
    Expected result:
    • The query lists accounts with Kerberos pre-authentication disabled (ideally empty โ€” this is rarely legitimately needed).
    • The fix silently re-enables pre-auth requirement.
    If it fails:Some legacy applications genuinely need pre-auth disabled for compatibility โ€” check before blanket-fixing every hit; if the README doesn't mention such a requirement, treat every hit as a real finding.
  • Find unconstrained-delegation objects:

    powershell
    Get-ADObject -Filter {UserAccountControl -band 0x800000} -Properties SamAccountName, UserAccountControl | Select-Object SamAccountName, Name
    

    Remediate via dsa.msc โ†’ object Properties โ†’ Delegation tab โ†’ Do not trust this user/computer for delegation. Expected result: A list of user/computer objects with the unconstrained-delegation UAC flag set โ€” DCs themselves normally show up here legitimately (that's expected/required for DCs specifically), so filter those out and focus on non-DC computer/user objects. If it fails: No error mode; the query itself is read-only. Removing unconstrained delegation from something that actually needs it (a legitimate multi-hop authentication scenario) breaks that functionality โ€” verify against the README/scenario description, especially for any non-DC hits, before remediating.

  • Add sensitive admin accounts to Protected Users (test login afterward โ€” this changes auth behavior):

    powershell
    Add-ADGroupMember -Identity "Protected Users" -Members "DomainAdminUser"
    
    Expected result:Silent on success.
    If it fails:
    • Protected Users membership can lock an account out of legitimate access if that account relies on NTLM, DES/RC4 Kerberos encryption, delegation, or cached credentials for something required โ€” test login immediately after adding an account, and be ready to remove them from the group (Remove-ADGroupMember) if something breaks.
    • This is explicitly called out as risky in the checklist bullet above for good reason.
  • Enforce AES-only Kerberos encryption (disable legacy RC4-HMAC) if the environment allows it โ€” this aligns with CIS Benchmark Level 1 guidance ("Network security: Configure encryption types allowed for Kerberos") which recommends restricting to AES128/AES256 and disabling DES/RC4:

    powershell
    Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\Kerberos\Parameters" -Name "SupportedEncryptionTypes" -Value 24 -Type DWord
    
    Expected result:
    • Silent on success.
    • Value 24 = AES128 + AES256 only (bitmask: 8 + 16).
    If it fails:
    • "Cannot find path" means the Kerberos\Parameters key doesn't exist yet on this box โ€” add -Force to Set-ItemProperty to create it.
    • Legacy clients/services that only support RC4 will fail to authenticate after this change โ€” this is exactly why the verification step right below (checking klist tickets for AES vs RC4) matters; if something breaks, this registry value is the first thing to suspect and can be reverted by deleting the key or setting it back to its prior value (commonly absent/default, which allows all types).
  • Require LDAP server signing โ€” CIS Level 1 ("Domain controller: LDAP server signing requirements" under Security Options). An unsigned LDAP bind can be relayed/tampered with in transit, the LDAP equivalent of the SMB-signing issue covered earlier โ€” this is a DC-only setting and won't appear on a member server. Fastest path โ€” GUI: gpmc.msc/secpol.msc โ†’ Local Policies โ†’ Security Options โ†’ Domain controller: LDAP server signing requirements โ†’ Require signing. Command-line path (same export โ†’ edit โ†’ import pattern as Section 6):

    1. Export current policy: secedit /export /cfg C:\ldapsign.cfg /areas SECURITYPOLICY
    2. Edit C:\ldapsign.cfg and set LDAPServerIntegrity = 2.
    3. Re-import: secedit /configure /db C:\Windows\security\local.sdb /cfg C:\ldapsign.cfg /areas SECURITYPOLICY
    4. Verify: re-export and confirm LDAPServerIntegrity = 2 is present, since a malformed edit fails silently just like in Section 6.

    Tip: Requiring LDAP signing can break older/misconfigured LDAP clients that only bind unsigned โ€” if something in the topology stops authenticating after this change, that's the first setting to suspect.

  • Verify the AES-only Kerberos change actually took effect rather than trusting the registry write โ€” after setting SupportedEncryptionTypes, force a fresh ticket and inspect it:

    shell
    klist purge
    klist tickets
    

    Look for AES256-CTS-HMAC-SHA1-96 (or AES128) as the encryption type on freshly-issued tickets instead of RC4-HMAC. Expected result: klist purge clears cached tickets; klist tickets (after doing something that triggers a fresh ticket request, like accessing a network resource) shows the new tickets' encryption type field. If it fails: If klist tickets still shows RC4-HMAC after the registry change, the setting may not have propagated yet โ€” this registry value typically needs a gpupdate /force or logoff/logon (sometimes a reboot) to take effect for new ticket requests, it's not always instant. If tickets show empty right after purge, that's expected โ€” request one by accessing any Kerberos-authenticated resource, then re-run klist tickets.

  • dcdiag fallback for targeted checks: running the full /v sweep is thorough but slow and noisy. If you only care about one thing (e.g., after a time-sync fix, or after touching services), run a targeted test instead: dcdiag /test:Advertising, dcdiag /test:Services, dcdiag /test:FrsEvent (or DFSREvent on newer domains), or dcdiag /test:NetLogons.

  • GUI alternative to repadmin: Active Directory Sites and Services (dssite.msc) โ†’ expand Sites โ†’ [Site Name] โ†’ Servers โ†’ [DC] โ†’ NTDS Settings โ†’ right-click each connection โ†’ Replicate Now. Slower for a full health check than repadmin /replsummary, but useful if you want to trigger replication interactively without memorizing switches.

  • Disable Print Spooler on Domain Controllers (mitigates PrintNightmare-class RPC privilege escalation) โ€” this reflects a Domain-Controller-profile CIS recommendation added after PrintNightmare (recent CIS DC benchmark guidance calls for the Print Spooler service to be disabled on DCs); treat it as a Level 2 / higher-impact item since it removes a service outright โ€” only if the README doesn't require print services on this box:

    powershell
    Stop-Service -Name "Spooler" -Force
    Set-Service -Name "Spooler" -StartupType Disabled
    
    Expected result:Both silent on success; confirm with Get-Service Spooler showing Stopped/Disabled.
    If it fails:
    • "Cannot stop service Spooler on computer '.' " with an access-denied-style error means not running elevated.
    • If a required print role/scenario needs the spooler, this will break printing entirely โ€” check the README first, exactly as the checklist bullet warns.
  • Enable Credential Guard to isolate LSA secrets from memory-dumping tools โ€” this maps to CIS Level 2 "Virtualization Based Security" guidance (requires appropriate hardware/VBS support โ€” verify it doesn't break the box before relying on it for points):

    powershell
    Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" -Name "LsaCfgFlags" -Value 1 -Type DWord
    
    Expected result:Silent on success; requires a reboot to actually take effect, and Credential Guard additionally requires UEFI, Secure Boot, and virtualization extensions to actually activate (not just the registry flag).
    If it fails:
    • On a virtual machine (which most CyberPatriot images are), Credential Guard often cannot actually activate even with this registry key set, because it needs nested virtualization/VBS support the hypervisor or VM configuration may not expose โ€” check System Information (msinfo32.exe) โ†’ "Virtualization-based security" status after reboot to confirm whether it's actually running, rather than assuming the registry write alone means it's active.
    • Don't rely on this for guaranteed points without confirming it actually took effect on this specific image.
  • If AD CS (Certificate Services) is installed, check certificate templates for ENROLLEE_SUPPLIES_SUBJECT (allows a requester to supply their own SAN, enabling impersonation) via certtmpl.msc โ€” verify Subject Name โ†’ Supply in the request is Disabled on authentication templates.

Tip: Disabling Print Spooler or enabling Credential Guard can break required functionality on some scenario boxes โ€” verify against the README first, and re-test the specific required service after the change.


8. DNS Server Hardening#

Applies if the DNS Server role is installed. Open via Win + R โ†’ dnsmgmt.msc, or Start โ†’ Administrative Tools โ†’ DNS.

Patch reminder โ€” configuration alone is not enough. The Windows DNS Server role has had serious remote-code-execution vulnerabilities in the past (e.g., CVE-2020-1350 / "SIGRed," a wormable RCE in the DNS Server role itself). A perfectly-configured zone with locked-down transfers and secure dynamic updates is still vulnerable if the underlying DNS Server binary is unpatched. Don't treat DNS hardening as "configure it correctly and move on" โ€” make sure Section 13 (Windows Updates) has actually been applied to this box, especially if it's offered any updates flagged critical/security for the DNS Server role specifically.

๐Ÿ“ธ Snapshot checkpoint: DNS and DHCP misconfigurations don't just affect this box โ€” they can break name resolution or address assignment for every other graded machine on the scoring network that depends on this server. Take a snapshot before changing zone transfer settings, dynamic update mode, forwarders, or (in the next section) DHCP scope options, so a mistake here doesn't cascade into other teams' โ€” or your own team's โ€” machines going dark.

  • Open the DNS server's Properties โ†’ Advanced tab and review server-level settings. If in doubt about a setting, leave "Fail on load if bad zone data" alone unless you know why โ€” misconfiguring it can take DNS offline.
  • Open Root Hints tab โ€” server FQDNs should only be a.root-servers.net through m.root-servers.net; verify against root-servers.org if IPs look suspicious.
  • Open Forwarders โ€” should typically be empty with "Use root hints if no forwarders are available" checked, unless the README specifies an internal/upstream forwarder to use.
  • Open Debug Logging โ€” enable logging categories for visibility (this doesn't affect security posture directly but helps troubleshooting/forensics).
  • Open Event Logging โ€” set to log All events.
  • Open Trust Anchors โ€” should typically be empty unless DNSSEC is explicitly part of the scenario.
  • For each Forward Lookup Zone:
    • General tab โ†’ set Dynamic updates to Secure only (never "Nonsecure and secure").
    • Zone Transfers tab โ†’ uncheck "Allow zone transfers", or if required for a secondary DNS server, restrict to "Only to servers listed on the Name Servers tab" โ€” never leave it open to "Any server."
  • Check Interfaces tab on the server properties โ€” ensure the DNS server only listens on authorized/internal IP addresses, not all interfaces if some are untrusted.
  • Audit zone records for anything unauthorized or suspicious (unexpected A/CNAME/TXT records, wildcard records used for tunneling or exfiltration).
  • If this server is also the DHCP server, check the DNS integration tab on DHCP to ensure dynamic A/PTR record updates only occur for requesting DHCP clients (not unrestricted).

Things to try / extra points#

  • Lock zone transfers down to named secondaries only:
    powershell
    Set-DnsServerPrimaryZone -Name "domain.local" -SecureSecondaries "TransferToZoneNameServer"
    
    Expected result: Silent on success. Verify with Get-DnsServerZone -Name domain.local | Select SecureSecondaries (or check the Zone Transfers tab in the GUI). If it fails: "Zone domain.local does not exist" โ€” use the real zone name from Get-DnsServerZone rather than the placeholder. If a legitimate secondary DNS server stops replicating after this, confirm it's actually listed on the zone's Name Servers tab โ€” TransferToZoneNameServer only allows transfers to servers explicitly listed there.
  • Harden the cache against poisoning/spoofing:
    powershell
    Set-DnsServerCache -CacheLockingPercent 96
    Set-DnsServer -SocketPoolSize 2500
    
    Expected result: Both silent on success. If it fails: No common failure mode beyond permission errors (run elevated). These are defense-in-depth tuning values, not commonly scenario-breaking โ€” low risk to apply.
  • Audit for suspicious record types often abused for data exfil/tunneling:
    powershell
    Get-DnsServerResourceRecord -ZoneName "domain.local" | Where-Object { $_.RecordType -in @("TXT","CNAME") }
    
    Expected result: A list of TXT/CNAME records in the zone โ€” some are completely legitimate (SPF records, service aliases), so this needs judgment, not automatic removal. If it fails: No error mode; a long, unfamiliar-looking TXT record value (especially one that looks like encoded/random data rather than a normal SPF/verification string) is the actual thing worth flagging, not TXT records in general.
  • List all zones and their transfer/update settings at a glance:
    powershell
    Get-DnsServerZone
    Get-DnsServerZoneTransferPolicy -ZoneName "domain.local" -ErrorAction SilentlyContinue
    
    Expected result: Get-DnsServerZone lists every zone with type (Primary/Secondary/Stub) and dynamic-update setting; the transfer-policy cmdlet often returns nothing (-ErrorAction SilentlyContinue suppresses the error) since transfer policies are an advanced/rarely-used feature โ€” that's normal. If it fails: No real failure mode for the zone listing itself; use it to spot any zone with DynamicUpdate set to NonsecureAndSecure (the setting this section's checklist explicitly says should be Secure only).
  • Check configured forwarders:
    powershell
    Get-DnsServerForwarder
    
    Expected result: Lists configured forwarder IPs, or an empty/error-like response if none are configured (root hints only) โ€” which is the recommended default per this section's checklist unless the README specifies otherwise. If it fails: No real error mode; cross-check any listed forwarder IP against what the README expects โ€” an unfamiliar forwarder IP is a real finding (could redirect DNS resolution to attacker-controlled infrastructure).
  • Verify root hints programmatically:
    powershell
    Get-DnsServerRootHint
    
    Expected result: Lists the 13 standard root server names/IPs (a.root-servers.net through m.root-servers.net). If it fails: Fewer than 13 entries, or an entry with a name/IP that doesn't match the real root server list, is a real finding โ€” verify suspicious-looking entries against the authoritative list at root-servers.org (the checklist bullet above already flags this).
  • Note on CIS coverage: the CIS Microsoft Windows Server Benchmark is primarily an OS-level baseline and does not have a dedicated DNS-role section โ€” the zone transfer/dynamic update hardening above comes from Microsoft's own DNS security guidance rather than a specific CIS control. Don't expect to find "DNS zone transfer" called out by name in the benchmark; it's still correct and worth doing.
  • Verify zone transfers are actually blocked (don't just trust the checkbox) โ€” from another machine on the network, attempt an AXFR zone transfer against the server:
    shell
    nslookup
    server <DNS-server-IP>
    ls -d domain.local
    
    A properly-locked-down server should refuse this ("Query refused" or similar) rather than dumping the full zone. Expected result (locked down): ls -d returns an error like "DNS server refused to transfer" โ€” no zone data dumped. Expected result (vulnerable): the full zone contents printed, meaning your transfer restriction isn't actually effective โ€” recheck the Zone Transfers tab / SecureSecondaries setting. If it fails to even connect: "DNS request timed out" or "can't find server name" means either the wrong IP was used or the DNS service isn't listening on that interface โ€” confirm the correct IP with ipconfig /all on the DNS server itself first.
  • Edge case โ€” secondary zones: if this server hosts a secondary zone (a read-only copy transferred from another DNS server), zone-transfer restrictions are configured on the primary zone's server, not here โ€” don't waste time looking for a transfer-restriction setting on a secondary zone that doesn't have one.
  • Check for stale/unpatched DNS Server updates specifically: Get-HotFix | Where-Object {$_.Description -like "*Security*"} | Sort-Object InstalledOn -Descending โ€” cross-reference against Section 13 rather than assuming general Windows Update coverage caught DNS-specific patches.

Tip: If DNS breaks after a change, the very first thing to check is the "fail on load if bad zone data" setting and whether the zone file itself got corrupted by an edit โ€” a restart is sometimes needed to fully take effect but is rarely the actual fix.


9. DHCP Server Checks#

DHCP is a comparatively simple, unauthenticated protocol, so role-specific vulnerabilities are limited โ€” the main risks are rogue/unauthorized DHCP servers and misconfigured scope options pointing clients to malicious infrastructure.

  • Open DHCP Management Console: Win + R โ†’ dhcpmgmt.msc.
  • Right-click the server โ†’ Manage authorized serversโ€ฆ โ†’ verify only legitimate domain DHCP servers are listed; Unauthorize any rogue entries.
  • Expand IPv4 โ†’ Scope โ†’ Scope Options and verify:
    • 003 Router points to the legitimate internal gateway.
    • 006 DNS Servers points strictly to authorized internal DNS server(s).
    • 015 DNS Domain Name matches the expected internal domain.
  • Check scope lease duration and address range are sane for the scenario (not obviously reconfigured to something suspicious).
  • If this server is also DNS-integrated, confirm dynamic DNS updates are restricted to only occur when explicitly requested by DHCP clients (Properties โ†’ DNS tab on the DHCP server).

Things to try / extra points#

  • Enumerate authorized DHCP servers in AD and spot rogue ones:
    powershell
    Get-DhcpServerInDC
    
    Remove an unauthorized entry:
    powershell
    Remove-DhcpServerInDC -DnsName "unauthorized.domain.local"
    
    Expected result: Get-DhcpServerInDC lists every DHCP server authorized in this AD domain (name + IP). Removal is silent on success. If it fails: "This operation can only be performed if the machine is joined to a domain" โ€” this only works on a domain-joined box with AD-integrated DHCP; a standalone DHCP server isn't tracked this way at all, so this check doesn't apply there. Don't unauthorize a server you're not certain is rogue โ€” cross-check against the README's described network topology first, since a legitimate secondary/backup DHCP server would also show up here.
  • Review scopes and options from PowerShell instead of clicking through the GUI:
    powershell
    Get-DhcpServerv4Scope
    Get-DhcpServerv4OptionValue -ScopeId <ScopeID>
    
    Expected result: Scope list with ID/range/state; option-value output shows each configured option (Router, DNS Servers, Domain Name, etc.) with its current value for that scope. If it fails: "ScopeId parameter is required" or similar if you leave the placeholder unfilled โ€” get the real scope ID from the first command's output first.
  • Audit current leases for unexpected/unauthorized devices:
    powershell
    Get-DhcpServerv4Lease -ScopeId <ScopeID>
    
    Expected result: A table of active leases โ€” IP, MAC (ClientId), hostname, lease expiry. If it fails: No error mode beyond a bad <ScopeID>; unfamiliar hostnames/MACs are worth a closer look but aren't automatically malicious on a real network โ€” cross-check against what devices the README/scenario says should be present, if it says anything at all.
  • Check for scope-level or server-level DNS dynamic update misconfiguration:
    powershell
    Get-DhcpServerv4DnsSetting
    
    Expected result: Shows whether dynamic DNS updates are enabled and under what conditions (e.g., DynamicUpdates: OnClientRequest, DeleteDnsRRonLeaseExpiry: True). If it fails: No real error mode; the finding to look for is DynamicUpdates: Always (updates DNS unconditionally) โ€” the checklist bullet above recommends restricting updates to only when explicitly requested by DHCP clients instead.
  • Note on CIS coverage: like DNS, the DHCP role isn't covered as its own section in the CIS Windows Server Benchmark โ€” the checks above come from general Microsoft/DHCP security guidance rather than a named CIS control. The benchmark's value here is indirect: keeping the underlying OS hardened (firewall, services, audit policy) still protects the DHCP role itself.
  • Verify scope option changes actually reach a client: the fastest real-world confirmation is a lease renewal test from any client on the same scope โ€” ipconfig /release then ipconfig /renew โ€” and then check the client picked up the corrected Router/DNS options, not just that the server-side setting shows the new value.
  • Edge case โ€” superscopes and multiple scopes: if the server has more than one scope (or a superscope grouping several), scope options can be set at the scope level, the server level, or both, and server-level options only apply where a scope doesn't explicitly override them. Don't assume fixing one scope's options fixed all of them โ€” check Get-DhcpServerv4Scope for every scope ID present, not just the first one you find.
  • GUI cross-check: dhcpmgmt.msc โ†’ expand IPv4 โ†’ Server Options (not just Scope Options) to see settings that apply server-wide unless a scope overrides them โ€” easy to miss if you only ever look inside a specific scope. is out of your control (it's on a different machine), but make sure YOUR box isn't accidentally running/authorizing an unintended DHCP scope, and that its own DHCP settings aren't the "rogue" element being tested.

10. File & Print Server / Share Permissions#

  • Enumerate all shares โ€” remove or lock down anything not required by the README.
  • For each required share, verify Share permissions AND NTFS permissions are least-privilege (avoid "Everyone: Full Control").
  • Check for hidden/administrative shares beyond expected defaults (C$, ADMIN$, IPC$ are normal defaults โ€” extra custom hidden shares (name$) are suspicious).
  • If quotas are part of the scenario, verify File Server Resource Manager (FSRM) quotas match the README's requirements.
  • Consider FSRM File Screening to block unauthorized executable uploads to shared drives if this fits the scenario (be careful โ€” don't block file types users legitimately need to share).
  • For Print and Document Services: Server Manager โ†’ Roles โ†’ right-click Print and Document Services โ†’ Remove Role Services for anything unneeded (e.g., unneeded print drivers, LPD service if not required).

Things to try / extra points#

  • List all shares and permissions quickly:
    powershell
    Get-SmbShare
    Get-SmbShareAccess -Name "<ShareName>"
    
    Expected result: Get-SmbShare lists every share (name, path, description); Get-SmbShareAccess lists the share-level permission entries (account + access right) for one named share. If it fails: "The term 'Get-SmbShare' is not recognized" on very old builds (2008 R2) โ€” use net share (next bullet) instead, which is universally available. "Cannot find the share" means a typo in <ShareName> โ€” get exact names from the first command's output.
  • Legacy but fast equivalent:
    shell
    net share
    
    Expected result: Lists share names, paths, and remarks in a simple table. If it fails: No real error mode โ€” this command essentially always works, which is exactly why it's listed as the fallback for older builds.
  • Find shares that aren't standard administrative shares ($-suffixed) for review:
    powershell
    Get-SmbShare | Where-Object { $_.Name -notlike "*$" }
    
    Expected result: Only non-administrative, "real" shares (the ones users/apps actually connect to), filtering out C$/ADMIN$/IPC$ noise. If it fails: No error mode; note this filter is imperfect โ€” a custom hidden share intentionally named to end in $ (the classic "hide a share" trick this section's checklist explicitly warns about) will be filtered OUT by this exact query, so also review the FULL Get-SmbShare list from the previous bullet to catch custom name$ shares this filtered view would hide from you.
  • Check NTFS ACLs on a share's underlying folder:
    powershell
    Get-Acl "C:\Shares\Public" | Format-List
    icacls "C:\Shares\Public"
    
    Expected result: A list of access control entries โ€” account + rights (FullControl, Modify, ReadAndExecute, etc.). If it fails: "Cannot find path" โ€” the actual share's local folder path may differ from this placeholder; get the real Path property from Get-SmbShare's output first. Seeing Everyone: FullControl on a share's folder is the classic finding this section is checking for โ€” tighten it to the specific authorized group/users instead using icacls or the Security tab in Explorer.
  • Set up executable file screening via FSRM (GUI): fsrm.msc โ†’ File Screening Management โ†’ right-click File Screens โ†’ Create File Screenโ€ฆ โ†’ select the share path โ†’ Block Executable Files โ†’ Create. Expected result: New file screen appears in the list; attempting to copy an .exe into that path from a client afterward fails with an access-denied-style error. If it fails: fsrm.msc not found/won't open โ€” the File Server Resource Manager role service isn't installed; add it via Server Manager (Add-WindowsFeature FS-Resource-Manager) first. If a required application legitimately needs to write executables to that share, this will break it โ€” verify against the README before applying.
  • Enforce SMB signing to prevent tampering/relay attacks on file traffic โ€” this aligns with CIS Level 1 "Microsoft network server: Digitally sign communications (always)" and "Microsoft network client: Digitally sign communications (always)" Security Options, applicable to both DC and Member Server profiles:
    powershell
    Set-SmbServerConfiguration -RequireSecuritySignature $true -Force
    
    Expected result: Silent on success (the -Force suppresses the confirmation prompt). Verify with Get-SmbServerConfiguration | Select RequireSecuritySignature. If it fails: SMB signing adds CPU overhead and can slow file transfers on older/underpowered clients โ€” not usually enough to matter for a competition VM, but worth knowing. If an older SMB client (e.g. a legacy embedded device the scenario doesn't actually care about) can no longer connect afterward, that's expected โ€” signing requires compatible clients.
  • Disable SMBv1 (legacy, exploitable, rarely needed):
    powershell
    Disable-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -NoRestart
    
    Expected result: Output showing the feature state changing to Disabled; a restart is typically required to fully complete the removal despite -NoRestart deferring it. If it fails: "Feature name SMB1Protocol is unknown" on very old builds where SMBv1 isn't managed as an optional feature the same way โ€” check Get-WindowsOptionalFeature -Online -FeatureName SMB1Protocol first to confirm the exact feature name exists on this build. If any device on the network genuinely still requires SMBv1 (old printers/NAS devices sometimes do), this will break connectivity to it โ€” confirm nothing required depends on it first, though SMBv1 dependence is rare enough in a CyberPatriot scenario that this is usually safe to disable.
  • CIS Level 1 "Network access" tip not yet listed: Network access: Restrict clients allowed to make remote calls to SAM โ€” restricting this to Administrators only reduces remote enumeration of the local account database via file-sharing protocols, and is a fairly low-risk win on a file server.

Tip: Removing a share the README calls "required" (even if its permissions look sloppy) will cost more than it's worth โ€” fix the permissions, don't delete the share, unless explicitly told to.


11. Windows Firewall#

  • Ensure Windows Defender Firewall is enabled on all profiles (Domain, Private, Public) โ€” servers are frequently found with it disabled "for convenience."
  • Set default inbound policy to block, default outbound to allow (unless the scenario needs specific outbound restrictions).
  • Review inbound rules for anything unusual/overly permissive that isn't required by the server's roles (e.g., an inbound rule opening a random high port).
  • Confirm rules exist (and are enabled) for the roles this server legitimately provides (AD DS, DNS, DHCP, File/Print, RDP if required) โ€” don't block your own required services.
  • Log dropped/blocked packets if you want the extra audit trail (optional, low risk).

Things to try / extra points#

  • Enable all profiles and set default policy in one shot:
    cmd
    netsh advfirewall set allprofiles state on
    netsh advfirewall set allprofiles firewallpolicy blockinbound,allowoutbound
    
    What "allprofiles" means
    Windows Firewall has three separate profiles โ€” Domain, Private, Public โ€” each with its own on/off state and rules, because you might want different rules depending on what network you're connected to. allprofiles applies the command to all three at once instead of having to repeat it three times with different profile names.
    Expected result: Both print Ok. If it fails: "The following command was not found" on very old syntax variants โ€” modern netsh advfirewall syntax as shown works on Server 2008 R2 and later; if genuinely on something older, use the GUI (wf.msc โ†’ Properties) instead. This can immediately break RDP/remote management if you're connected remotely and the required inbound rules aren't already in place โ€” verify your own access method has an allow rule before/immediately after running this, per the section's own required-service warning below.
  • Review current rules for anything unexpected:
    powershell
    Get-NetFirewallRule | Where-Object { $_.Enabled -eq "True" -and $_.Direction -eq "Inbound" } | Select-Object DisplayName, Action, Profile
    
    Expected result: A table of every enabled inbound rule with its action (Allow/Block) and which profile(s) it applies to. If it fails: No error mode; a long list is normal (many built-in rules exist for Windows components) โ€” focus on custom-looking rule names or rules opening unusual high ports rather than trying to review every single built-in entry.
  • Cross-check listening ports against firewall rules and running processes to spot something that shouldn't be open:
    powershell
    Get-NetTCPConnection -State Listen | Select-Object LocalAddress, LocalPort, OwningProcess | Sort-Object LocalPort
    Get-Process -Id <PID>
    
    Expected result: First command lists every listening TCP port with the owning process ID; second resolves a specific PID to its process name/path. If it fails: No error mode for either; an unfamiliar port with a process name you don't recognize is the actual thing to chase down โ€” Get-Process -Id <PID> | Select Path gives the full binary location, which is often the fastest way to judge whether something is legitimate.
  • This section aligns with CIS Level 1 "Windows Firewall with Advanced Security" guidance (same for DC and Member Server profiles) โ€” firewall state On, default inbound block/outbound allow for all three profiles (Domain, Private, Public) are core baseline items. A few more Level 1 firewall items worth checking while you're here:
    • Confirm each profile is set to not display a notification when a program is blocked (avoids inconsistent behavior/interruptions during scoring) โ€” optional but consistent with CIS's "settings" guidance for each profile.
    • Enable logging of dropped packets for each profile and increase the log file size limit well above the tiny Windows default, so evidence isn't immediately overwritten โ€” CIS recommends a substantially larger log size than the out-of-box default, though the exact recommended figure has varied across benchmark versions, so don't worry about hitting an exact number.
    powershell
    Set-NetFirewallProfile -Profile Domain,Private,Public -LogBlocked True -LogFileName "%SystemRoot%\System32\LogFiles\Firewall\pfirewall.log"
    
    Expected result:
    • Silent on success.
    • Confirm with Get-NetFirewallProfile | Select Name, LogBlocked, LogFileName; the log file itself appears at that path once traffic is actually blocked.
    If it fails:
    • "Access is denied" writing to the default log path is unusual for an elevated session but can happen on a heavily locked-down image โ€” redirect -LogFileName to a path you've confirmed is writable (e.g. %SystemRoot%\Temp\pfirewall.log) if so.
    • The log file won't appear until something is actually blocked and logged โ€” an empty/missing file immediately after running this isn't a failure, just means nothing's triggered it yet.

Tip: Before you block inbound broadly, make sure you know which ports AD/DNS/DHCP/RDP actually need (e.g., 53 DNS, 67/68 DHCP, 88 Kerberos, 389/636 LDAP, 445 SMB, 3389 RDP) โ€” a heavy-handed lockdown that breaks the domain controller's own required ports is a net loss.


12. Services Hardening#

Do this AFTER completing the Roles & Features audit in Section 3 โ€” many services underpin roles you've already decided to keep, so you should know what's "supposed" to be running before disabling anything.

  • Review all running services; identify anything non-standard or unexplained.
  • Disable/stop services clearly not required (Telnet, TFTP, Remote Registry, and similar legacy/high-risk services) โ€” unless required by the README.
  • Do NOT disable services underpinning your confirmed server roles (e.g., NTDS, DNS, DHCPServer, Netlogon, W32Time, LanmanServer for file shares).
  • Check service accounts โ€” services running as LocalSystem unnecessarily, or with weak/known passwords for custom service accounts, are worth reviewing.
  • Check for unquoted service paths with spaces (privilege escalation vector).

Things to try / extra points#

  • List running services for review:

    powershell
    Get-Service | Where-Object { $_.Status -eq "Running" } | Select-Object Name, DisplayName
    
    Expected result:A table of every currently-running service's short name and display name.
    If it fails:No error mode; a long list is normal on a server with multiple roles โ€” cross-reference against the "don't disable these" list in the checklist bullets above before touching anything unfamiliar.
  • Disable common legacy/high-risk services if present and unneeded:

    powershell
    Set-Service -Name "RemoteRegistry" -StartupType Disabled -Status Stopped
    Set-Service -Name "TlntSvr" -StartupType Disabled -Status Stopped
    
    Expected result:Silent on success.
    If it fails:
    • "Cannot find any service with service name" for TlntSvr just means Telnet Server isn't installed on this image at all โ€” nothing to do, not an error worth chasing.
    • Remote Registry being genuinely needed for a remote-management tool the README requires is rare but possible โ€” verify first.
  • Find services vulnerable to unquoted-path privilege escalation:

    powershell
    Get-CimInstance Win32_Service | Where-Object { $_.PathName -notlike '"*' -and $_.PathName -like '* *' } | Select-Object Name, PathName
    
    Expected result:Lists services whose executable path contains a space and isn't wrapped in quotes โ€” a real vulnerability (Windows tries each space-delimited segment as a possible executable, which an attacker can exploit by planting a malicious file at an earlier segment) if any hits come back.
    If it fails:No error mode; a hit needs manual remediation โ€” wrap the actual path in quotes via the service's registry ImagePath value (HKLM:\SYSTEM\CurrentControlSet\Services\<ServiceName>) or the vendor's installer/config tool if one exists, then restart the service to confirm it still starts correctly with the quoted path.
  • Cross-reference: for a DC, confirm these core services are running before/after any service cleanup: NTDS, Netlogon, DNS, KDC, W32Time, ADWS, DFSR (or NtFrs on older domains).

    powershell
    Get-Service NTDS, Netlogon, DNS, KDC, W32Time, ADWS
    
    Expected result:All listed as Running on a healthy DC.
    If it fails:
    • "Cannot find any service with service name 'DNS'" means the DNS role genuinely isn't installed on this DC โ€” not every DC also runs DNS, so a missing service here isn't automatically wrong, just confirm against what roles Section 3's audit found.
    • Any of these showing Stopped on a box that SHOULD have that role is a serious, high-priority finding โ€” restart it immediately (Start-Service <name>) and investigate why it stopped.
  • This aligns with CIS Benchmark "System Services" guidance (Level 1, both profiles), which recommends a "Disabled" or "Manual" startup type for a list of legacy/unnecessary built-in services not required by most scenarios (Telnet, TFTP, Simple TCP/IP Services, Fax, Print Spooler on non-print DCs, Remote Registry, etc.). Cross-check your running-services list above against that theme rather than trying to memorize an exact service list.

  • OpenSSH Server (sshd) and Windows Subsystem for Linux (LxssManager) โ€” two modern optional features that CIS Level 1 says should be disabled/uninstalled unless explicitly required, and that a server-focused checklist can easily miss since they're client-image habits. Either one, left enabled unnecessarily on a server, is a full remote-access or Linux-userland attack-surface expansion:

    powershell
    Get-WindowsCapability -Online | Where-Object Name -like "OpenSSH.Server*"
    Get-Service sshd, LxssManager -ErrorAction SilentlyContinue
    # If present and not required by the README:
    Stop-Service sshd -ErrorAction SilentlyContinue; Set-Service sshd -StartupType Disabled -ErrorAction SilentlyContinue
    Set-Service LxssManager -StartupType Disabled -ErrorAction SilentlyContinue
    
    Expected result:Get-WindowsCapability shows install state (Installed/NotPresent) for OpenSSH Server; Get-Service shows current status for both if present (silently returns nothing for either that isn't installed, thanks to -ErrorAction SilentlyContinue).
    If it fails:
    • No real error mode given the suppression flags โ€” absence of both services is the common, expected case on most server images (neither is installed by default) and means this whole bullet is a no-op, not a failure.
    • If the README specifically requires SSH access to this server (some Linux-interop scenarios do), don't disable sshd โ€” harden its sshd_config instead using the same guidance as the Linux checklist's SSH section.

Tip: When in doubt about a service tied to a required role, set it to the correct startup type and leave it running rather than disabling it outright โ€” a stopped critical service is one of the fastest ways to silently fail a scored check.


13. Windows Updates#

  • Check Windows Update configuration/history โ€” determine if updates are being managed locally, via WSUS, or are disabled.
  • Install available critical/security updates if the environment allows internet or WSUS access (note: many competition images are offline โ€” don't assume internet access; check the README for guidance).
  • Ensure automatic updates aren't configured to something that silently blocks all patching if the scenario expects you to patch.
  • If offline, at minimum ensure the Windows Update service itself isn't maliciously disabled, and update settings aren't configured to block a legitimate internal WSUS server.

Things to try / extra points#

  • Check update history and pending updates:
    powershell
    Get-HotFix | Sort-Object InstalledOn -Descending
    
    Expected result: A table of installed updates (KB numbers) with install dates, most recent first. If it fails: InstalledOn showing blank for some entries is a known cosmetic quirk of Get-HotFix on certain builds โ€” not a real error; cross-check with Get-WmiObject Win32_QuickFixEngineering for an alternate view if the dates matter for a forensics question. A very old InstalledOn date on everything is itself the finding (this box hasn't been patched in a long time), not a broken command.
  • Check Windows Update service state:
    powershell
    Get-Service wuauserv
    
    Expected result: Shows current status โ€” typically Running or Stopped (Windows Update service is often stopped between update checks even on a healthy system, which is normal, not a vulnerability by itself). If it fails: No error mode; if StartType shows Disabled (not just currently stopped), that's the actual finding โ€” a maliciously disabled Windows Update service prevents ALL future patching regardless of connectivity; fix with Set-Service wuauserv -StartupType Manual (its normal default).
  • If WSUS is configured, confirm the pointed-to server is the legitimate internal one, not a rogue redirect:
    powershell
    Get-ItemProperty "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -ErrorAction SilentlyContinue
    
    Expected result: If WSUS is configured via policy, shows the WUServer/WUStatusServer values โ€” verify these point at the actual internal WSUS server the README/network topology describes. If it fails: No output (with -ErrorAction SilentlyContinue suppressing the "not found" error) just means WSUS isn't configured via this policy path at all โ€” the server is using default Windows Update/Microsoft Update instead, which isn't inherently wrong. A WUServer value pointing at an unfamiliar IP/hostname is a real, serious finding โ€” that's a mechanism by which an attacker could push malicious "updates."

Tip: In most CyberPatriot images, do NOT run a full Windows Update pass blindly early on โ€” some images are intentionally offline/air-gapped and a stuck update check can waste significant round time. Check connectivity and README guidance first.

Why patching a Domain Controller specifically matters right now: 2026 has been a heavy year for critical, DC-targeting Windows vulnerabilities โ€” multiple Active Directory Domain Services remote-code-execution flaws (including one with a CVSS of 8.8 exploitable by any domain-authenticated attacker with no user interaction) and a critical Kerberos Key Distribution Center RCE affecting Server 2012 through Server 2025 domain controllers, all patched via routine cumulative updates across the year. There's also a critical, unauthenticated, network-reachable Windows DNS Server RCE (CVSS 9.8) โ€” directly relevant if this box is running the DNS role from Section 8. None of this means you should chase individual CVE numbers during a round โ€” it means the generic "make sure cumulative updates are actually installed" check above is disproportionately high-value on a DC compared to a regular workstation, since a DC is both the most attractive target and the one most likely to be running AD DS, Kerberos, and DNS all at once.


14. Remote Desktop / RDS / Remote Access#

  • Determine if Remote Desktop is required by the README. If not required, disable it. If required, harden it rather than disabling it.
  • Enforce Network Level Authentication (NLA) for RDP connections.
  • Restrict Remote Desktop Users group membership to only authorized accounts.
  • If Remote Desktop Services (RDS) role (Session Host/full RDS deployment) is installed, verify it's actually required by the scenario โ€” this is a heavier role than basic RDP and is a common "rogue role" candidate.
  • Disable Remote Assistance and RDP Shadowing unless explicitly required.
  • Review WinRM/PowerShell Remoting configuration โ€” ensure it's not accepting unencrypted traffic or trusting wildcard hosts.

Things to try / extra points#

  • Disable RDP entirely if not required:
    powershell
    Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server" -Name "fDenyTSConnections" -Value 1 -Type DWord
    
    Expected result: Silent on success; RDP connections to this box are refused immediately (no reboot required for this particular setting). If it fails: If you're managing this box over RDP right now, running this command disconnects you immediately โ€” only run it from local/console access, or via a different remote-management channel (WinRM/PowerShell remoting) you're certain will remain available. If disabled by mistake and you're now locked out remotely, you need local/hypervisor console access to re-enable it (set the value back to 0).
  • If RDP IS required, enforce NLA instead of disabling โ€” this aligns with CIS Level 1 "Require user authentication for remote connections by using Network Level Authentication," applicable to both DC and Member Server profiles:
    powershell
    Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" -Name "UserAuthentication" -Value 1 -Type DWord
    
    Expected result: Silent on success; new RDP connection attempts now require NLA (pre-authentication before the full session negotiates) โ€” visible as a policy difference in the RDP client's connection behavior. If it fails: Very old RDP clients that don't support NLA will be unable to connect at all after this change โ€” if the scenario requires access from an old/embedded RDP client, this could break required functionality; verify first. If you don't see a behavior change, confirm you're editing the right sub-key (WinStations\RDP-Tcp, not the parent Terminal Server key used by the disable-RDP setting above โ€” easy to conflate the two paths).
  • CIS Level 1 item not yet listed: Set client connection encryption level โ†’ High Level (under Remote Desktop Session Host settings) โ€” ensures RDP sessions use strong encryption rather than the client-negotiated default.
  • Device redirection hardening โ€” CIS Level 2 items covering what a connecting RDP client is allowed to redirect into the session: disallow LPT port redirection, disallow WebAuthn redirection (prevents a remote client's security key/biometric device from being usable inside the session, closing an unusual credential-relay path), and restrict UI Automation redirection. Lower priority than NLA/encryption but a legitimate "extra credit" pass if this server is RDP-reachable:
    powershell
    Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" -Name "fDisableLPT" -Value 1 -Type DWord
    Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" -Name "DisableWebAuthnRedirection" -Value 1 -Type DWord
    
    Expected result: Silent on success (or creates the key path if missing โ€” add -Force if you get a path-not-found error, since this policy path may not exist yet on a box that's never had these settings configured). If it fails: "Cannot find path" โ€” add -Force to create the key. Low functional risk โ€” LPT redirection and WebAuthn redirection are rarely relied upon in a competition scenario, so this is a fairly safe Level 2 item to apply broadly.
  • Audit who's allowed to RDP in:
    powershell
    Get-LocalGroupMember -Group "Remote Desktop Users"
    
    Expected result: Lists accounts/groups currently in the Remote Desktop Users local group. If it fails: Empty result is fine if RDP access is granted only to Administrators (who can always RDP in regardless of this group's membership) โ€” don't assume empty means "nobody can RDP." Remove anyone unauthorized with Remove-LocalGroupMember -Group "Remote Desktop Users" -Member "<name>".
  • Disable Remote Assistance and RDP shadowing:
    powershell
    Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Remote Assistance" -Name "fAllowToGetHelp" -Value 0
    Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" -Name "Shadow" -Value 0
    
    Expected result: Silent on success; Remote Assistance invitations can no longer be created, and RDP session shadowing (an admin silently viewing/controlling another user's active RDP session) is disabled. If it fails: "Property Shadow does not exist" โ€” add -Force, or create the key first with New-Item if the whole Terminal Services policy path is missing. Low functional risk โ€” neither feature is commonly relied upon in competition scenarios.
  • Lock down WinRM trusted hosts and require encryption:
    powershell
    Get-Item WSMan:\localhost\Client\TrustedHosts
    Set-Item WSMan:\localhost\Client\TrustedHosts -Value "" -Force
    Set-Item WSMan:\localhost\Service\AllowUnencrypted -Value $false -Force
    
    Expected result: Get-Item shows the current trusted-hosts list (often *, meaning "trust anyone," on an unhardened image โ€” that's the finding). The Set-Item calls clear it to empty and disable unencrypted WinRM traffic. If it fails: "Access is denied" โ€” the WinRM service/client config requires an elevated session. If your own remote-management tooling (scoring engine, a teammate's remote PowerShell session) relies on WinRM with the current TrustedHosts value, clearing it to empty can break that connectivity โ€” same caution as the RDP-disable warning above; know how you're connecting before locking this down. If WinRM itself isn't configured yet (WinRM: command not found-style errors), run Enable-PSRemoting first, or skip this bullet if WinRM isn't in use on this box at all.

Tip: If the scoring engine or your team connects to the box via RDP for management, disabling RDP outright can lock everyone out โ€” confirm how the box is being accessed before flipping this off.


15. IIS Web Server Hardening#

Applies if the Web Server (IIS) role is installed.

  • First, browse the site from your host machine's browser before changing anything โ€” observe how it handles bad/missing-page requests, and get a feel for expected normal behavior.
  • Inspect the default content folder C:\inetpub\wwwroot โ€” look for anything beyond the expected iisstart.html/Default.htm/welcome.png. Open unfamiliar HTML files in Notepad and images in an image viewer to check for tampering. Delete non-critical/unexpected content.
  • Server Manager โ†’ Roles โ†’ right-click Web Server (IIS) โ†’ Remove Role Services โ€” uncheck anything not needed. The most commonly exploited extras are ASP.NET, CGI, and FTP Server โ€” remove if not explicitly required.
  • In IIS Manager (inetmgr):
    • Authentication: disable Anonymous Authentication if the scenario allows (test that the site still functions as required afterward โ€” some scenarios intentionally need anonymous access and disabling it costs functionality points).
    • Default Document: should be a short, standard ordered list (Default.htm, Default.asp, index.htm, index.html, iisstart.htm) โ€” remove anything unexpected.
    • Error Pages: verify custom error paths look legitimate (standard <LANGUAGE-TAG> defaults to en-US).
    • Handler Mappings: by default only OPTIONSVerbHandler, TRACEVerbHandler, and StaticFile should be present โ€” investigate anything extra.
    • HTTP Request Headers: should typically be empty โ€” remove anything present.
    • Logging: ensure it's enabled (not greyed out).
    • Modules: all module types should show as Native; Request Filtering, Output Caching, and Server Certificates (unless HTTPS is in use) should be empty by default.
    • Shared Configuration: disable (uncheck) unless explicitly required.
  • Disable dangerous/unnecessary HTTP verbs (TRACE, TRACK, and typically OPTIONS/DELETE) via Request Filtering.
  • Enforce TLS 1.2/1.3 and disable SSLv3/TLS 1.0/TLS 1.1 at the SChannel registry level.
  • Remove executable rights from upload/content directories (e.g., /uploads/, /images/) โ€” restrict handler access policy to Read/Script only, not Execute.

Things to try / extra points#

  • Deny dangerous verbs via PowerShell:
    powershell
    Import-Module WebAdministration
    Add-WebConfigurationProperty -Filter /system.webServer/security/requestFiltering/verbs -PSPath "IIS:\" -Name "." -Value @{verb='TRACE'; allowed='false'}
    Add-WebConfigurationProperty -Filter /system.webServer/security/requestFiltering/verbs -PSPath "IIS:\" -Name "." -Value @{verb='TRACK'; allowed='false'}
    
    Expected result: Silent on success. Confirm via IIS Manager โ†’ Request Filtering โ†’ HTTP Verbs tab, or by sending an actual TRACE request to the site and confirming it's rejected (403). If it fails: "The term 'Import-Module WebAdministration' ... module could not be loaded" means the IIS Management Scripts and Tools feature isn't installed โ€” add it via Server Manager (Add-WindowsFeature Web-Scripting-Tools) first. Duplicate entries (running this twice) can error "already exists" โ€” harmless, means it's already configured.
  • Disable legacy TLS/SSL protocols:
    powershell
    $Path = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols"
    New-Item -Path "$Path\TLS 1.0\Server" -Force | Out-Null
    Set-ItemProperty -Path "$Path\TLS 1.0\Server" -Name "Enabled" -Value 0 -Type DWord
    New-Item -Path "$Path\TLS 1.1\Server" -Force | Out-Null
    Set-ItemProperty -Path "$Path\TLS 1.1\Server" -Name "Enabled" -Value 0 -Type DWord
    
    Expected result: Silent on success; requires a reboot to fully take effect (SChannel protocol settings are read at boot/service-start, not live). If it fails: No common error beyond permission issues. Older clients that only support TLS 1.0/1.1 will be unable to connect via HTTPS afterward โ€” if the scenario has legacy client requirements, verify before applying broadly; for a normal competition scenario this is almost always safe and expected.
  • Restrict handler execute rights on a specific directory (e.g., uploads):
    powershell
    Set-WebConfigurationProperty -Filter /system.webServer/handlers -Name accessPolicy -Value "Read, Script" -PSPath "IIS:\Sites\Default Web Site\uploads"
    
    Expected result: Silent on success; uploading and then attempting to EXECUTE a script/binary from that directory should now fail even if the upload itself succeeds. If it fails: "Cannot find path 'IIS:\Sites\Default Web Site\uploads'" โ€” the site name or subfolder doesn't match this server's actual structure; list real site names with Get-Website and real virtual directories with Get-WebVirtualDirectory first.
  • Disable anonymous auth via PowerShell:
    powershell
    Set-WebConfigurationProperty -Filter /system.webServer/security/authentication/anonymousAuthentication -Name enabled -Value False -PSPath IIS:\
    
    Expected result: Silent on success; browsing the site afterward should now prompt for credentials instead of loading directly. If it fails: If the scenario's website is supposed to be publicly viewable without login, this breaks that requirement โ€” exactly the warning in the checklist tip below; test the site's actual expected behavior immediately after this change and revert (-Value True) if it broke something required.
  • Audit installed native/managed modules for anything resembling a webshell or unauthorized ISAPI filter:
    powershell
    Get-WebGlobalModule
    Get-WebManagedModule
    
    Expected result: Lists installed IIS modules by name and image path โ€” cross-reference against the standard default module list mentioned in the checklist bullets above (mostly Native type on a default install). If it fails: No error mode; an unfamiliar module name or one pointing at a DLL in an unusual path (not the standard %windir%\System32\inetsrv\ location) is the actual finding worth investigating directly.
  • If the server hosts a backend application (PHP/ASP.NET with a database, etc.), also work through a web-application-specific checklist in addition to this one โ€” general IIS hardening alone won't cover app-level injection/auth flaws.
  • Note on CIS coverage: IIS hardening isn't part of the Windows Server Benchmark at all โ€” CIS publishes a separate CIS Microsoft IIS Benchmark. The items in this section come from Microsoft/general web-hardening guidance rather than the Windows Server benchmark; if your team wants deeper IIS-specific coverage, that's the document to pull from, not this one.

Tip: Disabling Anonymous Authentication can break a site that's supposed to be publicly viewable in the scenario. Test the site's expected use case immediately after the change; if it breaks required functionality, re-enable and look for a different fix (e.g., restricting by NTFS ACL instead).


16. Local Accounts & Built-In Accounts (On the Server Itself)#

Distinct from AD/domain accounts โ€” this is the local SAM database on the server itself (relevant even on a DC for the local Administrator, and very relevant on member/standalone servers).

  • Enumerate local accounts (including any hidden from Control Panel) and local group memberships โ€” local admin group membership is separate from Domain Admins and is frequently overlooked.
  • Disable unused built-in accounts: Guest, DefaultAccount, WDAGUtilityAccount (if present and unused).
  • Rename the built-in Administrator account (also enforced via GPO in Section 5 for domain-joined boxes, but verify locally too).
  • Review local Administrators group membership on the server โ€” remove any account (local or domain) that doesn't need local admin rights on this specific box.
  • Review local Remote Desktop Users group membership.

Things to try / extra points#

  • Dump all local accounts, including ones not shown in Control Panel:
    powershell
    Get-LocalUser | Select-Object Name, Enabled, AccountExpires, LastLogon, PasswordRequired
    
    Expected result: A table of every local (non-domain) account on this specific machine's SAM database. If it fails: "not recognized" on very old builds โ€” fall back to net user (universally available). Remember: on a DC, most user management happens in AD, not here โ€” this cmdlet only shows the small set of genuinely local accounts (built-in Administrator/Guest and any locally-created ones).
  • Audit local privileged group membership (separate from Get-ADGroupMember โ€” this is the box's own local SAM):
    powershell
    Get-LocalGroupMember Administrators
    Get-LocalGroupMember -Group "Remote Desktop Users"
    
    Expected result: Lists local AND domain accounts/groups that have been granted local admin / local RDP rights on this specific machine. If it fails: No error mode; the important distinction to remember (per the checklist bullet above) is that this is separate from Domain Admins โ€” a domain account can be a completely unprivileged domain user and STILL be a local admin on this one box if someone added it here specifically, which is exactly what this check is meant to catch.
  • Disable unused built-ins and rename Administrator:
    shell
    net user Guest /active:no
    net user DefaultAccount /active:no
    wmic useraccount where name='Administrator' call rename name='RenamedAdmin'
    
    Expected result: Each net user command prints "The command completed successfully."; the wmic rename shows ReturnValue = 0; in its output block. If it fails: "Access is denied" means not elevated. If you're currently logged in AS the Administrator account, renaming it can be disorienting but won't log you out mid-session โ€” your existing session stays valid, but be aware the username changes for any NEW login. wmic is removed by default on Windows 11 24H2+ โ€” if "not recognized," rename via Rename-LocalUser -Name "Administrator" -NewName "RenamedAdmin" (PowerShell) or the Local Users and Groups GUI instead.
  • This section aligns with CIS Level 1 "Accounts" Security Options, consistent across DC and Member Server profiles: Accounts: Guest account status โ†’ Disabled, Accounts: Rename administrator account, Accounts: Rename guest account. One more Level 1 item worth adding here: Accounts: Limit local account use of blank passwords to console logon only โ†’ Enabled โ€” prevents a local account with a blank password from being used for network logon.

Tip: On a Domain Controller, "local" accounts are limited (DCs share a single SAM/AD database for most purposes) โ€” but the local Administrators group concept still matters on member servers and standalone file/print/web boxes. Don't skip this section just because the box is domain-joined.


17. Event Log & Audit Policy#

  • Enable auditing across key categories: Account Management, Logon/Logoff, Policy Change, Privilege Use, System, Object Access.
  • Check the Security log for failed logon attempts (Event ID 4625) and account lockouts (Event ID 4740) โ€” these can reveal brute-force attempts or a misconfigured service account, and are common forensics-question fodder.
  • Check for account creation events (Event ID 4720) to spot when an unauthorized account was added.
  • Ensure log sizes are reasonably large and set to not overwrite too quickly (so evidence isn't lost) โ€” but don't fight the scenario if a specific log size is mandated.
  • Enable PowerShell Script Block Logging for better visibility into what's been run on the box.

Things to try / extra points#

  • Enable broad category auditing (fast baseline):
    shell
    auditpol /set /category:"Account Management" /success:enable /failure:enable
    auditpol /set /category:"Logon/Logoff" /success:enable /failure:enable
    auditpol /set /category:"Policy Change" /success:enable /failure:enable
    auditpol /set /category:"Privilege Use" /success:enable /failure:enable
    auditpol /set /category:"System" /success:enable /failure:enable
    
    What auditpol is configuring
    Windows can log security events, but which categories get logged is off by default for most of them. auditpol /set turns on logging for a specific category โ€” here, "Account Management" (things like account creation/deletion/changes). /success:enable /failure:enable means log both successful AND failed attempts in that category, not just one or the other.
    Or enable everything (maximum logging, use if the scenario emphasizes forensics/audit points):
    shell
    auditpol /set /category:* /success:enable /failure:enable
    
    Expected result: Each line prints The command was successfully executed. If it fails: "The category name is unrecognized" โ€” category names are exact and version-sensitive; run auditpol /list /category:* to get this build's exact recognized names rather than retyping from memory. Access denied means not elevated.
  • Query failed logons directly for forensics/lockout investigation:
    powershell
    Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625} -MaxEvents 25
    Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4740} -MaxEvents 25
    
  • Query recent account-creation events:
    powershell
    Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4720} -MaxEvents 25
    
    Expected result (all three Get-WinEvent queries): Up to 25 matching event records, newest first, with timestamp, account, and details. If it fails: "No events were found that match the specified selection criteria" is a normal result meaning nothing of that type has happened yet (or the relevant audit category isn't enabled โ€” see the bullet just above; enable auditing FIRST, then these queries will start finding events going forward, but won't retroactively find events from before auditing was on).
  • Enable PowerShell logging:
    powershell
    New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" -Force | Out-Null
    Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" -Name "EnableScriptBlockLogging" -Value 1
    
    Expected result: Silent on success. Verify by running any PowerShell command afterward and checking Event Viewer โ†’ Applications and Services Logs โ†’ Microsoft โ†’ Windows โ†’ PowerShell โ†’ Operational for a new Script Block logging event (Event ID 4104). If it fails: No real failure mode for the registry write itself. If events still aren't appearing, confirm the PowerShell Operational log itself is enabled (right-click the log in Event Viewer โ†’ check "Enable Logging" if greyed out/disabled) โ€” the registry policy controls WHAT gets logged, but the log channel itself also needs to be turned on to actually record it.
  • This section aligns closely with CIS Benchmark Level 1 "Advanced Audit Policy Configuration" (both DC and Member Server profiles) โ€” the broad-category auditpol commands above cover most of what CIS asks for. A few more granular CIS Level 1 audit subcategories worth explicitly enabling if you want closer alignment:
    shell
    auditpol /set /subcategory:"Credential Validation" /success:enable /failure:enable
    auditpol /set /subcategory:"Security Group Management" /success:enable /failure:enable
    auditpol /set /subcategory:"Other Account Management Events" /success:enable /failure:enable
    
  • DC-specific CIS Level 1 subcategories (these only make sense on a Domain Controller โ€” the CIS Domain Controller profile calls these out specifically, they don't apply to the Member Server profile):
    shell
    auditpol /set /subcategory:"Directory Service Access" /success:enable /failure:enable
    auditpol /set /subcategory:"Directory Service Changes" /success:enable /failure:enable
    auditpol /set /subcategory:"Kerberos Authentication Service" /success:enable /failure:enable
    auditpol /set /subcategory:"Kerberos Service Ticket Operations" /success:enable /failure:enable
    
    Expected result (both subcategory blocks): Each line prints The command was successfully executed. If it fails: "The subcategory name is unrecognized" โ€” same fix as the category-level version above: auditpol /list /subcategory:* shows exact recognized names on this build. Running the DC-specific block on a non-DC member server will still often succeed syntactically (the subcategory names exist generically) but won't generate meaningful events since there's no AD DS/Kerberos KDC role generating that activity โ€” harmless to run, just not useful there.
  • CIS also recommends significantly increasing the default Application/Security/System event log maximum size well beyond the small out-of-box default, so a burst of activity doesn't overwrite evidence before you can review it โ€” exact size thresholds have varied across benchmark versions, so treat "much bigger than default" as the actionable guidance rather than chasing one specific number.

Tip: Turning on full auditing can generate a LOT of Security log noise fast โ€” if you need to find a specific event later, filter by Event ID and a tight time window rather than scrolling manually.


18. Registry, System Hardening & Exploit Mitigations#

General Windows hardening still applies to servers โ€” apply judiciously and re-test required functionality after each change.

  • Enforce UAC to always prompt on the secure desktop.
  • Disable AutoRun/AutoPlay globally.
  • Restrict null sessions / anonymous SAM enumeration.
  • Disable LLMNR/NBT-NS/mDNS to reduce local name-resolution poisoning risk (verify this doesn't break internal name resolution the scenario depends on).
  • Enforce SMB signing; disable SMBv1.
  • Enable system-wide exploit mitigations (DEP, SEHOP, ForceRelocateImages).
  • Protect LSASS as a protected process to hinder credential-dumping tools.
  • Restrict removable media (USB mass storage) if appropriate to the scenario.
  • Set PowerShell execution policy to Restricted unless scripts are required to run.
  • Set Boot-Start Driver Initialization Policy (Early Launch Antimalware) to only load Good/Unknown drivers at boot.
  • Disable "Shutdown: Allow system to be shut down without having to log on."

Things to try / extra points#

  • Most of the items in this section map onto CIS Level 1 "Security Options" / "Administrative Templates" guidance (both DC and Member Server profiles) โ€” UAC behavior, AutoPlay, null-session/anonymous restrictions, and SMB signing are all standard baseline items. LLMNR/mDNS disabling and PowerShell execution policy restriction are good practice but are not core CIS Windows Server Benchmark controls โ€” they come from broader Microsoft security guidance, so don't expect to find them called out by that name in the benchmark itself.
  • Early Launch Antimalware boot policy โ€” CIS Level 1, both profiles. Stops a malicious or unsigned boot-start driver from loading before AV gets a chance to scan it:
    powershell
    New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\EarlyLaunch" -Force | Out-Null
    Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\EarlyLaunch" -Name "DriverLoadPolicy" -Value 3 -Type DWord
    
    Expected result: Silent on success; takes effect on next boot. If it fails: No common error beyond permissions. Value 3 = "Good and unknown" (blocks known-bad only) โ€” the more aggressive 1 ("Good only") can prevent legitimate unsigned/unknown drivers required by the scenario's hardware from loading; 3 is the safer default unless you specifically know every driver on this box is signed.
  • "Shutdown: Allow system to be shut down without having to log on" โ€” CIS Level 1 Security Options item, should be Disabled. On by default this lets anyone at the physical console (or console-equivalent) power off the server from the logon screen without ever authenticating โ€” a low-tech but real availability/DoS-adjacent finding that's easy to miss because it's not framed as an "access" setting:
    powershell
    # secpol.msc / GPMC โ†’ Security Options โ†’ "Shutdown: Allow system to be shut down without having to log on" โ†’ Disabled
    secedit /export /cfg C:\shutdownpol.cfg /areas SECURITYPOLICY
    # Edit C:\shutdownpol.cfg, set: ShutdownWithoutLogon = 0
    secedit /configure /db C:\Windows\security\local.sdb /cfg C:\shutdownpol.cfg /areas SECURITYPOLICY
    
    Expected result: Both secedit commands print The task has completed successfully. If it fails: Same secedit troubleshooting as Section 6's password-policy walkthrough โ€” a malformed edited line shows as "completed with one or more errors," check %windir%\security\logs\scesrv.log for specifics. On a DC, remember local secedit-based policy can be overridden by domain GPO โ€” set this via GPMC's Default Domain Policy instead if it doesn't stick.
  • UAC hardening:
    powershell
    Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "EnableLUA" -Value 1 -Type DWord
    Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "ConsentPromptBehaviorAdmin" -Value 2 -Type DWord
    Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "PromptOnSecureDesktop" -Value 1 -Type DWord
    
  • Disable AutoRun/AutoPlay:
    powershell
    Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer" -Name "NoDriveTypeAutoRun" -Value 255 -Type DWord
    Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer" -Name "NoAutoplayfornonVolume" -Value 1 -Type DWord
    
  • Restrict null sessions and anonymous enumeration:
    powershell
    Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" -Name "RestrictAnonymous" -Value 1 -Type DWord
    Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" -Name "RestrictAnonymousSAM" -Value 1 -Type DWord
    Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" -Name "EveryoneIncludesAnonymous" -Value 0 -Type DWord
    Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters" -Name "RestrictNullSessAccess" -Value 1 -Type DWord
    
  • Disable LLMNR/mDNS multicast name resolution:
    powershell
    New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient" -Force | Out-Null
    Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient" -Name "EnableMulticast" -Value 0 -Type DWord
    
  • System-wide exploit mitigations:
    powershell
    Set-ProcessMitigation -System -Enable DEP, EmulateAtlThunks, SEHOP, ForceRelocateImages
    
  • Protect LSASS memory from dumping tools (e.g., Mimikatz-style attacks) โ€” flag this as a CIS-adjacent Level 2 item: it's a strong defense-in-depth control but can interfere with some legitimate security/monitoring tools that hook LSASS, so test after applying:
    powershell
    Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" -Name "RunAsPPL" -Value 1 -Type DWord
    
  • Restrict USB mass storage:
    powershell
    Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\USBSTOR" -Name "Start" -Value 4 -Type DWord
    
  • Restrict PowerShell execution policy:
    powershell
    Set-ExecutionPolicy -ExecutionPolicy Restricted -Scope LocalMachine -Force
    
  • Reset system folder ACLs if ownership/permissions look tampered with:
    cmd
    icacls "C:\Windows\System32" /reset /T /C /Q
    
  • System file integrity check if core binaries seem replaced/corrupted:
    shell
    sfc /scannow
    DISM /Online /Cleanup-Image /RestoreHealth
    
    Expected result (all registry/mitigation/policy blocks above): every Set-ItemProperty/New-Item is silent on success โ€” verify with a matching Get-ItemProperty read-back rather than trusting the write alone. Set-ProcessMitigation is also silent; confirm with Get-ProcessMitigation -System. Set-ExecutionPolicy is silent; confirm with Get-ExecutionPolicy -List. icacls /reset prints one line per file/folder processed, ending in a summary of successes/failures. sfc /scannow takes several minutes and ends with one of: no violations found, violations found and repaired, or violations found but NOT all repaired (requiring DISM /RestoreHealth first to fix the underlying component store, then re-running sfc). If it fails: "Cannot find path" on any Set-ItemProperty โ€” add -Force (creates the key) or run the paired New-Item line first if one is shown. RunAsPPL (LSASS protection) can break legitimate security tools or some VPN/smartcard software that hooks LSASS โ€” test required functionality immediately after. Restricting the execution policy to Restricted blocks ALL .ps1 scripts from running, including your own remediation scripts โ€” if you need to keep running PowerShell scripts for the rest of the round, use RemoteSigned instead, or plan to temporarily bypass with -ExecutionPolicy Bypass on a per-invocation basis. icacls /reset on System32 is a heavy hammer โ€” it resets to INHERITED defaults, which is usually correct for cleaning up tampered permissions, but double-check nothing scenario-specific was intentionally customized there first. sfc /scannow reporting unrepairable violations after a DISM /RestoreHealth pass usually means the DISM online repair source itself couldn't fetch clean files (no internet/WSUS) โ€” if this image is offline, this specific repair may not be achievable; note it rather than looping on it.

Tip: Disabling LLMNR/NetBIOS can break name resolution on networks that rely on it for non-DNS hosts โ€” if other scenario machines suddenly can't be reached by name, this is one of the first things to check.


19. Malware, Persistence & Backdoor Hunting#

Windows Defender Antivirus (not to be confused with Windows Defender Firewall in Section 11)#

Server 2016+ ships Windows Defender Antivirus by default (unless the feature was explicitly removed via Server Manager). It's easy to assume "servers don't need AV configuration" and skip this entirely โ€” don't:

  • Confirm real-time protection is actually on, not just installed:

    powershell
    Get-MpComputerStatus | Select AntivirusEnabled, RealTimeProtectionEnabled, AntispywareEnabled
    Get-MpPreference | Select DisableRealtimeMonitoring, PUAProtection
    
  • CIS Level 1: PUA (Potentially Unwanted Application) protection should be set to Block, and Network Protection (blocks connections to known-malicious IPs/domains at the network level) should be enabled โ€” both are off by default and neither shows up if you only check "is Defender on":

    powershell
    Set-MpPreference -PUAProtection Enabled
    Set-MpPreference -EnableNetworkProtection Enabled
    
  • Check for malicious scan exclusions โ€” a whole-drive or user-folder exclusion means malware sitting there is never scanned:

    powershell
    Get-MpPreference | Select ExclusionPath, ExclusionExtension, ExclusionProcess
    
  • Check for Image File Execution Options (IFEO) hijacks โ€” a classic accessibility-tool backdoor (e.g., sethc.exe/utilman.exe remapped to cmd.exe).

  • Check Run/RunOnce registry keys (both HKLM and HKCU, plus WOW6432Node) for unauthorized startup entries.

  • Verify Winlogon Shell and Userinit values are unmodified defaults.

  • Check for WMI event subscriptions used for fileless persistence.

  • Review Scheduled Tasks for anything running elevated or from a non-standard path outside \Microsoft\....

  • Check for AppInit_DLLs / AppCertDlls injection hooks.

  • Scan common drop locations (C:\Users\Public, C:\ProgramData, C:\Windows\Temp) for unauthorized executables/scripts/media.

  • Check Alternate Data Streams (ADS) on user/system folders for hidden payloads.

  • Review the hosts file for unauthorized DNS-spoofing entries.

  • Check for port proxies / port forwarding rules and unexpected static routes.

Things to try / extra points#

  • IFEO hijack check:
    powershell
    Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\*" -ErrorAction SilentlyContinue | Select-Object PSChildName, Debugger
    
  • Startup key audit:
    powershell
    Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run*","HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run*","HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run*"
    
  • Winlogon check (expected: Shell = explorer.exe, Userinit = C:\Windows\system32\userinit.exe,):
    powershell
    Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" | Select-Object Shell, Userinit
    
  • WMI persistence sweep:
    powershell
    Get-CimInstance -Namespace root\subscription -ClassName __EventConsumer
    Get-CimInstance -Namespace root\subscription -ClassName __EventFilter
    
  • Scheduled Task sweep:
    powershell
    Get-ScheduledTask | Where-Object { $_.State -ne "Disabled" -and $_.TaskPath -notlike "\Microsoft*" } | Select-Object TaskName, TaskPath
    
  • DLL injection hook check:
    powershell
    Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows" -Name "AppInit_DLLs" -ErrorAction SilentlyContinue
    Get-ItemProperty "HKLM:\System\CurrentControlSet\Control\Session Manager\AppCertDlls" -ErrorAction SilentlyContinue
    
  • Rogue file sweep:
    powershell
    Get-ChildItem -Path C:\Users\Public, C:\ProgramData, C:\Windows\Temp -Recurse -Include *.mp3,*.mp4,*.avi,*.exe,*.bat,*.vbs,*.ps1 -ErrorAction SilentlyContinue
    
  • Alternate Data Stream sweep:
    powershell
    Get-ChildItem -Path C:\Users\ -Recurse -Stream * | Where-Object { $_.Stream -ne ':$DATA' }
    
  • Hosts file review:
    powershell
    Get-Content C:\Windows\System32\drivers\etc\hosts | Where-Object { $_ -notlike "#*" -and $_.Trim() -ne "" }
    
  • Port proxy / routing table review:
    cmd
    netsh interface portproxy show all
    route print
    
    Expected result (all persistence-hunting checks above): each is read-only and should come back mostly/entirely empty on a clean system โ€” Run keys will have a handful of legitimate vendor entries (that's normal), WMI subscriptions/AppInit_DLLs/AppCertDlls/port proxies should typically be completely empty, Winlogon Shell/Userinit should exactly match the documented expected defaults, and the hosts file should have no active (non-comment) lines beyond maybe a 127.0.0.1 localhost entry. If it fails: No error mode for any of these โ€” they're all pure reads. Get-ChildItem -Stream * can be slow across a large C:\Users tree; that's expected, not a hang. A hit on ANY of these (a Winlogon Shell that isn't explorer.exe, a non-empty AppInit_DLLs, a WMI event consumer/filter pair, a scheduled task outside \Microsoft\... you don't recognize, an ADS attached to an otherwise-normal-looking file) is a genuine, high-priority finding โ€” these are all real, documented persistence techniques, not false-positive-prone heuristics like some earlier bulk-search checks in this document.
  • Admin-tool sabotage check โ€” a recurring technique that disables Task Manager/cmd.exe/Control Panel for the current user via raw registry policy keys, without any corresponding GPO (so gpresult alone won't show it as a policy source โ€” you have to check the registry directly). This matters on a server too: a locked-out cmd.exe can block your own remediation commands.
    powershell
    Get-ItemProperty "HKCU:\Software\Policies\Microsoft\Windows\System" -Name "DisableCMD" -ErrorAction SilentlyContinue
    Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\System" -Name "DisableTaskMgr" -ErrorAction SilentlyContinue
    Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer" -Name "NoControlPanel" -ErrorAction SilentlyContinue
    
    Remove any that are set:
    powershell
    Remove-ItemProperty -Path "HKCU:\Software\Policies\Microsoft\Windows\System" -Name "DisableCMD" -ErrorAction SilentlyContinue
    Remove-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\System" -Name "DisableTaskMgr" -ErrorAction SilentlyContinue
    Remove-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer" -Name "NoControlPanel" -ErrorAction SilentlyContinue
    
    Expected result: The check queries silently return nothing if clean (with -ErrorAction SilentlyContinue suppressing the "not found" noise); a returned value of 1 means that lockdown IS active. The removal commands are silent whether or not the property existed. If it fails: No real error mode given the suppression flags. This entire check only covers HKCU โ€” the currently logged-in user's hive โ€” if the sabotage was applied to a DIFFERENT user's profile, you won't see it here; you'd need to load that user's hive (reg load) to check it, similar to the technique noted in other sections of this document. If Task Manager/cmd.exe are still blocked after removing these values, log off and back on (or open a fresh Explorer session) โ€” some of these policies are read once at logon/shell-start, not live.
  • gpresult /z โ€” dump the full effective policy actually in force (local + Default Domain Policy + any OU-linked GPOs, all combined) as a final verification pass:
    powershell
    gpresult /z > C:\gpresult_after.txt
    
    Expected result: Same as Section 5's /z usage โ€” a large, detailed dump of every effective setting and which GPO/policy source won it. If it fails: Same troubleshooting as Section 5's identical command โ€” no new failure modes here, this is just re-run as a final confirmation pass rather than a first check. On a server this is the only reliable way to see the real resulting policy, since local secpol.msc, the Default Domain Policy, and any OU-specific GPOs all stack โ€” checking secpol.msc alone (especially on a DC, where it's largely inert) will not tell you what's actually being enforced.

Tip: These checks apply to server images too, not just clients โ€” a compromised server is often a more valuable persistence target than a workstation, so don't skip this section just because the box is "just a DC."


20. Backup & Restore Considerations#

  • Confirm whether Windows Server Backup or a similar backup role/feature is required by the README โ€” don't remove it if so.
  • Be aware that AD changes (deleting OUs, GPOs, or accounts) are harder to reverse than client-side tweaks โ€” when uncertain, prefer disabling over deleting.
  • Check Volume Shadow Copies for unexpected staged/hidden files (attackers sometimes stash tools in shadow copy-accessible paths) โ€” but don't indiscriminately delete shadow copies if backups depend on them.
  • If you must make a broad, risky change (e.g., large GPO edit, bulk account changes), document the before-state first (see Section 2) so you can recover context if something breaks.

Things to try / extra points#

  • List existing shadow copies:
    shell
    vssadmin list shadows
    
    Expected result: A list of existing volume shadow copies with creation timestamps, or "No items found that satisfy the query" if none exist (a completely normal result on many images, not an error). If it fails: "Access is denied" โ€” needs an elevated prompt. This is read-only and safe to run any time; don't delete shadow copies unless you're certain backups don't depend on them (per the checklist bullet above).
  • Snapshot critical state before large changes (reuse the same approach as the Forensics section):
    powershell
    Get-GPOReport -All -ReportType Html -Path C:\Users\Public\AllGPOs_Before.html
    
    Expected result: One HTML file containing every GPO's full settings report, written to the given path. If it fails: Same GroupPolicy-module/RSAT dependency as every other GPO cmdlet in this document โ€” see Section 5's notes if "not recognized." Can take a while on a domain with many GPOs; that's expected for a full -All report, not a hang.

Tip: Competition rounds are time-boxed โ€” a mistake that requires restoring AD from backup will usually cost you more time than the round has left. Favor smaller, verifiable, reversible changes over big sweeping ones when working on AD-critical infrastructure.


21. Server 2008 vs. 2012+ UI Differences#

Source material for this checklist assumes a 2008 R2-style interface unless otherwise noted; be ready to adapt:

Area Server 2008 / 2008 R2 Server 2012+
Roles/Features management "Roles" and "Features" panes in Server Manager Unified "Manage โ†’ Add Roles and Features" wizard
Server Manager Remoting Simple toggle "Configure Server Manager Remote Management" link, PowerShell Configure-SMRemoting
Start Menu Classic Start Menu โ†’ Administrative Tools Start Screen / searchable Administrative Tools; many admins use Win + R + .msc shortcuts directly (works on both)
IE ESC Present, same registry keys Present; some inconsistencies reported
PowerShell cmdlet availability Fewer built-in AD/DNS/DHCP cmdlets (may need RSAT modules imported explicitly) Much richer built-in module support (ActiveDirectory, DnsServer, DhcpServer)
  • If a GUI setting described in this checklist can't be found on the installed OS version, try the equivalent PowerShell/net/secedit command instead โ€” the underlying setting almost always exists even if the UI path moved.
  • If you run a command-line change and it appears to succeed with no error but nothing changes, always verify with a read-back command before assuming it worked โ€” silent failures are common with older OS versions and cmdlet/module mismatches.
  • On 2008 R2, confirm the ActiveDirectory, DnsServer, DhcpServer, and GroupPolicy PowerShell modules are imported/available before relying on cmdlets from this checklist:
    powershell
    Import-Module ActiveDirectory, DnsServer, DhcpServer, GroupPolicy
    
    Expected result: Silent on success; afterward, cmdlets from all four modules (e.g. Get-ADUser, Get-DnsServerZone, Get-DhcpServerv4Scope, Get-GPO) become available in this session. If it fails: "The specified module was not loaded because no valid module file was found" for any of the four means that role's RSAT tools genuinely aren't installed on this box โ€” install via Add-WindowsFeature RSAT-AD-PowerShell, RSAT-DNS-Server, RSAT-DHCP, or GPMC respectively (only install the ones for roles actually relevant to this server โ€” a plain file server doesn't need DnsServer/DhcpServer modules, for instance). This import only lasts for the current PowerShell session โ€” re-run it (or open a fresh session where the relevant role is already installed, which auto-loads its module) each time you start a new session on 2008 R2-era boxes where auto-loading isn't reliable.

22. Final Sweep / Closing Checklist#

๐Ÿ“ธ Snapshot checkpoint: Take one more snapshot now, before your final verification pass and before any last reboot. If something in this closing sweep reveals a problem (a broken required service, a locked-out account), you want a very recent known-good point to fall back to instead of unwinding hours of hardening changes by hand.

  • Re-read the README one final time โ€” confirm every required role, account, and service is still functioning as specified.
  • Re-run Get-WindowsFeature | Where-Object Installed and compare against your Section 3 baseline โ€” confirm nothing required was accidentally removed.
  • Re-run dcdiag /v (if a DC) and confirm no new errors were introduced by your changes.
  • Confirm DNS resolution still works both internally and (if applicable) externally: nslookup domain.local, nslookup google.com.
  • Confirm DHCP is still issuing leases if required: check Get-DhcpServerv4Lease or test a client renewal.
  • Confirm required shares are still reachable with correct permissions: Get-SmbShare, test access as a non-admin authorized user if possible.
  • Confirm you can still log in as the accounts the README says should have access (don't lock yourself or required service accounts out).
  • Verify Windows Firewall is enabled on all profiles and required ports are still open.
  • Verify all forensics question answers are saved in the expected location/format.
  • Do one last pass through Event Viewer Security log for anything alarming introduced during your own changes (e.g., repeated failed logons from your own testing that could be mistaken for an attack, account lockouts you caused).
  • Save/document a final summary of every change made, in case a teammate needs to pick up where you left off or you need to justify a change in a written question.

Final tip: On server images, the biggest score swings come from (1) not breaking required AD/DNS/DHCP/File/Web functionality, and (2) catching the "big rock" items โ€” rogue roles, unauthorized privileged accounts, open zone transfers, and disabled firewalls โ€” rather than chasing every obscure registry tweak. Work top-down through this checklist, verify as you go, and prioritize breadth over exhaustive depth on any single obscure setting if time is short.