Skip to content

Releases: uziii2208/PwnRM

v2.1.0

Choose a tag to compare

@uziii2208 uziii2208 released this 30 Aug 17:59

What's Changed

This release expands PwnRM from a post-exploitation shell into an elite operator platform — 5 new specialized attack modules, in-memory credential extraction bypassing EDR process creation telemetry, multi-vector coerced authentication, Server 2025 LAPS hunting, DACL privilege escalation analysis, polymorphic PowerShell AST obfuscation, hardened SOCKS5 multiplexing, 3 targeted security fixes, and a completely custom self-contained GitHub Actions CI/CD Security Audit Pipeline with zero third-party SAST dependencies.


New Modules — 5 Specialized Attack Capabilities


!vss — In-Memory VSS Shadow Copy Extractor [modules/vss.py]

Direct WMI / CIM reflection via [wmiclass]"Win32_ShadowCopy" to locate and copy active SAM, SYSTEM, and NTDS.dit credential hives from Volume Shadow Copies without invoking vssadmin.exe, ntdsutil.exe, or any other subprocess that generates EDR process creation telemetry. After extraction, the shadow copy is immediately deleted via .Delete() to eliminate forensic artifacts. This is the difference between an operator that leaves a smoking gun in Windows Event Log 4688 and one that doesn't.


!coerce — Multi-Vector Coerced Authentication Engine [modules/coerce.py]

Triggers NTLM authentication coercion from the remote target back to an operator-controlled listener (Responder / ntlmrelayx) via four distinct protocol vectors — all executed in-memory through the active PSRP session without staging any binary. Vectors: WebDAV HTTP UNC path (\\listener@80\share\dummy.txt) bypassing SMB signing requirements for ADCS ESC8 Web Enrollment relay attacks; MS-RPRN Print Spooler (\pipe\spoolss); MS-EFSR PetitPotam (\pipe\efsrpc); MS-DFSNM (\pipe\netdfs). Operator selects the appropriate vector based on available services and SMB signing posture on the target.


!laps — Windows LAPS & Server 2025 Hunter [modules/laps.py]

In-memory LDAP query covering both Legacy LAPS and Modern LAPS schemas in a single pass. Legacy LAPS: reads ms-Mcs-AdmPwd (cleartext local admin password) and ms-Mcs-AdmPwdExpirationTime (expiration timestamp) from DirectorySearcher. Modern LAPS (Windows Server 2025 / Windows 11 Azure-joined): queries msLAPS-Password, msLAPS-EncryptedPassword, and msLAPS-PasswordHistory — covering both cleartext and AES-256-CBC encrypted password blobs alongside full history. Results surface directly in the operator console and are ingested into the !loot pipeline.


!acl — Active Directory DACL Privilege Escalation Scout [modules/acl.py]

Audits discretionary access control lists on Tier-0 Active Directory objects: AdminSDHolder, Domain Admins, Domain Controllers, krbtgt, and linked GPOs. For each object, enumerates principals holding high-risk access rights — GenericAll, WriteDacl, WriteOwner, GenericWrite, User-Force-Change-Password — and flags them as privilege escalation vectors. All enumeration runs in-memory via DirectoryServices LDAP queries on the active PSRP session; no SharpACLs, BloodHound collection, or additional binary is staged.


!token — Process Token Hunter & In-Memory Impersonation [modules/token.py]

Enumerates process tokens across active logon sessions and performs privilege escalation triage by inspecting SeImpersonatePrivilege, SeAssignPrimaryTokenPrivilege, SeDebugPrivilege, and SeBackupPrivilege — flagging each with its current enabled/disabled state and the corresponding abuse primitive. Token duplication via DuplicateTokenEx reflection and named pipe client token coercion are surfaced as impersonation pathways, without requiring any binary drop to disk.


OPSEC & Core Improvements

Polymorphic PowerShell AST Command Obfuscation [core/opsec.py]

Added obfuscate_cmd() — a dynamic backtick and whitespace splitter for PowerShell cmdlet names that activates under stealth and hybrid-cloud OPSEC profiles. The function inserts randomized backtick sequences between cmdlet name characters (Invoke-Expression) while preserving variable names, parameters, and argument structure, defeating static ScriptBlockLogging signatures without breaking execution. Jitter sleep timing calculations across all profiles are now seeded from secrets.randbelow() rather than time.sleep(random.uniform(...)), making inter-request timing non-deterministic to statistical network anomaly detectors. OPSEC profile table updated with the new obfuscation column.

Thread-Safe SOCKS5 Proxy & Port Forwarding [core/tunnel.py]

Hardened Socks5Server and PortForwarder with threading.Lock() mutex guards on all connection tracking and socket descriptor operations, preventing race conditions during concurrent multi-session switches. Socket timeout guards are now applied on both ends of the relay preventing orphaned socket descriptors from accumulating in process memory on abrupt client disconnects.

WebSocketTransport — Public API Export [core/__init__.py, __init__.py]

WebSocketTransport is now exported from pwnrm.core and pwnrm package top-level __all__, making it accessible as a library transport for consumers integrating PwnRM programmatically.

bump_version.py — Hardened Repo Root Detection [src/release/bump_version.py]

Refactored find_repo_root() to check both __file__ parent and Path.cwd() as starting points, and tightens the root detection predicate to require both pyproject.toml and src/pwnrm/ to exist before declaring a match — eliminating false positives when the script is run from nested directories or via CI. cleanup_build_artifacts() now uses ignore_errors=True consistently and guards __pycache__ removal behind is_dir(). TARGETS expanded to include README.md Changelog badge for atomic version synchronization.


Security Hardening (Auditor Pass)

CWE-918 — SSRF / Proxy MitM via Ambient Environment [core/transports.py]

