From 4f13ec25a4a89bed0d1ce65603e92a387dfdc1cf Mon Sep 17 00:00:00 2001 From: j0hnZ3RA Date: Tue, 7 Apr 2026 22:28:10 -0300 Subject: [PATCH 1/5] Add ldapx dependency for LDAP query obfuscation 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. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 4fd1f71749..a1070c9fe0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ dependencies = [ "dploot>=3.2.2", "dsinternals>=1.2.4", "jwt>=1.3.1", + "ldapx>=0.4.0", "lsassy>=3.1.11", "masky>=0.2.1", "minikerberos>=0.4.1", From c76d6812d2fb4ce9c2c54e2e4e91eac7b1559134 Mon Sep 17 00:00:00 2001 From: j0hnZ3RA Date: Tue, 7 Apr 2026 22:28:34 -0300 Subject: [PATCH 2/5] Add --obfuscate CLI arguments for LDAP protocol 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) --- nxc/protocols/ldap/proto_args.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nxc/protocols/ldap/proto_args.py b/nxc/protocols/ldap/proto_args.py index b98c216d9c..23d44e831d 100644 --- a/nxc/protocols/ldap/proto_args.py +++ b/nxc/protocols/ldap/proto_args.py @@ -45,4 +45,10 @@ def proto_args(parser, parents): bgroup.add_argument("--bloodhound", action="store_true", help="Perform a Bloodhound scan") bgroup.add_argument("-c", "--collection", default="Default", help="Which information to collect. Supported: Group, LocalAdmin, Session, Trusts, Default, DCOnly, DCOM, RDP, PSRemote, LoggedOn, Container, ObjectProps, ACL, ADCS, All. You can specify more than one by separating them with a comma.") + ogroup = ldap_parser.add_argument_group("LDAP Obfuscation", "Options for LDAP query obfuscation via ldapx") + ogroup.add_argument("--obfuscate", action="store_true", help="Enable LDAP query obfuscation") + ogroup.add_argument("--obfuscate-filter", type=str, default="CSG", help="ldapx chain codes for filter obfuscation (default: CSG)") + ogroup.add_argument("--obfuscate-basedn", type=str, default=None, help="ldapx chain codes for baseDN obfuscation (disabled by default)") + ogroup.add_argument("--obfuscate-attrs", type=str, default=None, help="ldapx chain codes for attribute list obfuscation (disabled by default)") + return parser From b2bcef46a83e5edd485ac352251b0f1014248d74 Mon Sep 17 00:00:00 2001 From: j0hnZ3RA Date: Tue, 7 Apr 2026 22:30:52 -0300 Subject: [PATCH 3/5] Integrate ldapx obfuscation into LDAP search pipeline 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. --- nxc/protocols/ldap.py | 82 +++++++++++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 18 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 8b271f6714..5896282a17 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -50,6 +50,12 @@ from nxc.helpers.negotiate_parser import parse_challenge from nxc.paths import CONFIG_PATH +try: + import ldapx + HAS_LDAPX = True +except ImportError: + HAS_LDAPX = False + ldap_error_status = { "1": "STATUS_NOT_SUPPORTED", "533": "STATUS_ACCOUNT_DISABLED", @@ -89,6 +95,10 @@ def __init__(self, args, db, host): self.sid_domain = "" self.scope = None self.configuration_context = "" + self.obfuscate = False + self.obfuscate_filter_chain = "" + self.obfuscate_basedn_chain = None + self.obfuscate_attrs_chain = None connection.__init__(self, args, db, host) @@ -103,6 +113,19 @@ def proto_logger(self): ) def create_conn_obj(self): + if getattr(self.args, "obfuscate", False): + if not HAS_LDAPX: + self.logger.fail("--obfuscate requires ldapx. Install with: pip install ldapx") + return False + self.obfuscate = True + self.obfuscate_filter_chain = getattr(self.args, "obfuscate_filter", "CSG") + self.obfuscate_basedn_chain = getattr(self.args, "obfuscate_basedn", None) + self.obfuscate_attrs_chain = getattr(self.args, "obfuscate_attrs", None) + if self.obfuscate_filter_chain and "O" in self.obfuscate_filter_chain: + self.logger.fail("Filter chain code 'O' (OID) is incompatible with impacket's LDAP parser. Remove it from --obfuscate-filter.") + return False + self.logger.info(f"LDAP obfuscation enabled: filter={self.obfuscate_filter_chain}, baseDN={self.obfuscate_basedn_chain or 'disabled'}, attrs={self.obfuscate_attrs_chain or 'disabled'}") + try: proto = "ldaps" if self.port == 636 else "ldap" ldap_url = f"{proto}://{self.host}" @@ -660,6 +683,11 @@ def search(self, searchFilter, attributes, sizeLimit=0, baseDN=None, searchContr elif baseDN is None: baseDN = self.baseDN + if self.obfuscate: + searchFilter = self._obfuscate_filter(searchFilter) + baseDN = self._obfuscate_basedn(baseDN) + attributes = self._obfuscate_attrs(attributes) + try: if self.ldap_connection: self.logger.debug(f"Search Filter={searchFilter}") @@ -688,6 +716,39 @@ def search(self, searchFilter, attributes, sizeLimit=0, baseDN=None, searchContr return [] return [] + 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 + def users(self): """ Retrieves user information from the LDAP server. @@ -1295,12 +1356,7 @@ def gmsa(self): sids = [ace["Ace"]["Sid"].formatCanonical() for ace in dacl["Dacl"]["Data"] if ace["AceType"] == 0x00] self.logger.debug(f"msDS-GroupMSAMembership: {sids}") search_filter = "(|" + "".join([f"(objectSid={sid})" for sid in sids]) + ")" - resp = self.ldap_connection.search( - searchBase=self.baseDN, - searchFilter=search_filter, - attributes=["sAMAccountName"], - sizeLimit=0, - ) + resp = self.search(search_filter, ["sAMAccountName"]) resp_parsed = parse_result_attributes(resp) if len(resp_parsed) > 1: principal_with_read = [f"{item['sAMAccountName']}" for item in resp_parsed] @@ -1340,12 +1396,7 @@ def gmsa_convert_id(self): else: # getting the gmsa account search_filter = "(objectClass=msDS-GroupManagedServiceAccount)" - gmsa_accounts = self.ldap_connection.search( - searchBase=self.baseDN, - searchFilter=search_filter, - attributes=["sAMAccountName"], - sizeLimit=0, - ) + gmsa_accounts = self.search(search_filter, ["sAMAccountName"]) gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts) if gmsa_accounts_parsed: self.logger.debug(f"Total of records returned {len(gmsa_accounts_parsed):d}") @@ -1363,12 +1414,7 @@ def gmsa_decrypt_lsa(self): gmsa_id, gmsa_pass = self.args.gmsa_decrypt_lsa.split("_")[4].split(":") # getting the gmsa account search_filter = "(objectClass=msDS-GroupManagedServiceAccount)" - gmsa_accounts = self.ldap_connection.search( - searchBase=self.baseDN, - searchFilter=search_filter, - attributes=["sAMAccountName"], - sizeLimit=0, - ) + gmsa_accounts = self.search(search_filter, ["sAMAccountName"]) gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts) if gmsa_accounts_parsed: self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}") From df27567f5f4c27c8895f580df8701b235b946467 Mon Sep 17 00:00:00 2001 From: j0hnZ3RA Date: Sat, 11 Apr 2026 13:49:00 -0300 Subject: [PATCH 4/5] Address review: direct import, init in __init__, drop getattr - 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() --- nxc/protocols/ldap.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 5896282a17..1df487a912 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -50,11 +50,7 @@ from nxc.helpers.negotiate_parser import parse_challenge from nxc.paths import CONFIG_PATH -try: - import ldapx - HAS_LDAPX = True -except ImportError: - HAS_LDAPX = False +import ldapx ldap_error_status = { "1": "STATUS_NOT_SUPPORTED", @@ -95,10 +91,10 @@ def __init__(self, args, db, host): self.sid_domain = "" self.scope = None self.configuration_context = "" - self.obfuscate = False - self.obfuscate_filter_chain = "" - self.obfuscate_basedn_chain = None - self.obfuscate_attrs_chain = None + self.obfuscate = args.obfuscate + self.obfuscate_filter_chain = args.obfuscate_filter if args.obfuscate else "" + self.obfuscate_basedn_chain = args.obfuscate_basedn if args.obfuscate else None + self.obfuscate_attrs_chain = args.obfuscate_attrs if args.obfuscate else None connection.__init__(self, args, db, host) @@ -113,14 +109,7 @@ def proto_logger(self): ) def create_conn_obj(self): - if getattr(self.args, "obfuscate", False): - if not HAS_LDAPX: - self.logger.fail("--obfuscate requires ldapx. Install with: pip install ldapx") - return False - self.obfuscate = True - self.obfuscate_filter_chain = getattr(self.args, "obfuscate_filter", "CSG") - self.obfuscate_basedn_chain = getattr(self.args, "obfuscate_basedn", None) - self.obfuscate_attrs_chain = getattr(self.args, "obfuscate_attrs", None) + if self.obfuscate: if self.obfuscate_filter_chain and "O" in self.obfuscate_filter_chain: self.logger.fail("Filter chain code 'O' (OID) is incompatible with impacket's LDAP parser. Remove it from --obfuscate-filter.") return False From abed22e13979211b62e7acd6d7bc347d031a7c43 Mon Sep 17 00:00:00 2001 From: j0hnZ3RA Date: Mon, 13 Apr 2026 01:33:41 -0300 Subject: [PATCH 5/5] Simplify LDAP obfuscation to a single --obfuscate flag - 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 --- nxc/protocols/ldap.py | 54 +++++++------------------------- nxc/protocols/ldap/proto_args.py | 6 +--- 2 files changed, 12 insertions(+), 48 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 1df487a912..fe413bb5c0 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -92,9 +92,6 @@ def __init__(self, args, db, host): self.scope = None self.configuration_context = "" self.obfuscate = args.obfuscate - self.obfuscate_filter_chain = args.obfuscate_filter if args.obfuscate else "" - self.obfuscate_basedn_chain = args.obfuscate_basedn if args.obfuscate else None - self.obfuscate_attrs_chain = args.obfuscate_attrs if args.obfuscate else None connection.__init__(self, args, db, host) @@ -110,10 +107,7 @@ def proto_logger(self): def create_conn_obj(self): if self.obfuscate: - if self.obfuscate_filter_chain and "O" in self.obfuscate_filter_chain: - self.logger.fail("Filter chain code 'O' (OID) is incompatible with impacket's LDAP parser. Remove it from --obfuscate-filter.") - return False - self.logger.info(f"LDAP obfuscation enabled: filter={self.obfuscate_filter_chain}, baseDN={self.obfuscate_basedn_chain or 'disabled'}, attrs={self.obfuscate_attrs_chain or 'disabled'}") + self.logger.info("LDAP query obfuscation enabled") try: proto = "ldaps" if self.port == 636 else "ldap" @@ -673,9 +667,16 @@ def search(self, searchFilter, attributes, sizeLimit=0, baseDN=None, searchContr baseDN = self.baseDN if self.obfuscate: - searchFilter = self._obfuscate_filter(searchFilter) - baseDN = self._obfuscate_basedn(baseDN) - attributes = self._obfuscate_attrs(attributes) + try: + if searchFilter: + searchFilter = ldapx.obfuscate_filter(searchFilter, "CSG") + if baseDN: + baseDN = ldapx.obfuscate_basedn(baseDN, "CX") + if attributes: + attributes = ldapx.obfuscate_attrlist(attributes, "CR") + self.logger.debug(f"Obfuscated baseDN: {baseDN}") + except Exception as e: + self.logger.debug(f"ldapx obfuscation failed, using original query: {e}") try: if self.ldap_connection: @@ -705,39 +706,6 @@ def search(self, searchFilter, attributes, sizeLimit=0, baseDN=None, searchContr return [] return [] - 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 - def users(self): """ Retrieves user information from the LDAP server. diff --git a/nxc/protocols/ldap/proto_args.py b/nxc/protocols/ldap/proto_args.py index 23d44e831d..4b28976956 100644 --- a/nxc/protocols/ldap/proto_args.py +++ b/nxc/protocols/ldap/proto_args.py @@ -45,10 +45,6 @@ def proto_args(parser, parents): bgroup.add_argument("--bloodhound", action="store_true", help="Perform a Bloodhound scan") bgroup.add_argument("-c", "--collection", default="Default", help="Which information to collect. Supported: Group, LocalAdmin, Session, Trusts, Default, DCOnly, DCOM, RDP, PSRemote, LoggedOn, Container, ObjectProps, ACL, ADCS, All. You can specify more than one by separating them with a comma.") - ogroup = ldap_parser.add_argument_group("LDAP Obfuscation", "Options for LDAP query obfuscation via ldapx") - ogroup.add_argument("--obfuscate", action="store_true", help="Enable LDAP query obfuscation") - ogroup.add_argument("--obfuscate-filter", type=str, default="CSG", help="ldapx chain codes for filter obfuscation (default: CSG)") - ogroup.add_argument("--obfuscate-basedn", type=str, default=None, help="ldapx chain codes for baseDN obfuscation (disabled by default)") - ogroup.add_argument("--obfuscate-attrs", type=str, default=None, help="ldapx chain codes for attribute list obfuscation (disabled by default)") + ldap_parser.add_argument("--obfuscate", action="store_true", help="Enable LDAP query obfuscation via ldapx") return parser