Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 73 additions & 3 deletions nxc/modules/maq.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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")

Expand All @@ -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}")
2 changes: 2 additions & 0 deletions nxc/parsers/ldap_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions tests/e2e_commands.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down