diff --git a/nxc/modules/maq.py b/nxc/modules/maq.py index 1ae4d04a02..ce554ca5dd 100644 --- a/nxc/modules/maq.py +++ b/nxc/modules/maq.py @@ -1,3 +1,5 @@ +from impacket.ldap import ldapasn1 as ldapasn1_impacket +from impacket.ldap import ldaptypes from nxc.helpers.misc import CATEGORY from nxc.parsers.ldap_results import parse_result_attributes @@ -15,14 +17,17 @@ class NXCModule: Podalirius: @podalirius_ """ - def options(self, context, module_options): - """No options available""" - name = "maq" description = "Retrieves the MachineAccountQuota domain-level attribute" supported_protocols = ["ldap"] category = CATEGORY.ENUMERATION + def options(self, context, module_options): + """ + USER Username to enumerate machine-join count for (optional) + """ + self.user = module_options.get("USER", None) + def on_login(self, context, connection): context.log.display("Getting the MachineAccountQuota") @@ -34,3 +39,68 @@ def on_login(self, context, connection): return context.log.highlight(f"MachineAccountQuota: {entries[0]['ms-DS-MachineAccountQuota']}") + + if self.user: + self._enum_user_machines(context, connection, self.user, int(entries[0]["ms-DS-MachineAccountQuota"])) + + def _enum_user_machines(self, context, connection, username, maq): + context.log.display(f"Resolving SID for user: {username}") + + user_resp = connection.search(f"(sAMAccountName={username})", ["objectSid"]) + user_entries = parse_result_attributes(user_resp) + if not user_entries: + context.log.fail(f"Could not resolve SID for user: {username}") + return + + user_sid = user_entries[0]["objectSid"] + context.log.debug(f"User SID: {user_sid}") + + context.log.display("Querying computers...") + + # SD flags control: OWNER_SECURITY_INFORMATION = 1 + # Needed to retrieve nTSecurityDescriptor owner for LDAP-created accounts (addcomputer.py) + sd_control = ldapasn1_impacket.Control() + sd_control["controlType"] = "1.2.840.113556.1.4.801" + sd_control["criticality"] = False + sd_control["controlValue"] = b"\x30\x03\x02\x01\x01" + + paged_control = ldapasn1_impacket.SimplePagedResultsControl(criticality=True, size=1000) + + comp_resp = connection.ldap_connection.search( + searchBase=connection.baseDN, + searchFilter="(objectCategory=computer)", + attributes=["name", "ms-DS-CreatorSID", "nTSecurityDescriptor"], + sizeLimit=0, + searchControls=[paged_control, sd_control], + ) + computers = parse_result_attributes(comp_resp) + + user_computers = [] + for comp in computers: + matched = False + + # Method 1: ms-DS-CreatorSID — set by DC for SAMR domain joins + creator_sid = comp.get("ms-DS-CreatorSID") + if creator_sid is not None: + matched = creator_sid == user_sid + + # Method 2: nTSecurityDescriptor owner — set for direct LDAP creation (addcomputer.py) + if not matched: + raw_sd = comp.get("nTSecurityDescriptor") + if raw_sd is not None: + sd = ldaptypes.SR_SECURITY_DESCRIPTOR() + sd.fromString(raw_sd) + matched = sd["OwnerSid"].formatCanonical() == user_sid + + if matched: + user_computers.append(comp.get("name", "Unknown")) + + count = len(user_computers) + remaining = maq - count + + context.log.highlight(f"Machines joined by '{username}': {count}/{maq} (remaining quota: {remaining})") + if remaining <= 0: + context.log.fail(f"'{username}' has reached the MachineAccountQuota — cannot join additional machines!") + + for name in user_computers: + context.log.highlight(f" Computer: {name}") diff --git a/nxc/parsers/ldap_results.py b/nxc/parsers/ldap_results.py index 36f673dca6..5fa8bb0546 100644 --- a/nxc/parsers/ldap_results.py +++ b/nxc/parsers/ldap_results.py @@ -17,6 +17,8 @@ def parse_result_attributes(ldap_response): val_decoded = UUID(bytes=val.__bytes__()) elif str(attribute["type"]) == "objectSid": val_decoded = sid_to_str(val.__bytes__()) + elif str(attribute["type"]) == "ms-DS-CreatorSID": + val_decoded = sid_to_str(val.__bytes__()) elif str(attribute["type"]) == "dNSProperty": val_decoded = val.__bytes__() else: diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 850262cfc5..192d25ee75 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -231,6 +231,7 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-net netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M groupmembership -o USER=LOGIN_USERNAME netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M laps netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M maq +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M maq -o USER=LOGIN_USERNAME netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M modify-group -o USER=USERNAME GROUP="Domain Admins" netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M modify-group -o USER=USERNAME GROUP="Domain Admins" REMOVE=True netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pre2k