Add LDAP query obfuscation support via ldapx#1192
Add LDAP query obfuscation support via ldapx#1192j0hnZ3RA wants to merge 5 commits intoPennyw0rth:mainfrom
Conversation
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.
|
Very interesting, thanks for the PR! |
| 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 |
There was a problem hiding this comment.
What could be reasons that this fails? Are there edge case scenarios where conversions fail, or what could lead to a crash?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
If you prefer to remove the try/except and let exceptions propagate naturally, I can make that change.
There was a problem hiding this comment.
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
|
Agreed, simplified to a single |
Description
Integrate ldapx to enable LDAP query obfuscation through the
--obfuscateflag, covering filters, BaseDNs, and attribute lists transparently via the centralsearch()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:
Obfuscated (CSG chain):
Usage
Available chain codes (impacket-compatible)
Design
--obfuscatesearch(), so every enumeration command benefits automaticallyThis 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
Setup guide for the review
Requirements:
ldapxdependency:pip install ldapx(zero external dependencies)Testing steps:
--obfuscateto establish baseline--obfuscateand verify identical results--obfuscate --debugto observe the obfuscated queries in the log output--obfuscate-filter CSGHor--obfuscate-basedn CS--obfuscate-filter COis 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
queries obfuscated
results remain the same
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)