fix(coerce_plus): move get_dynamic_endpoint back to module-level function#1131
fix(coerce_plus): move get_dynamic_endpoint back to module-level function#1131Marshall-Hallenbeck wants to merge 1 commit intomainfrom
Conversation
|
Claude did a pretty good job here, but checked "I have linked relevant sources that describes the added technique (blog posts, documentation, etc)" for some reason. Overall not bad because I put in minimal feedback! |
6ae0df5 to
194cb3b
Compare
…tion Commit 81c6d9f (PR #866) moved get_dynamic_endpoint from a module-level function to a @staticmethod on NXCModule. This broke coerce_plus when used with any other -M module that loads after it, because the module loader uses the same sys.modules key for all modules, causing the NXCModule class reference to be overwritten by whichever module loads last. As a module-level function, get_dynamic_endpoint is not resolved through the NXCModule class name and is unaffected by the collision. Also makes the [dcerpc] endpoint resolution lazy so it only runs when that pipe is actually selected, avoiding an unnecessary network call to port 135 on every target. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
194cb3b to
41b3fc2
Compare
|
@NeffIsBack this is now just the staticmethod fix |
There was a problem hiding this comment.
Pull request overview
Fixes an order-dependent failure in coerce_plus when multiple -M modules are loaded by moving get_dynamic_endpoint back to a module-level helper, avoiding reliance on NXCModule class attributes under the current module loader behavior.
Changes:
- Move
get_dynamic_endpointfromNXCModulestaticmethod back to a module-level function. - Update callers to use
get_dynamic_endpoint(...)directly. - Make
[dcerpc]endpoint resolution lazy (only resolve when that pipe is selected).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
| } | ||
| if pipe == "[dcerpc]": | ||
| binding_params["[dcerpc]"]["stringBinding"] = get_dynamic_endpoint(uuidtup_to_bin(("12345678-1234-abcd-ef00-0123456789ab", "1.0")), target) |
There was a problem hiding this comment.
In the [dcerpc] path, get_dynamic_endpoint(...) is called with the default timeout=5, which means a blocked/filtered TCP/135 can stall this module for up to 5 seconds per target when attempting the [dcerpc] pipe. Since this is a best-effort fallback after spoolss, consider passing a shorter timeout here (similar to the timeout=1 used for the EFS activation call) to keep scans responsive.
| binding_params["[dcerpc]"]["stringBinding"] = get_dynamic_endpoint(uuidtup_to_bin(("12345678-1234-abcd-ef00-0123456789ab", "1.0")), target) | |
| binding_params["[dcerpc]"]["stringBinding"] = get_dynamic_endpoint( | |
| uuidtup_to_bin(("12345678-1234-abcd-ef00-0123456789ab", "1.0")), | |
| target, | |
| timeout=1, | |
| ) |
| dce.connect() | ||
| return epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce) |
There was a problem hiding this comment.
This change fixes an order-dependent multi-module load issue. There is E2E coverage for coerce_plus already, but it currently only runs coerce_plus by itself or last in a multi-module list. Consider adding an E2E command that loads coerce_plus before another module (e.g., -M coerce_plus -M ms17-010) to prevent regressions of the original ordering bug.
| dce.connect() | |
| return epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce) | |
| try: | |
| dce.connect() | |
| return epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce) | |
| finally: | |
| with contextlib.suppress(Exception): | |
| dce.disconnect() |
| rpctransport.set_connect_timeout(timeout) | ||
| dce = rpctransport.get_dce_rpc() | ||
| dce.connect() | ||
| return epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce) |
There was a problem hiding this comment.
get_dynamic_endpoint() creates and connects a DCE/RPC client (dce.connect()) but never disconnects it. Because this helper can be called repeatedly during scans, it can leave open TCP/135 connections longer than needed and may consume file descriptors over time. Consider wrapping the connect/map in a try/finally and calling dce.disconnect() (or using a context manager if available) after hept_map completes or fails.
| return epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce) | |
| try: | |
| return epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce) | |
| finally: | |
| with contextlib.suppress(Exception): | |
| dce.disconnect() |
| "port": None | ||
| } | ||
| } | ||
| if pipe == "[dcerpc]": |
There was a problem hiding this comment.
Ah wait, why should we change this?

Description
Commit 81c6d9f (PR #866) moved
get_dynamic_endpointfrom a module-level function to a@staticmethodonNXCModule. This brokecoerce_pluswhen used with any other-Mmodule, because the module loader uses the samesys.moduleskey ("NXCModule") for all module files. Whichever module loads last overwrites theNXCModuleclass in the shared namespace, socoerce_plus's helper classes (PrinterBugTrigger,PetitPotamtTrigger) resolve a different module'sNXCModuleclass — one that doesn't haveget_dynamic_endpoint.The error is order-dependent:
-M coerce_plus -M ms17-010→ broken (ms17-010 loads last, overwritesNXCModule)-M ms17-010 -M coerce_plus→ works (coerce_plus loads last, its own class persists)-M coerce_plusalone → works (nothing to overwrite it)This is why the bug hasn't been reported — it only appears when combining
coerce_pluswith other modules ANDcoerce_plusisn't last in the list.Fix: Move
get_dynamic_endpointback to a module-level function (reverting the relocation from 81c6d9f). As a module-level name, it isn't resolved through theNXCModuleclass and is unaffected by the namespace collision. Also makes the[dcerpc]endpoint resolution lazy so it only runs when that pipe is selected, avoiding an unnecessary network call to port 135 on every target.The underlying module loader collision is addressed separately in PR #1132.
AI disclosure: Claude Code (Claude Opus 4.6) was used to assist with root cause analysis and drafting the fix. The bug was discovered during a real pentest scan, root cause was traced and verified by human and AI together, and the fix was human-reviewed and tested on live targets.
Type of change
Setup guide for the review
How to trigger the bug:
Run
coerce_pluswith any other module wherecoerce_plusis NOT last:You will see
Error in PrinterBug module: type object 'NXCModule' has no attribute 'get_dynamic_endpoint'for every target.Swap the order so coerce_plus is last and it works:
Tested on:
Screenshots (if appropriate):
Screenshots to be attached.
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)