Enforced session.trust_env = False across all Transport base class instantiations. Previously, Python requests would honour HTTP_PROXY / HTTPS_PROXY environment variables, allowing a compromised operator environment to silently reroute WinRM traffic through an attacker-controlled proxy. With trust_env = False, ambient proxy configuration is fully ignored regardless of execution context.

CWE-208 / CWE-385 — Timing Side-Channel in CredSSP pubKeyAuth Verification [core/transports.py]

Replaced standard equality comparison (==) on the CredSSP Server-To-Client Binding hash verification with hmac.compare_digest() — a constant-time comparison that eliminates the byte-by-byte early-exit timing oracle that could leak information about the expected hash to an active MitM attacker.

CWE-532 — Credential Identifier Leakage into Logs [core/api.py]

Sanitized logging.info() call sites in create_transport() to remove raw credential variable references from structured log output. Transport authentication method is now logged descriptively without serializing any object that may hold credential material.

CWE-200 / CWE-522 — Expanded History Exclusion Pattern [shell/pwnshell.py]

HISTORY_EXCLUDE_PATTERN regex expanded to automatically suppress offensive tooling and credential keywords from .pwnrm_history: mimikatz, secretsdump, lsass, ccache, kirbi, dpapi — added alongside the existing -p, -H, --hash, --pfx-pass, password, secret, token patterns.


Custom CI/CD Security Audit Pipeline [.github/]

A fully self-contained GitHub Actions security audit pipeline with zero dependency on third-party SAST tools. All analysis is implemented directly in Python using ast, re, and subprocess.

