Skip to content

defender: winrm support via native WSMan - #1402

Open
XiaoliChan wants to merge 29 commits into
Pennyw0rth:mainfrom
XiaoliChan:winrm-defender
Open

defender: winrm support via native WSMan#1402
XiaoliChan wants to merge 29 commits into
Pennyw0rth:mainfrom
XiaoliChan:winrm-defender

Conversation

@XiaoliChan

@XiaoliChan XiaoliChan commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Depends on #1386 (defender module) and #1397 (--wmi-query) - merge those first.

Description

Adds winrm support to the defender module through the native WSMan primitives: DefenderWinRM queries MSFT_MpPreference via wql_enumerate and calls its methods (Set, Add, Remove) via wmi_invoke - no PowerShell runspace, no DCOM.

Also extends wmi_invoke to 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

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Deprecation of feature or functionality
  • This change requires a documentation update
  • This requires a third party update (such as Impacket, Dp loot, lsassy, etc)
  • This PR was created with the assistance of AI (Claude Code - implementation and live testing)

Setup guide for the review

Just install pypsrp

Screenshots (if appropriate)

  • Disable Defender
image
  • Enable Defender
image

Checklist

  • I have ran Ruff against my changes (poetry run ruff check ., use --fix to automatically fix what it can)
  • I have added or updated the tests/e2e_commands.txt file if necessary (new modules or features are required to be added to the e2e tests)
  • If reliant on changes of third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
  • I have linked relevant sources that describes the added technique (blog posts, documentation, etc)
  • I have performed a self-review of my own code (not an AI review)
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant