Skip to content

Add LDAP query obfuscation support via ldapx#1192

Open
j0hnZ3RA wants to merge 5 commits intoPennyw0rth:mainfrom
j0hnZ3RA:feature/ldap-query-obfuscation
Open

Add LDAP query obfuscation support via ldapx#1192
j0hnZ3RA wants to merge 5 commits intoPennyw0rth:mainfrom
j0hnZ3RA:feature/ldap-query-obfuscation

Conversation

@j0hnZ3RA
Copy link
Copy Markdown

@j0hnZ3RA j0hnZ3RA commented Apr 8, 2026

Description

Integrate ldapx to enable LDAP query obfuscation through the --obfuscate flag, covering filters, BaseDNs, and attribute lists transparently via the central search() method.

LDAP query obfuscation has shown excellent results against identity protection solutions which rely on pattern matching of well-known LDAP filters to detect enumeration and attacks like Kerberoasting, AS-REP Roasting, delegation abuse, and admin reconnaissance. By applying composable transformations (case randomization, spacing injection, garbage filter insertion, hex encoding, De Morgan rewrites) the queries remain semantically identical but defeat signature-based detection.

Original:

(&(adminCount=1)(objectClass=user))

Obfuscated (CSG chain):

(&(|(adMiNCount=1)(mYsFWsLZpn:WLdZRcNUMN:=jQmvbtrrvU))(|(objectClass=user)(cMYwPeUXeH~=xHHQtiCaqL)))

Usage

# Default obfuscation (Case + Spacing + Garbage)
nxc ldap <dc> -u <user> -p <pass> --users --obfuscate

# Custom filter chain
nxc ldap <dc> -u <user> -p <pass> --groups --obfuscate --obfuscate-filter CSGH

# Full obfuscation (filter + baseDN + attributes)
nxc ldap <dc> -u <user> -p <pass> --users --obfuscate --obfuscate-basedn CS --obfuscate-attrs CR

Available chain codes (impacket-compatible)

Code Transformation
C Case randomization
S Spacing injection
G Garbage filter insertion
H Hex encoding of values
D De Morgan boolean rewrites
R Operand reordering

The OID code (O) is intentionally blocked as it is incompatible with impacket's LDAP parser.

Design

  • Zero overhead when disabled, conditional import, no code path changes without --obfuscate
  • Graceful fallback, if obfuscation fails on any query, the original is used with a debug log
  • Central hook, all queries flow through search(), so every enumeration command benefits automatically
  • ldapx is zero-dependency, adds no transitive dependencies to NetExec

This PR was created with the assistance of AI (Claude Code / Opus 4.6) for code generation, but all changes were human-reviewed, human-tested against a live Active Directory environment, and human-verified for correctness.

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, Dploot, lsassy, etc)
  • This PR was created with the assistance of AI (list what type of assistance, tool(s)/model(s) in the description)

Setup guide for the review

Requirements:

  • Install the ldapx dependency: pip install ldapx (zero external dependencies)
  • Any Active Directory environment for testing

Testing steps:

  1. Run any LDAP enumeration command without --obfuscate to establish baseline
  2. Run the same command with --obfuscate and verify identical results
  3. Run with --obfuscate --debug to observe the obfuscated queries in the log output
  4. Try custom chains with --obfuscate-filter CSGH or --obfuscate-basedn CS
  5. Verify that --obfuscate-filter CO is rejected with a clear error (OID incompatible with impacket)

All LDAP commands are covered: --users, --groups, --computers, --dc-list, --find-delegation, --trusted-for-delegation, --kerberoasting, --asreproast, --admin-count, --password-not-required, --pass-pol, --pso, --get-sid, --active-users, --query.

Screenshots (if appropriate):

executed with the --obfuscate flag

image

queries obfuscated

image

results remain the same

image

