defender: winrm support via native WSMan - #1402
Open
XiaoliChan wants to merge 29 commits into
Open
Conversation
Ported from wmiexec-Pro (https://github.com/XiaoliChan/wmiexec-Pro). Supports check / disable / enable / exclude / remove actions over the smb and wmi protocols against the root/Microsoft/Windows/Defender namespace. Method invocation (disable/enable/exclude/remove) relies on kwargs-style WMI method calling - only supplied InParams are sent, unspecified ones are marked null via NdTable, preserving existing configuration. This requires the impacket wmi-Call branch (pending upstream PR). disable/enable verify the resulting state and warn when settings did not change: on some systems (e.g. Windows 11 as local admin, Tamper Protection) disable/enable is blocked while exclusions still work. Verified against Server 2019 with Cortex XDR present: all actions tested over both smb and wmi protocols, exclusions confirmed added and removed via check.
…MConnection When running over the wmi protocol the module now reuses the protocol's authenticated IWbemLevel1Login instead of creating its own DCOMConnection to the same target - whose disconnect would clash with the protocol's own on impacket versions that do not guard the INTERFACE.CONNECTIONS thread entry (KeyError: ThreadPoolExecutor-x_x). Over smb nothing changes: the module still creates its own DCOMConnection since the protocol has none.
…nd DCOMConnection Same pattern as the rdp module fix: BitLockerWMI creates its own DCOMConnection to the same target the wmi protocol already holds one for, and disconnecting it removes the shared INTERFACE.CONNECTIONS thread entry, so the protocol-level disconnect afterwards raises KeyError on impacket versions that do not guard it. Reuse the protocol's IWbemLevel1Login instead - over smb nothing changes.
…lter The query is sent as a wsman:Filter with the Microsoft WQL dialect and executed server-side (projection and WHERE included) - no PowerShell process on the target, no DCOM access. Results are pulled until EndOfSequence. Gated by requires_admin: the WinRM service WMI plugin only serves Administrators, Interactive and Remote Management Users (network logons lack the Interactive group), and the namespace itself needs Remote Enable on top (Administrators by default).
…ection Enumerating the shell resource succeeds for any WinRM-authorized user, so every valid login was reported as (Pwn3d!). admin_privs now comes from a WinRM configuration read - admin only, milliseconds on both paths. Command execution is probed separately by opening each shell endpoint (cmd and PowerShell runspace have separate SDDLs) without running a command, shown SSH-style: [+] dom\admin:pass (Pwn3d!) Shell access! (all) [+] dom\user:pass Shell access! (cmd only) The WQL enumeration is extracted into wql_enumerate along the way, used by the later hashdump and snapshot code paths, and gated by requires_admin like the query entry point.
…he rdp module wmi_invoke on the winrm protocol executes a WMI method natively over WS-Management (method INPUT body in the class resource URI namespace, instance addressed by a selector on the class key property) - no PowerShell process, the WinRM equivalent of the smb/wmi protocols calling WMI methods over DCOM. The rdp module gains winrm support: enable/disable via Win32_TerminalServiceSetting.SetAllowTSConnections, RAM toggling and the port query via StdRegProv over the same channel.
The winrm wmi_query now returns records in the same nested
{prop: {"value": ...}} format as the smb/wmi protocols, so modules
built on connection.wmi_query work unchanged over any of the three
protocols. enum_dns just declares winrm support - the queries run
natively over WSMan against the root/microsoftdns namespace.
Works unchanged on top of the aligned wmi_query record format; the IPAddress lookup is tolerant of the key being absent because WSMan serialization omits empty array properties entirely, where DCOM returns them with a null value.
BitLockerWinRM queries Win32_EncryptableVolume in the MicrosoftVolumeEncryption namespace natively over WSMan. A missing namespace is reported by WSMan as a:DestinationUnreachable, which is mapped to the same friendly message as the DCOM paths.
Lists volume shadow copies through the native WSMan WQL query against Win32_ShadowCopy with the same output as the smb/wmi protocols: nested single-child property values are unwrapped (InstallDate arrives as <InstallDate><Datetime>...</Datetime>), ISO datetimes are normalized to the DMTF format DCOM returns and boolean properties are capitalized, so a snapshot row is byte-identical between the winrm and wmi protocols. Gated by requires_admin like the other WMI read paths.
A winrm RemoteOperations class (matching the impacket naming) creates a VSS snapshot with Win32_ShadowCopy.Create via native WSMan Invoke and resolves the DeviceObject through WQL, all without spawning a process. The SAM and SYSTEM hives are locked by the OS on the live filesystem, so they are fetched from the shadow copy GLOBALROOT path with conn.fetch (PowerShell runspace, PSRP-fragmented so no MaxEnvelopeSizeKb change is needed); the bootkey is recovered locally with impacket's LocalOperations. sam() is rewritten on top of it: hashes are dumped with SAMHashes, printed and stored in the database like the smb protocol, and the shadow copy is deleted explicitly through finish() while the connection is still alive (the destructor alone fires after the protocol disconnected and can no longer reach the service).
lsa() follows the sam() flow: bootkey from the shadow copy SYSTEM hive, SECURITY hive fetched from the shadow copy GLOBALROOT path, LSASecrets parsing it offline. GMSA managed passwords are decrypted and the secrets plus cached hashes exported to ~/.nxc/logs/lsa like the smb protocol. The reg-save-through-a-shell implementation is gone, so is --dump-method which only selected its shell type.
The hive goes through WinRM at ~1MB/s (PSRP fragments the file into ~150KB envelopes), so the bootkey step sits silent for about 25 seconds. Display a grab-a-coffee hint first, like the dpapi module does.
The database path is read from the registry through StdRegProv (it is not always the default one), the ntds.dit is fetched from a snapshot of its volume and parsed offline with NTDSHashes on top of the sam/lsa bootkey. Mirrors the smb ntds() output, db filtering and exports; adds --ntds, --history, --kerberos-keys, --enabled, --user.
--kerberos-keys and --enabled now require --ntds, and --history also covers the SAM dump.
sam/lsa/ntds can read the hives and database from a snapshot listed by --list-snapshots instead of creating one, which is also not deleted after the dump.
PowerShell 2.0 / .NET 2.0 (2008 R2, Windows 7) refuse kernel paths like the shadow copy GLOBALROOT prefix, and serializing one big output object OOMs the runspace: copy the file to a normal path with native copy first and read it in base64 chunks when the plain fetch fails. The WMI plugin there also has no WS-Delete, fall back to vssadmin for the shadow copy deletion.
The hives are saved with reg save wrapped in a runspace (a backup operator with WinRM access does not necessarily have a WinRS cmd shell), fetched back, parsed locally and deleted in the same session. reg save is refused on HKLM\SECURITY for a plain backup operator, so that hive is optional and the LSA secrets are skipped when missing. No NTDS dump over winrm: there is no equivalent to the machine account DCSync escalation, the module just says so.
The smb side now walks the hives with regsecrets: backup-intent opens and registry queries through the SeBackupPrivilege, no RegSaveKey and no hive file dropped on the target. regsecrets opens the Lsa class keys without backup intent and starts RemoteRegistry through SCMR though, both denied to a backup operator: a small subclass opens the keys with backup intent instead, and the service is trigger-started by opening the winreg pipe.
FileTransfer owns the routing: it sizes the file once - a CIM_DataFile GET first, no process involved, then a PowerShell probe for the kernel paths the provider cannot see - and tries the routes below, best first. FileTransfer_WMI answers with one PS_ModuleFile GET, zero process on the target not even a runspace, whatever the size: files above the stock 500KB envelope get MaxEnvelopeSizeKb raised for the read and restored right after, a crash in between leaves the bigger value behind. A fault - no Powershellv3 namespace on systems predating PowerShell v3, a stalled GET, or the same fault a missing file raises - falls through. FileTransfer_Compatible serves every system with the PowerShell machinery: one ReadAllBytes invoke for small files, since every pypsrp invoke costs a fresh session and the parallel setup does not pay off; parallel chunks over four connections for big ones, keeping the server memory flat with the file size; and a native cmd copy bridge for the kernel paths the managed layer of PowerShell 2.0 rejects, like the shadow copy GLOBALROOT prefix. Chunk clients run with a raised read timeout, the few MB of base64 they carry do not always fit the default 30s on an inspected host.
The protocol get_file, the hashdump remote operations and the backup_operator winrm side go through it, and the fetch fallbacks now live in one place instead of remoteops.
sam() and lsa() built their hive paths from the shadow_copy_path property while ntds() used shadow_path(): keep the mapping helper only. The property becomes dead code and goes. The SYSTEM fetch comment and hint no longer claim ~1MB/s and 25 seconds: the file transfer reads it in a few seconds now.
A missing namespace or class, bad WQL or access denied dumped a raw WSManFaultError traceback through wmi_query: the fault reason now prints in one line and the query returns empty, so modules built on it degrade gracefully - enum_dns says the target is not a DNS server instead of crashing on one.
# Conflicts: # nxc/modules/bitlocker.py
# Conflicts: # tests/e2e_commands.txt
WMI method parameters that are arrays (e.g. MSFT_MpPreference ExclusionPath) are sent as repeated XML elements in the ExecMethod INPUT body, the WS-Management serialization for arrays.
DefenderWinRM queries MSFT_MpPreference and calls its methods (Set, Add, Remove) through the winrm protocol's wql_enumerate and wmi_invoke: no PowerShell runspace, no DCOM.
XiaoliChan
requested review from
Marshall-Hallenbeck and
zblurx
as code owners
September 6, 2026 14:02
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds winrm support to the defender module through the native WSMan primitives:
DefenderWinRMqueriesMSFT_MpPreferenceviawql_enumerateand calls its methods (Set,Add,Remove) viawmi_invoke- no PowerShell runspace, no DCOM.Also extends
wmi_invoketo support array parameters (e.g.ExclusionPath,ExclusionProcess,ExclusionExtension): each list item is sent as a repeated XML element in the ExecMethod INPUT body, the WS-Management serialization for arrays.Type of change
Setup guide for the review
Just install pypsrp
Screenshots (if appropriate)
Checklist
poetry run ruff check ., use--fixto automatically fix what it can)tests/e2e_commands.txtfile if necessary (new modules or features are required to be added to the e2e tests)