Workflow (.github/workflows/security_audit.yml) — 5-job pipeline: setup → unit_and_regression (fail-fast) → custom_security_audit + dependency_audit (parallel) → report_and_annotate (always runs). Triggers: push to master/main/dev/release/*, pull request, weekly cron (Monday 03:00 UTC), and manual dispatch with audit_level input.

PowerShell Injection Auditor (.github/scripts/audit_ps_injection.py) — AST data-flow analyzer walking all ast.JoinedStr f-string nodes and string concatenations that feed into run_sync(), run_with_interrupt(), and run_command(). Traces variable origin up to 3 assignment hops and flags server-returned or user-supplied values that reach PS string interpolation without _pde(), _pse(), or str_b64() wrapping. Known-safe patterns (build_amsi_patch() output, secrets.token_hex() identifiers) are allowlisted.

Cryptographic Hygiene Auditor (.github/scripts/audit_crypto.py) — CryptoAuditVisitor AST walker validating CSPRNG usage (secrets vs random), secrets.randbelow() argument bounds (must be > 1), secrets.token_bytes() entropy floor (≥ 16 bytes), Fernet _SESSION_KEY lifecycle (no disk write, no log), and MD5 usage discrimination (file integrity = INFO, auth context = HIGH).

Filesystem & Permissions Auditor (.github/scripts/audit_filesystem.py) — Enforces atomic O_CREAT | O_EXCL file creation patterns for all writes under _PWNRM_DIR, verifies 0o600 chmod follows within 5 lines of os.open(), detects tempfile.mktemp() TOCTOU usage, checks __pycache__ git-tracking via git ls-files, and verifies symlink attack guard on history file creation.

XML Deserialization Safety Auditor (.github/scripts/audit_xml_safety.py) — Confirms defusedxml monkey-patches ET.fromstring and ET.parse on import, verifies defusedxml>=0.7.1 pinning in requirements.txt, and checks all ET.fromstring() call sites are wrapped in exception handlers catching defusedxml-specific exceptions.

Secret & Credential Exposure Auditor (.github/scripts/audit_secrets_exposure.py) — Regex scan across .py and .ps1 ...

Read more

v2.0.1

Choose a tag to compare

@uziii2208 uziii2208 released this 30 Aug 01:32

What's Changed


Security Hardening — Full Niche Operator Quality Pass (FIX-01 – FIX-09)

  • FIX-01 (CWE-400 / Silent Data Corruption): Fixed closure rebinding bug in download() stream collector (pwnshell.py:788–822). bytearray += bytes inside the inner closure was rebinding the local parameter name rather than mutating the shared buffer — outer buf stayed empty across all chunks. Replaced with in-place buf.extend(chunk) and added diagnostic separation between empty-stream and collection-failure error paths.

  • FIX-02 (CWE-78 / CWE-88): Resolved PowerShell single-quote injection in upload() user-supplied dst path (pwnshell.py:631–674). Replaced all single-quoted PS strings with double-quoted equivalents escaped via _pde() and enforced -LiteralPath across every file manipulation cmdlet (Add-Content, Move-Item, Remove-Item).

  • FIX-03 (CWE-73 / CWE-22): Fixed client-side trust of server-returned Resolve-Path output for local filename derivation (pwnshell.py:709–745). Default local destination is now derived strictly from args[0] (user-supplied RPATH via PureWindowsPath(args[0]).name), not from the server-resolved path. Added CWD-escape check: resolved dst is validated against Path.cwd() and rejected on traversal.

  • FIX-04 (CWE-693): Replaced static 6-byte AMSI patch signature b8 57 00 07 80 c3 (ret 0x80070057) with dynamic build_amsi_patch() polymorphic NOP generator (commands.py:38–54, evasion.py:54–75). Stub prefix is selected at runtime from xchg ax,ax / nop / multi-byte NOP variants via secrets.choice(). Eliminated hardcoded Start-Sleep -Seconds 1 OPSEC anti-pattern, replaced with VirtualProtect RWX → Marshal::Copy → RX transition without deterministic sleep.

  • FIX-05 (CWE-330): Replaced non-CSPRNG Mersenne Twister random.randint / randbytes with secrets module across all security-sensitive call sites (commands.py:56–95, spnego.py:140–155, transports.py:295–310). XOR keys now use secrets.randbelow(254) + 1 (range [1,254], never zero-key), CredSSP nonces use secrets.token_bytes(32), and PS namespace/classname identifiers use secrets.token_hex().

  • FIX-06 (CWE-611): Integrated defusedxml.ElementTree as a safe XML parsing layer in core/psrp.py:5–16. Implementation monkey-patches ET.fromstring and ET.parse with defusedxml equivalents on import, with graceful fallback to stdlib ET if defusedxml is unavailable. defusedxml>=0.7.1 added to requirements.txt.

  • FIX-07 (CWE-22 / CWE-78): Reimplemented missing _validate_remote_path() regression (pwnshell.py:727–748). Strict regex whitelist _SAFE_WINPATH (^[A-Za-z]:\\...) and _SAFE_UNCPATH (^\\\\...) blocking directory traversal sequences (..), shell metacharacters ($, `, ;, &, `), and control characters.

  • FIX-08 (CWE-400): Enforced MAX_SYNC_BYTES = 1 * 1024 * 1024 output ceiling in run_sync() (pwnshell.py:298–315). Overflow triggers output truncation with a warning and active runspace interrupt via signal — prevents OOM from anomalous or adversarially large remote command output.

  • FIX-09 (CWE-295 / CWE-300): Implemented cryptographic pubKeyAuth echo-back verification in CredSSP handshake (transports.py:318–350). Server-returned pubKeyAuth blob is unwrapped and compared against SHA256(b"CredSSP Server-To-Client Binding Hash\x00" + nonce + pubkey) — MitM relay without the private key is now detectable. Added CredSSP v5 protocol fallback for legacy domain configurations.


Operational Improvements (OPT-01 – OPT-08)

  • OPT-01: Eliminated CtrlCHandler re-entry race condition in run_with_interrupt() (pwnshell.py:317–335). Handler now covers the entire generator stream duration rather than re-entering per-chunk, closing the window where a Ctrl+C between iterations would fall through to the default SIGINT handler.

  • OPT-02: Upload progress display overhauled (pwnshell.py:648–665). Progress is now calculated in-flight before chunk submission and rendered as a real-time percentage (Upload X/Y (Z%)), replacing the post-chunk counter that lagged one iteration behind actual transfer state.

  • OPT-03: help() refactored from a hardcoded static string into a structured COMMAND_REGISTRY dict (pwnshell.py:65–135). All commands with syntax and descriptions are sourced from a single registry — new commands added to the plugin system appear in help output automatically without manual string editing.

  • OPT-04: Transcript log writes now pass through strip_ansi() (pwnshell.py:270–295). Raw VT100/ANSI escape codes no longer pollute .log files — output is clean ASCII/UTF-8 readable in any editor or SIEM pipeline.

  • OPT-05: Replaced colloquial comment explaining SPNEGO/Kerberos AP_REQ extraction as "hijack" with proper technical rationale citing RFC 4121 NegTokenInit unwrapping and MS-WSMV §3.2.5.1 GSS-API framing requirements (transports.py:257–270).

  • OPT-06: Hardcoded magic constant 0xe0088237 in NTLM Type 1 flags defined as named constant NTLM_FLAGS_STANDARD with comprehensive bitwise breakdown documentation (spnego.py:38–53).

  • OPT-07: Sensitive download buffer zeroized on MD5 integrity failure (pwnshell.py:810–822). buf[:] = b'\x00' * len(buf) executes immediately on checksum mismatch before error is surfaced — prevents credential material from lingering in process memory after a corrupt transfer.

  • OPT-08: Introduced HISTORY_EXCLUDE_PATTERN regex filter on prompt_toolkit FileHistory (pwnshell.py:55–65). Commands matching password|secret|hash|token|pfx|ccache (case-insensitive) are not written to .pwnrm_history, preventing credential strings from persisting to disk across sessions.


Niche Source Audit Fixes (NICHE-01 – NICHE-02)

  • NICHE-01: Replaced bare except: clause in utfstr() (utils.py:34–40) with except Exception: — preserves KeyboardInterrupt and SystemExit propagation during XML decode errors, fixing silently swallowed Ctrl+C in long enumeration runs.

  • NICHE-02: Sanitized single-quote injection in SMB target array construction in get_shares_ps() (shares.py:18–28). Target strings are now stripped and double-quote escaped via t.strip().replace('"', '"')before splicing into the@('TARGETS')` PowerShell array literal.


Architectural Additions (Tier 1–3)

  • Ephemeral Session Encryption: In-memory Fernet key (_SESSION_KEY = Fernet(Fernet.generate_key())) generated at process start and used to encrypt all serialized session metadata written to ~/.pwnrm/sessions/. Key exists only in process memory — session files are unreadable after PwnRM exits, eliminating post-engagement forensic credential recovery from session artifacts.

  • Dual ETW Telemetry Evasion: !evasion now patches both EtwEventWrite and EtwEventWriteFull in ntdll.dll. Windows 10 21H2+, Windows 11, and Server 2022/2025 route a significant subset of kernel ETW events through EtwEventWriteFull — patching only EtwEventWrite left a detectable telemetry path open on all post-21H2 targets.

  • Structured Loot MANIFEST.json (core/loot.py): LootManager now maintains an operational MANIFEST.json under ~/.pwnrm/loot/ with per-artifact entries including filename, type, category, source_command, ISO 8601 timestamp, sha256, target, and path. Both store_credential() and store_artifact() update the manifest atomically via O_CREAT | O_WRONLY | O_TRUNC with 0o600 permissions.

  • Multi-Session Output Labeling: fan_out_exec output streams in !session exec-all are now prefixed with [S:{sid} | {host}] tags per line, disambiguating interleaved output across concurrent sessions.

  • Plugin Integrity Verification: ModuleManager now calls verify_plugin_integrity() before dynamic import — validates that each *.py module file is owned by the current user (st_uid == os.getuid()) and is not world-writable (st_mode & stat.S_IWOTH == 0). World-writable plugin files are rejected with a warning.

  • BloodHound CE v6 Meta Headers: !bloodhound in-memory LDAP enumeration output now includes correct BloodHound CE v6 metadata headers {"meta": {"type": "...", "count": N, "version": 6, "methods": 0}} — previously missing headers caused silent rejection or misparse on BloodHound CE ingestion.

  • Playbook on_fail Cleanup Hooks: PlaybookRunner YAML DSL now supports cleanup_on_fail keys per step. On mid-playbook failure, registered cleanup commands are executed in reverse order against the active session to remove staged artifacts before the operator loses access.

  • RBCD Inspection in !kerberos: Added msDS-AllowedToActOnBehalfOfOtherIdentity attribute enumeration. Surfaces write-capable principals and flags RBCD abuse candidates alongside existing S4U2Self / constrained delegation checks.

  • Enterprise CA Health Check in !adcs: Added CA certificate expiration detection and CRL/AIA distribution point reachability probing. Offline CDP and AIA misconfiguration are flagged as strong indicators of unmaintained PKI — correlated with elevated ESC4/ESC7 probability in the output.

  • Type Annotations (PEP 484): Comprehensive type annotations added across core/api.py, core/credentials.py, core/psrp.py, core/runspace.py, core/session_mgr.py, core/transports.py, and core/utils.py. All public method signatures now carry Optional, Union, Tuple, List, Callable, and Generator hints compatible with Python 3.10+ strict mode.

  • Test Suite Expansion: 22 unit and security regression tests passing at 100% success rate (pytest / unittest), up from 21 in v2.0.0.


Full Changelog: v2.0.0...v2.0.1

v2.0.0

Choose a tag to compare

@uziii2208 uziii2208 released this 29 Aug 09:32

What's Changed

Important

v2.0.0 is a full architectural overhaul. The PwnShell REPL, auth layer, module system, and output pipeline are not backward-compatible with v1.x configuration files or session artifacts.


Core Architecture — Multi-Session Manager & Jump Graph (session_mgr.py)

  • !session — Concurrent PSRP Runspace orchestration across heterogeneous targets: Prior to v2.0.0, PwnRM maintained a single active Runspace per process, requiring the operator to re-authenticate on context switches between pivots. The new SessionManager holds an indexed pool of pypsrp.RunspacePool objects keyed by session ID, each tagged with target metadata (hostname, auth type, auth material hash). !session list renders the live pool with connection state; !session switch <id> performs a zero-reauth context switch by hot-swapping the active RunspacePool reference in PwnShell; !session exec-all <cmd> fans out a single command across all open sessions and aggregates structured results. Session metadata is serialized and persisted under ~/.pwnrm/sessions/ with strict 0o700 (POSIX) / icacls "%USERNAME%:(OI)(CI)F" /inheritance:r (Windows NTFS) permission enforcement, preventing multi-tenant credential leakage — consistent with the ACL hardening introduced in v1.2.1.

Tunneling — In-Band PSRP SOCKS5 Proxy & Port Forward Multiplexer (tunnel.py)

  • !socks — RFC 1928 compliant SOCKS5 server multiplexed through PowerShell streams: Conventional SOCKS pivoting requires a listener on the remote host (chisel, ligolo, etc.) and a firewall exception. PwnRM's SOCKS5 implementation runs a local asyncio server bound to 127.0.0.1:1080 (configurable) and tunnels TCP frames through the existing PSRP stdout/stdin stream pair using a length-prefixed framing protocol. No listener socket is opened on the remote host; traffic appears as normal WinRM WSManFault-shaped XML fragments to network inspection. Implementation is RFC 1928 §3–§6 compliant: CONNECT method, IPv4/IPv6/FQDN address types, and AUTH_NONE / USERNAME_PASSWORD sub-negotiation (§4).

  • !portfwd — Local-to-remote TCP port forwarding with lifecycle management: !portfwd <LPORT> <RHOST>:<RPORT> spawns an asyncio TCP listener on LPORT that proxies connections through the active PSRP session to RHOST:RPORT on the target network segment. !portfwd list enumerates active forward rules with their bound port and target; !portfwd stop <id> cleanly drains the asyncio transport and closes the listener without disrupting the parent PSRP session.


Loot — Structured & Encrypted Loot Pipeline (loot.py)

  • !loot — Atomic loot ingestion with per-target namespacing: Credentials, Kerberos tickets (.ccache, .kirbi), certificates (.pfx, .pem), DPAPI master keys, and memory dumps are stored under ~/.pwnrm/loot/<target>/ in per-artifact-type subdirectories. File creation uses O_CREAT | O_EXCL atomic semantics to prevent TOCTOU overwrites, and all written files are immediately chmod'd to 0o600. The ingestion pipeline accepts raw bytes from any command module and performs MIME sniffing to auto-classify artifact type before persistence, removing the need for manual file-type specification per loot operation.

OPSEC — Profile & Jitter Engine (opsec.py)

  • !opsec — Behavioural profile switching with per-request timing jitter: Four operational profiles are implemented: stealth (high jitter, low throughput, legitimate Microsoft WinRM Client User-Agent header, command fragment obfuscation), balanced (moderate jitter, no obfuscation), aggressive (minimal jitter, raw commands), and hybrid-cloud (tuned for Azure Arc / WinRM-over-HTTPS environments with cloud-specific User-Agent strings). Jitter is applied at the PSRP transport layer between requests using a per-profile (min_ms, max_ms) range sampled from a uniform distribution, making request cadence non-deterministic to timing-based EDR heuristics.

Modules — Pluggable Modular Engine (src/pwnrm/modules/)

  • BaseModule / ModuleManager — Hot-loadable plugin architecture: v2.0.0 introduces a BaseModule abstract class exposing name, aliases, help_text, run(shell, args), and optional tab_complete(shell, prefix) contract. ModuleManager performs filesystem discovery of *.py files under src/pwnrm/modules/, imports them, and registers all BaseModule subclasses into the PwnShell command dispatcher at startup. Third-party and user-authored modules drop into the modules directory with no changes to core — the loader handles registration, tab completion injection, and !help listing automatically.

Exploitation — Full ADCS Engine ESC1–ESC16+ (adcs.py)

  • !adcs — Deep ADCS template vulnerability audit without dropping binaries: Implements in-memory LDAP enumeration of Certificate Authority configuration and certificate template ACLs to audit for ESC1 (enrollee-supplied Subject), ESC2 (Any Purpose EKU), ESC3 (Certificate Request Agent), ESC4 (template write ACL), ESC6 (EDITF_ATTRIBUTESUBJECTALTNAME2), ESC7 (CA officer ACL), ESC9/ESC10 (no-security-extension / weak certificate mapping), ESC13 (OID group link), ESC15 (schema version 1 + Application Policy EKU), ESC16 (disabled szOID_NTDS_CA_SECURITY_EXT globally), and ESC17 (WSUS-to-ADCS abuse — enumerates WUServer registry value under HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate to identify HTTP-only WSUS endpoints susceptible to MitM, where an attacker-controlled update package can trigger a certificate enrollment request to an internal CA under the machine account context, enabling domain privilege escalation). Enterprise CA discovery queries CN=Certification Authorities,CN=Public Key Services,CN=Services in the configuration naming context. All enumeration runs through PowerShell LDAP queries on the active PSRP session; no SharpADCS, Certify, or Certipy binary is staged to disk.

Kerberos — Advanced Suite (kerberos.py)

  • !kerberos — AES-priority Kerberoasting, AS-REP roasting, Diamond Tickets, and Server 2025 dMSA/BadSuccessor inspection: Kerberoasting requests are issued with etype=18 (AES256-CTS-HMAC-SHA1-96) preference before falling back to RC4, consistent with environments where RC4 is deprecated under Msds-SupportedEncryptionTypes. AS-REP roasting enumerates accounts with DONT_REQ_PREAUTH set via LDAP and pre-computes roastable target lists. Diamond Ticket workflow assistance provides step-by-step identity-swap guidance for environments where synthetic Golden/Silver Tickets trigger KDC anomaly detection on Defender for Identity. The BadSuccessor inspection block enumerates msDS-ManagedAccount / delegated Managed Service Account (dMSA) objects and surfaces misconfigured successor principal assignments introduced in Server 2025.

Cloud — Hybrid Entra ID / Azure AD PRT Pivot (entra.py)

  • !entra — Device join status inspection and WAM / Azure CLI / Az PowerShell credential cache extraction: Queries dsregcmd /status output via PSRP and parses AzureAdJoined, DomainJoined, and EnterpriseJoined fields to determine hybrid join topology. When AzureAdJoined: YES, the module enumerates Web Account Manager (WAM) token broker cache paths in the user profile for ms-deviceid and PRT-adjacent token blobs. Azure CLI token cache (~/.azure/msal_token_cache.json) and Azure PowerShell credential cache (~\AppData\Roaming\Windows Azure PowerShell\TokenCache.dat) paths are identified and flagged for extraction via !loot.

Credential Hunting — Deep Credential & Token Hunter (creds.py)

  • !creds — DPAPI master key enumeration, browser credential store detection, and token privilege audit: Enumerates %APPDATA%\Microsoft\Protect\<SID>\ for DPAPI master key files. Browser credential store detection covers Chrome, Edge, and Brave Login Data (SQLite) and Local State (AES-GCM key blob) paths under %LOCALAPPDATA%. Token privilege audit inspects the current runspace token for SeDebugPrivilege, SeImpersonatePrivilege, and SeTcbPrivilege, flagging each with its enable/disable state and a note on the relevant abuse primitive.

Evasion — Polymorphic Evasion & EDR Scout (evasion.py)

  • !evasion — Randomized polymorphic AMSI/ETW patching and EDR driver fingerprinting: AMSI bypass patches AmsiScanBuffer in amsi.dll using a randomized stub (variable nop-sled length, randomized jmp displacement encoding) to avoid static signature matching on the patch bytes. ETW bypass patches EtwEventWrite in ntdll.dll with a ret / xor eax, eax; ret stub selected at random. EDR driver detection queries \\.\ device namespace and the SCM service list for csagent.sys (CrowdStrike), SentinelAgent.sys (SentinelOne), WdFilter.sys (Windows Defender), CbDefenseW64.sys (Carbon Black), and cylance.sys (Cylance Protect), reporting detected products with their loaded driver path.

Reconnaissance — In-Memory BloodHound AD Graph Collector ([bloodhound.py](https://github.com/uziii2208/PwnRM/blob/master/src/pwnrm/modules/blood...

Read more

v1.2.6

Choose a tag to compare

@uziii2208 uziii2208 released this 24 Aug 12:37

What's Changed

Bug Fixes - SPNEGO / Authentication Layer spnego.py, mock_server.py

  • !spnego - None ResponseToken in Type 2 challenge causes auth failure (impacket compatibility break): When PwnRM negotiated NTLM through SPNEGO against a server whose SPNEGO stack was assembled via impacket's pyasn1 binding, NegTokenResp["ResponseToken"] returned None rather than the raw NTLM Type 2 bytes. This occurred because impacket's SPNEGO_NegTokenResp only populates the field when the internal pyasn1 component path is written through the correct accessor — which differs between impacket versions — leaving the field unset despite the bytes being present in the DER envelope. Downstream, NTLMAuthChallenge(type2) received None, producing an immediate TypeError and aborting the handshake. Fixed in spnego.py:64-71 by scanning the raw data_in buffer for the NTLMSSP\x00 signature when ResponseToken is None, extracting the challenge directly from the blob, and raising SPNEGOError("SPNEGO: missing ResponseToken in Type 2 challenge") only if the signature is absent entirely.

  • !mock_server - Fixed offset NTLM token detection breaks SPNEGO-wrapped NegTokenInit from impacket: The mock server's auth dispatch used a hardcoded token[8:12] byte slice to identify NTLM message type, assuming tokens always arrive as raw NTLMSSP. When Linux impacket sends a SPNEGO-wrapped NegTokenInit, the NTLMSSP signature appears at a variable offset inside the ASN.1 envelope — token[8:12] reads the wrong bytes, the type check falls through, and the server rejects valid negotiation tokens. Simultaneously, the original NTLM_CHALLENGE was a hand-rolled byte stub that did not conform to MS-NLMP §2.2.1.2 — missing proper TargetInfo AV_PAIRS, a valid server challenge value, and a well-formed Version field — causing impacket's strict ASN.1 parser to reject the challenge during the mock test cycle. Fixed in mock_server.py:218-232 by replacing the fixed-offset slice with token.find(b"NTLMSSP\x00") for signature detection, computing msg_type relative to sig_pos, and inserting a bare-SPNEGO fast path that issues a challenge and returns immediately when no NTLMSSP signature is found. NTLM_CHALLENGE was rebuilt via _build_ntlm_type2() (conforming to MS-NLMP §2.2.1.2 with MsvAvNbDomainName + MsvAvEOL AV_PAIRS and a non-zero ServerChallenge) and wrapped in a fully hand-rolled DER SPNEGO NegTokenResp via _build_spnego_challenge() (RFC 4178 §4.2.2) to avoid impacket version-dependent field-assignment behavior. Affected mock_server.py:49-110, 218-248.

Housekeeping

Full Changelog: v1.2.5...v1.2.6

v1.2.4

Choose a tag to compare

@uziii2208 uziii2208 released this 23 Aug 19:59

What's Changed

Critical Fix - PSRP Wire Format runspace.py

  • run_command - 100% pipeline failure on Windows Server 2025 WinRM (w:InternalError / w:InvalidSelectors): The custom MS-PSRP fragmentation layer in _fragment() encoded Runspace/Pipeline GUIDs as 32-byte ASCII hex strings instead of the 16-byte binary format required by MS-PSRP spec §2.2.1. Additionally, rsp:ShellId in the Create request body was hardcoded to "http://localhost/wsman" (the ResourceURI value) instead of a valid UUID, causing Windows to invalidate the shell immediately after creation. Combined, these two defects caused _create_pipeline() to receive w:InternalError on first command dispatch and w:InvalidSelectors on all subsequent commands - 100% failure rate on Windows Server 2025. Windows Server builds may have been more lenient and partially functional. Fixed by replacing the custom PSRP implementation with pypsrp as the PSRP backend while retaining PwnRM's own SPNEGO/Kerberos/CredSSP/ClientCert transport layer for authentication. Affected runspace.py entirely.

  • __enter__ - bare _receive() calls crash shell init on Windows Server 2025: Two _receive() calls in __enter__ were issued without a CommandId, which Windows Server WinRM rejects with w:InternalError at the shell init stage. After the first receive failed, the shell was invalidated by the server - every subsequent request returned w:InvalidSelectors. Resolved as part of the PSRP backend replacement.

Dependency

  • Added pypsrp>=0.8.0 to requirements.txt and pyproject.toml.

Full Changelog: v1.2.1...v1.2.4

v1.2.1

Choose a tag to compare

@uziii2208 uziii2208 released this 23 Aug 14:40

What's Changed

Bug Fixes - Post-Exploitation Modules (adtriage.py, sessions.py, shares.py, pwnshell.py)

  • !adtriage / !sessions / !shares - [bool]::Parse FormatException (100% failure rate): Template substitution in all three resource loaders was replacing '__QUICK__' (with surrounding single quotes) instead of the inner token __QUICK__, stripping the quotes the .ps1 template already provides. The emitted PowerShell was [bool]::Parse($false) / [bool]::Parse(True) depending on attempt - both are invalid inputs to the CLR static method, which strictly requires "True" or "False" as quoted string literals. Fixed by targeting __QUICK__ (no quotes) in all three loaders so the template's existing single quotes are preserved, producing well-formed [bool]::Parse('True') / [bool]::Parse('False') output. Affected adtriage.py:16, sessions.py:15, shares.py:18.

  • !shares - Silent no-op on default invocation (@('') array clobber): When !shares is called without explicit targets, shares.py substituted an empty string into @('__TARGETS__'), producing @('') - a one-element PowerShell array containing an empty string. $Targets.Count evaluated to 1, satisfying the Count -gt 0 branch and bypassing both AD domain controller discovery and local WMI share enumeration entirely - zero output, zero error. Fixed by replacing the entire @('__TARGETS__') expression with @() on the no-target path, correctly producing an empty array and allowing the intended discovery branches to execute. Affected shares.py:18–25, shares.ps1:210.

  • !amsi - Silent bypass failure via [ref]$null in VirtualProtect out-parameter: amsi_bypass() passed [ref]$null as the lpflOldProtect out-parameter to VirtualProtect. $null is a read-only automatic variable in PowerShell - the CLR marshaler cannot write the returned uint into it, throwing Cannot set the Value property for a Reference object whose Value is not a variable. As a result, VirtualProtect failed to mark AmsiScanBuffer as writable, the subsequent Marshal::Copy hit a write-protected page, and the bypass silently failed while the shell still printed [+] AMSI bypass applied. Fixed by declaring a typed mutable variable $old = [uint32]0 before each call and passing [ref]$old. Affected pwnshell.py:431–435.

  • !netrun - TargetParameterCountException on zero-argument invocation: When !netrun was called with a URL and no trailing arguments, the no-args branch selected EntryPoint.Invoke($null, $null). For any assembly with a standard Main(string[] args) entry point (one parameter), the CLR reflection API requires exactly one argument - passing $null throws System.Reflection.TargetParameterCountException: Parameter count mismatch, aborting execution before the assembly runs. Fixed by dynamically inspecting EntryPoint.GetParameters().Length: zero-parameter entry points use Invoke($null, $null); one-parameter entry points use Invoke($null, [object[]](,[string[]]@())), supplying a valid empty string array. Affected pwnshell.py:494.

Full Changelog: v1.2.0...v1.2.1

v1.2.0

Choose a tag to compare

@uziii2208 uziii2208 released this 22 Aug 02:39

What's Changed


New Features

  • !shares - SMB Share Scout: Self-contained share enumeration running entirely inside the remote PowerShell session. Discovers local shares via Win32_Share, probes ~20 common UNC paths for read/write access, scans ACLs for overly permissive entries (Everyone, Authenticated Users), detects sensitive files in SYSVOL/NETLOGON including GPP cPassword auto-detection (CVE-2014-1812), and reports open files and active SMB sessions. Supports -q for quick local-only mode and explicit [HOST..] targeting with AD-based auto-discovery.

  • !sessions - Session & Network Snapshot: Active logon session enumeration via Win32_LogonSession/Win32_LoggedOnUser with session type classification (Interactive/RDP/Network/Service). Covers RDP client MRU and saved credentials from registry, Kerberos ticket cache (klist) with TGT flagging, established TCP connections with process attribution and external IP detection, listening port inventory with service labels, named pipe exposure with sensitive pipe flagging (lsass, spoolss, samr, epmapper...), and SYSTEM-level scheduled tasks. Supports -q for logon sessions + RDP MRU only.


Hardening (3584ce5)

  • run_sync - Memory Cap: Enforced a 1 MB output ceiling on synchronous command accumulation. Streams exceeding the limit are truncated and the runspace is interrupted to prevent OOM from anomalous remote endpoints.
  • !download - Stream Guard: !download chunk accumulator now enforces a configurable memory cap (PWNRM_MAX_DL, default 256 MB). Overflow raises a RuntimeError and surfaces immediately rather than crashing silently.
  • SPNEGO/Kerberos - Exception Hardening: Replaced bare except: clauses in NTLM and Kerberos unwrap routines with except Exception: to prevent silent swallowing of fatal decryption failures and RC4 keystream desynchronization.
  • Session Directory - Windows ACL: _PWNRM_DIR now explicitly strips inherited ACEs and grants exclusive (OI)(CI)F rights to the current user via icacls on Windows, mirroring the existing 0o700 POSIX enforcement.

Refactor (64da5ba)

  • Transport - Request Timeouts: Enforced an explicit timeout=30 across all transport adapters (BasicTransport, ClientCertTransport, SPNEGOTransport, KerberosTransport, CredSSPTransport) to prevent uninterruptible client hangs on unresponsive targets.
  • Utils - Certificate String Normalization: get_server_certificate() now strips PEM headers and all whitespace in a single re.sub() pass, correctly handling cross-platform line endings (\r\n) that the previous str.replace() approach missed.
  • Utils - load_pfx() Windows ACL: PFX temp directory now applies the same icacls exclusive-access hardening as _PWNRM_DIR, consistent across platforms.

Full Changelog: v1.1.1...v1.2.0

v1.1.1

Choose a tag to compare

@uziii2208 uziii2208 released this 18 Aug 12:33

What's Changed

Security Fix — (GHSA-jwc5-6vfh-r4h2, 9f5ad17)

  • Transport — Redirect Blocking: Disabled HTTP redirect following (allow_redirects=False, max_redirects=0) across all transport sessions. Any 3xx response from a WinRM server now raises TransportError and aborts the connection instead of forwarding the request to the redirect target.
  • Transport — Unencrypted Response Passthrough: _decrypted_response() now short-circuits and returns the raw response when no --Encrypted Boundary marker is present, preventing a panic on plaintext SOAP faults.
  • Transport — CredSSP TLS Certificate Guard: Added an explicit check for peer certificate existence after TLS handshake in CredSSPTransport._auth() before attempting to extract the public key.
  • Utils — KRB5CCNAME Path Prefix: Replaced lstrip("FILE:") with removeprefix("FILE:") in load_kerberos_ccache() to avoid stripping leading characters that happen to match the charset {F,I,L,E,:}.
  • Utils — PFX Validation: load_pfx() now raises ValueError if the parsed PFX is missing either a private key or a certificate, rather than propagating a NoneType error downstream.

Fixes (f96a20d)

  • !netrun — TargetParameterCountException: Fixed invocation of Assembly.EntryPoint.Invoke — arguments are now correctly wrapped as [object[]](,[string[]](...)) when args are present, or $null otherwise.
  • CredSSP — TLS Trailer Padding: tls_trailer_length is now calculated against the original plaintext length instead of the post-encryption ciphertext length.
  • _SAFE_WIN_PATH_RE: Relaxed regex to allow UNC paths (\\server\share\...) in addition to drive-letter paths, while retaining the PS injection character blocklist.
  • !download — Malformed Base64: Wrapped stream chunk decoding in try/except to prevent client crash on malformed Base64 output from a rogue server.

Hardening (604bb53)

  • Transcript Log: start_log() now opens with O_CREAT|O_EXCL|O_WRONLY and randomizes the filename with a 4-byte urandom suffix.
  • History File: .pwnrm_history creation now unlinks any existing symlink at that path before O_CREAT|O_EXCL open.

Maintenance (dca4f4c)

  • Bump version to 1.1.1 across pyproject.toml, __init__.py, and api.py.
  • Banner updated to grayscale gradient.
  • Credit to @phamthanha-a for reporting GHSA-jwc5-6vfh-r4h2.

Full Changelog: v1.1.0...v1.1.1

v1.1.0

Choose a tag to compare

@uziii2208 uziii2208 released this 18 Aug 07:20

What's Changed

Security Fixes (88b3125)

  • ASN.1 / GSS-API Framing: Fixed BER/DER short-form length decoding in krb5_mech_indep_token_decode — skip was incorrectly calculated as 2 + length instead of 2, causing malformed token parsing on any non-trivial Kerberos exchange.
  • CredSSP Auth Validation: Properly unwrap the server pubKeyAuth token before comparing against the SHA-256 EPA binding hash. Previously the raw octet string was compared directly, making the MitM detection ineffective.
  • SPNEGO Empty Token Guard: SPNEGOProxyNTLM.step() now short-circuits cleanly when data_in is empty (server accepts auth on HTTP 200 without a negotiation token).
  • URL Parsing Hardening: create_transport() now sanitizes http:// / https://-prefixed targets via urlparse to prevent host and port mangling when a full URL is passed as the target argument.
  • History File Permissions: .pwnrm_history is now created with 0o600 (or chmod-ed on first open) to prevent multi-tenant local credential leaks.
  • Remote Path Injection: upload() and download() now validate the server-returned temp path via _validate_remote_path() before embedding it in PowerShell execution contexts, with a safe fallback to C:\Windows\Temp\.
  • Certificate Port Binding: get_server_certificate() now defaults to port 5986 for HTTPS targets instead of 443, preventing EPA channel binding desync on non-standard WinRM deployments.
  • strip_ansi() Null Safety: Added a None guard — previously passed a None value (e.g. from a missing XML element) would raise TypeError mid-session.

Core Fixes (3069e2f)

  • PSRP Defragmentation: Raised the minimum valid fragment size in _defragment() from 21 → 65 bytes (header 13B + payload prefix 20B + UUID fields 32B) to prevent struct.error on short/malformed streams.
  • Empty Stream Elements: b64decode now receives s.text or "" instead of a bare s.text to handle empty <rsp:Stream/> elements without crashing.
  • SOAP Action Null Check: wsa:Action element existence is now validated before .text access — previously a malformed SOAP response missing the header caused an unhandled AttributeError.
  • PIPELINE_STATE Parsing: PIPELINE_STATE integer is now parsed with a try/except fallback to 0 — prevents TypeError/ValueError on malformed server responses.
  • Terminal State Handling: Runspace run_command() now recognizes CommandState/Terminated and CommandState/Failed in addition to CommandState/Done to prevent infinite receive loops on abnormal command termination.
  • Fragment Buffer Reset: Fragment buffer is now reset on START_FRAGMENT (flags & 0b10) to prevent cross-message state poisoning.
  • SOAP Fault Detection: Added fallback detection via s:Fault element when wsa:Action header is absent from the SOAP response.

Shell Fixes (9d6c6a2, 83e9064)

  • _PWNRM_DIR NameError: Defined _PWNRM_DIR at module level before the PwnShell class — previously caused an immediate NameError crash on any TTY session startup (FileHistory) and on !log.
  • Download Integrity Bypass: Added a len(buf) < 32 guard in download() before the MD5 trailer slice — a rogue server returning exactly the 32-byte MD5(empty) hash would silently pass the integrity check and write a 0-byte file. Adjusted from < 33 to < 32 to correctly allow 0-byte file downloads.
  • src_ps Stale Reference: src_ps is now re-derived after src is reassigned to the remote zip path in directory download mode — previously Remove-Item and Download-Remote operated on the original directory path instead of the generated .zip temp file.
  • Empty Args IndexError (!psrun, !upload, !netrun): All three commands now guard against empty args before any args[0] access, returning a usage hint instead of crashing with IndexError.
  • Backslash Normalization: Fixed over-escaped backslash replacement in _importPathFix (\\\\\\\\ → \\\\) — directory ZIP generation was producing malformed paths on the remote side.

Hardening (6db35ee)

  • Session Directory Permissions: ~/.pwnrm is now chmod 0o700 at startup and transcript log files are opened with 0o600 — prevents other local users from reading command history or session transcripts containing credentials.
  • Transport Decryption Integrity: Decryption failures in _decrypted_response() now raise TransportError immediately instead of silently logging and continuing, preventing stream desync.

Full Changelog: v1.0.5...v1.1.0

v1.0.5

Choose a tag to compare

@uziii2208 uziii2208 released this 17 Aug 19:53

What's Changed

Improvements & Hardening (db744d2)

  • PowerShell Helpers: Added character escaping (_pde) for server-provided paths embedded in PowerShell execution contexts.
  • Stream Handling: Added memory limits (max 256 object IDs / 32 MB) to in-flight PSRP fragment buffers to prevent resource exhaustion.
  • XML Processing: Migrated inbound XML parsing to defusedxml for safer document processing.
  • Path Validation: Added _validate_remote_path to enforce strict path format verification on remote server responses.
  • File Permissions: Hardened temporary certificate handling by enforcing restricted filesystem access (0o700 / 0o600) during key extraction.

Dependencies (db744d2)

  • Added defusedxml>=0.7.1

Maintenance (b30ead6)

  • Bump version to 1.0.5 and update distribution build artifacts.

Full Changelog: v1.0.4...v1.0.5