Checklist:

  • I have ran Ruff against my changes (poetry: 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 (PR here: https://github.com/Pennyw0rth/NetExec-Wiki)

j0hnZ3RA added 3 commits April 7, 2026 22:28
Add ldapx>=0.4.0 as a project dependency. ldapx is a zero-dependency
library that provides LDAP filter, BaseDN, and attribute list
obfuscation through composable middleware chains, enabling evasion of
security solutions that rely on pattern matching of LDAP queries.
Add a new argument group with options to control LDAP query obfuscation:
- --obfuscate: enable LDAP query obfuscation via ldapx
- --obfuscate-filter: chain codes for filter obfuscation (default: CSG)
- --obfuscate-basedn: chain codes for BaseDN obfuscation (off by default)
- --obfuscate-attrs: chain codes for attribute list obfuscation (off by default)
Hook ldapx into the central search() method so all LDAP queries are
automatically obfuscated when --obfuscate is enabled. This covers
filters, BaseDNs, and attribute lists with graceful fallback on errors.

Key changes:
- Conditional ldapx import with HAS_LDAPX guard
- Validation in create_conn_obj() blocks incompatible OID code
- Three private helpers (_obfuscate_filter/basedn/attrs) with try/except
- Refactor gmsa methods to route through search() for full coverage

LDAP query obfuscation applies transformations such as case randomization,
spacing injection, and garbage filter insertion that preserve query
semantics while defeating signature-based detection in identity protection
solutions.

Tested against Active Directory with all major enumeration commands
(--users, --groups, --computers, --dc-list, --find-delegation,
--kerberoasting, --asreproast, --admin-count, --pass-pol, --query, etc.)
producing identical results to non-obfuscated queries.
@NeffIsBack
Copy link
Copy Markdown
Member

Very interesting, thanks for the PR!

@NeffIsBack NeffIsBack added the enhancement New feature or request label Apr 11, 2026
Comment thread nxc/protocols/ldap.py Outdated
Comment thread nxc/protocols/ldap.py Outdated
Comment thread nxc/protocols/ldap.py Outdated
Comment on lines +719 to +750
def _obfuscate_filter(self, search_filter):
if not self.obfuscate_filter_chain or not search_filter:
return search_filter
try:
obfuscated = ldapx.obfuscate_filter(search_filter, self.obfuscate_filter_chain)
self.logger.debug(f"Obfuscated filter: {obfuscated}")
return obfuscated
except Exception as e:
self.logger.debug(f"ldapx filter obfuscation failed, using original: {e}")
return search_filter

def _obfuscate_basedn(self, base_dn):
if not self.obfuscate_basedn_chain or not base_dn:
return base_dn
try:
obfuscated = ldapx.obfuscate_basedn(base_dn, self.obfuscate_basedn_chain)
self.logger.debug(f"Obfuscated baseDN: {obfuscated}")
return obfuscated
except Exception as e:
self.logger.debug(f"ldapx baseDN obfuscation failed, using original: {e}")
return base_dn

def _obfuscate_attrs(self, attributes):
if not self.obfuscate_attrs_chain or not attributes:
return attributes
try:
obfuscated = ldapx.obfuscate_attrlist(attributes, self.obfuscate_attrs_chain)
self.logger.debug(f"Obfuscated attributes: {obfuscated}")
return obfuscated
except Exception as e:
self.logger.debug(f"ldapx attribute obfuscation failed, using original: {e}")
return attributes
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What could be reasons that this fails? Are there edge case scenarios where conversions fail, or what could lead to a crash?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No known crash scenarios with the current NetExec filters, all were tested against AD. The try/except is defensive, following the same pattern used throughout ldap.py (e.g. enum_host_info, check_ldap_signing), to prevent a future ldapx regression or an unusual filter from a third-party module from breaking the entire enumeration flow.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you prefer to remove the try/except and let exceptions propagate naturally, I can make that change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, do we want/need different levels of obfuscation? Imo we could just do --obfuscate and then apply all at once. I don't really see why I should only obfuscate parts of the query if we can just obfuscate as much as possible. Thoughts?

Then we could also just wrap them in one try&except.

- Use direct `import ldapx` since it is a declared dependency
- Move obfuscation config from create_conn_obj() to __init__()
- Access args attributes directly instead of using getattr()
- Removed --obfuscate-filter, --obfuscate-basedn, and --obfuscate-attrs
  flags; --obfuscate now applies filter, baseDN, and attribute list
  obfuscation together with sensible chain defaults
- Consolidated the three _obfuscate_* helpers into inline code in
  search() wrapped in a single try/except
@j0hnZ3RA
Copy link
Copy Markdown
Author

Agreed, simplified to a single --obfuscate flag that applies sensible default chains to filter, baseDN, and attribute list at once. The three helpers were consolidated into inline code in search() wrapped in a single try/except.

@NeffIsBack NeffIsBack added the reviewed code Label for when a static code review was done label Apr 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request reviewed code Label for when a static code review was done

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants