From 176c480beb05f80fe0c8ab50a1ffa0f8b02981a4 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Sun, 21 Jul 2024 12:54:20 +0300 Subject: [PATCH 01/18] Update proto_args, added find delegation Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/ldap/proto_args.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nxc/protocols/ldap/proto_args.py b/nxc/protocols/ldap/proto_args.py index fc01c9d30..e97f98458 100644 --- a/nxc/protocols/ldap/proto_args.py +++ b/nxc/protocols/ldap/proto_args.py @@ -17,6 +17,7 @@ def proto_args(parser, parents): vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain", "Options to to play with Kerberos") vgroup.add_argument("--query", nargs=2, help="Query LDAP with a custom filter and attributes") + vgroup.add_argument("--find-delegation", action="store_true", help="Finds delegation relationships within an Active Directory domain.") vgroup.add_argument("--trusted-for-delegation", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION") vgroup.add_argument("--password-not-required", action="store_true", help="Get the list of users with flag PASSWD_NOTREQD") vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1") From 18a857396ca9c59f334bdaa2a4db0423e6b33c49 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Sun, 21 Jul 2024 12:59:10 +0300 Subject: [PATCH 02/18] Update ldap.py, added findDelegation Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/ldap.py | 106 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 59af5b9b9..07c12b892 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -27,6 +27,7 @@ from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError from impacket.krb5.types import Principal, KerberosException from impacket.ldap import ldap as ldap_impacket +from impacket.ldap import ldaptypes from impacket.ldap import ldapasn1 as ldapasn1_impacket from impacket.ldap.ldap import LDAPFilterSyntaxError from impacket.smb import SMB_DIALECT @@ -1085,6 +1086,111 @@ def query(self): vals = vals.replace("SetOf: ", "") self.logger.highlight(f"{attr:<20} {vals}") + def find_delegation(self): + def printTable(items, header): + colLen = [] + for i, col in enumerate(header): + rowMaxLen = max(len(str(row[i])) for row in items) + colLen.append(max(rowMaxLen, len(col))) + + # Create the format string for each row + outputFormat = " ".join([f"{{{num}:{width}s}}" for num, width in enumerate(colLen)]) + + # Print header + self.logger.highlight(outputFormat.format(*header)) + self.logger.highlight(" ".join(["-" * itemLen for itemLen in colLen])) + + # Print rows + for row in items: + self.logger.highlight(outputFormat.format(*row)) + + # Building the search filter + search_filter = ("(&(|(UserAccountControl:1.2.840.113556.1.4.803:=16777216)(UserAccountControl:1.2.840.113556.1.4.803:=" + "524288)(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))" + "(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(UserAccountControl:1.2.840.113556.1.4.803:=8192)))" + ) + attributes = ["sAMAccountName", + "pwdLastSet", + "userAccountControl", + "objectCategory", + "msDS-AllowedToActOnBehalfOfOtherIdentity", + "msDS-AllowedToDelegateTo"] + + resp = self.search(search_filter, attributes, 0) + + answers = [] + self.logger.debug(f"Total of records returned {len(resp):d}") + + for item in resp: + if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: + continue + mustCommit = False + sAMAccountName = "" + userAccountControl = 0 + delegation = "" + objectType = "" + rightsTo = [] + protocolTransition = 0 + + # After receiving responses we parse through to determine the type of delegation configured on each object + try: + for attribute in item["attributes"]: + if str(attribute["type"]) == "sAMAccountName": + sAMAccountName = str(attribute["vals"][0]) + mustCommit = True + elif str(attribute["type"]) == "userAccountControl": + userAccountControl = str(attribute["vals"][0]) + if int(userAccountControl) & UF_TRUSTED_FOR_DELEGATION: + delegation = "Unconstrained" + rightsTo.append("N/A") + elif int(userAccountControl) & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: + delegation = "Constrained w/ Protocol Transition" + protocolTransition = 1 + elif str(attribute["type"]) == "objectCategory": + objectType = str(attribute["vals"][0]).split("=")[1].split(",")[0] + elif str(attribute["type"]) == "msDS-AllowedToDelegateTo": + if protocolTransition == 0: + delegation = "Constrained" + rightsTo = list(attribute["vals"]) + + # Not an elif as an object could both have rbcd and another type of delegation configured for the same object + if str(attribute["type"]) == "msDS-AllowedToActOnBehalfOfOtherIdentity": + rbcdRights = [] + rbcdObjType = [] + search_filter = "(&(|" + sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=bytes(attribute["vals"][0])) + for ace in sd["Dacl"].aces: + search_filter = search_filter + "(objectSid=" + ace["Ace"]["Sid"].formatCanonical() + ")" + search_filter = search_filter + ")(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" + delegUserResp = self.search(search_filter, attributes=["sAMAccountName", "objectCategory"], sizeLimit=999) + for item2 in delegUserResp: + if isinstance(item2, ldapasn1_impacket.SearchResultEntry) is not True: + continue + rbcdRights.append(str(item2["attributes"][0]["vals"][0])) + rbcdObjType.append(str(item2["attributes"][1]["vals"][0]).split("=")[1].split(",")[0]) + + if mustCommit is True: + if int(userAccountControl) & UF_ACCOUNTDISABLE: + self.logger.debug("Bypassing disabled account %s " % sAMAccountName) + else: + for rights, objType in zip(rbcdRights, rbcdObjType): + answers.append([rights, objType, "Resource-Based Constrained", sAMAccountName]) + + # Print unconstrained + constrained delegation relationships + if (delegation in ["Unconstrained", "Constrained", "Constrained w/ Protocol Transition"] and mustCommit): + if int(userAccountControl) & UF_ACCOUNTDISABLE: + self.logger.debug("Bypassing disabled account %s " % sAMAccountName) + else: + answers = [sAMAccountName, objectType, delegation, rightsTo] + + except Exception as e: + self.logger.error("Skipping item, cannot process due to error %s" % str(e)) + + if len(answers) > 0: + printTable(answers, header=["AccountName", "AccountType", "DelegationType", "DelegationRightsTo"]) + else: + self.logger.fail("No entries found!") + def trusted_for_delegation(self): # Building the search filter searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=524288)" From a9181f469d88182e1cde8ad4858c734bd7844e52 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:32:23 +0300 Subject: [PATCH 03/18] Update ldap.py for find delegation Added try except on header Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/ldap.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 07c12b892..f64a0c4e0 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1089,20 +1089,23 @@ def query(self): def find_delegation(self): def printTable(items, header): colLen = [] - for i, col in enumerate(header): - rowMaxLen = max(len(str(row[i])) for row in items) - colLen.append(max(rowMaxLen, len(col))) - - # Create the format string for each row - outputFormat = " ".join([f"{{{num}:{width}s}}" for num, width in enumerate(colLen)]) - - # Print header - self.logger.highlight(outputFormat.format(*header)) - self.logger.highlight(" ".join(["-" * itemLen for itemLen in colLen])) - - # Print rows - for row in items: - self.logger.highlight(outputFormat.format(*row)) + try: + for i, col in enumerate(header): + rowMaxLen = max(len(str(row[i])) for row in items) + colLen.append(max(rowMaxLen, len(col))) + + # Create the format string for each row + outputFormat = " ".join([f"{{{num}:{width}s}}" for num, width in enumerate(colLen)]) + + # Print header + self.logger.highlight(outputFormat.format(*header)) + self.logger.highlight(" ".join(["-" * itemLen for itemLen in colLen])) + + # Print rows + for row in items: + self.logger.highlight(outputFormat.format(*row)) + except Exception as e: + self.logger.fail("Header Index error " + str(e)) # Seen in line rowMaxlen and highlight row variable # Building the search filter search_filter = ("(&(|(UserAccountControl:1.2.840.113556.1.4.803:=16777216)(UserAccountControl:1.2.840.113556.1.4.803:=" From 509f6d1373d26523567dd790d03d08961ee3eb95 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:38:57 +0300 Subject: [PATCH 04/18] Update ldap.py ruff fix Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/ldap.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index f64a0c4e0..b8f8b931d 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1090,22 +1090,22 @@ def find_delegation(self): def printTable(items, header): colLen = [] try: - for i, col in enumerate(header): - rowMaxLen = max(len(str(row[i])) for row in items) - colLen.append(max(rowMaxLen, len(col))) - - # Create the format string for each row - outputFormat = " ".join([f"{{{num}:{width}s}}" for num, width in enumerate(colLen)]) - - # Print header - self.logger.highlight(outputFormat.format(*header)) - self.logger.highlight(" ".join(["-" * itemLen for itemLen in colLen])) - - # Print rows - for row in items: - self.logger.highlight(outputFormat.format(*row)) - except Exception as e: - self.logger.fail("Header Index error " + str(e)) # Seen in line rowMaxlen and highlight row variable + for i, col in enumerate(header): + rowMaxLen = max(len(str(row[i])) for row in items) + colLen.append(max(rowMaxLen, len(col))) + + # Create the format string for each row + outputFormat = " ".join([f"{{{num}:{width}s}}" for num, width in enumerate(colLen)]) + + # Print header + self.logger.highlight(outputFormat.format(*header)) + self.logger.highlight(" ".join(["-" * itemLen for itemLen in colLen])) + + # Print rows + for row in items: + self.logger.highlight(outputFormat.format(*row)) + except Exception as e: + self.logger.fail("Header Index error " + str(e)) # Building the search filter search_filter = ("(&(|(UserAccountControl:1.2.840.113556.1.4.803:=16777216)(UserAccountControl:1.2.840.113556.1.4.803:=" From 7db7de4f1ebfe1c3a1ac1fc290db62dd53324595 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:17:21 +0300 Subject: [PATCH 05/18] Update ldap.py. Added processAttributeValue Function A new helper function was introduced to process the content of LDAP AttributeValue objects. Updated printTable Function Resource-Based Constrained Delegation Processing Added Constant Variables Constant variables were defined for userAccountControl values. Modular Code Structure The overall structure was made more modular; functions were clearly separated for better readability. Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/ldap.py | 97 +++++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 45 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index b8f8b931d..9c2b0c603 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1087,46 +1087,54 @@ def query(self): self.logger.highlight(f"{attr:<20} {vals}") def find_delegation(self): + # Constants for delegation types + UF_TRUSTED_FOR_DELEGATION = 0x80000 + UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x1000000 + UF_ACCOUNTDISABLE = 0x2 + + def processAttributeValue(attribute): + # Extract the payload value from the AttributeValue object + if hasattr(attribute, "payload"): + return str(attribute.payload) + return str(attribute) + def printTable(items, header): colLen = [] - try: - for i, col in enumerate(header): - rowMaxLen = max(len(str(row[i])) for row in items) - colLen.append(max(rowMaxLen, len(col))) + for i, col in enumerate(header): + rowMaxLen = max(len(str(row[i])) for row in items) + colLen.append(max(rowMaxLen, len(col))) - # Create the format string for each row - outputFormat = " ".join([f"{{{num}:{width}s}}" for num, width in enumerate(colLen)]) + # Create the format string for each row + outputFormat = " ".join([f"{{{num}:{width}s}}" for num, width in enumerate(colLen)]) - # Print header - self.logger.highlight(outputFormat.format(*header)) - self.logger.highlight(" ".join(["-" * itemLen for itemLen in colLen])) + self.logger.highlight(outputFormat.format(*header)) + self.logger.highlight(" ".join(["-" * itemLen for itemLen in colLen])) - # Print rows - for row in items: - self.logger.highlight(outputFormat.format(*row)) - except Exception as e: - self.logger.fail("Header Index error " + str(e)) + # Print rows + for row in items: + # Burada DelegationRightsTo'yu düzeltmek için join() ekleyin + row[3] = ", ".join(str(x) for x in row[3]) if isinstance(row[3], list) else row[3] + self.logger.highlight(outputFormat.format(*row)) # Building the search filter - search_filter = ("(&(|(UserAccountControl:1.2.840.113556.1.4.803:=16777216)(UserAccountControl:1.2.840.113556.1.4.803:=" - "524288)(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))" - "(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(UserAccountControl:1.2.840.113556.1.4.803:=8192)))" - ) - attributes = ["sAMAccountName", - "pwdLastSet", - "userAccountControl", - "objectCategory", - "msDS-AllowedToActOnBehalfOfOtherIdentity", - "msDS-AllowedToDelegateTo"] + search_filter = ("(&(|(UserAccountControl:1.2.840.113556.1.4.803:=16777216)" + "(UserAccountControl:1.2.840.113556.1.4.803:=524288)" + "(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))" + "(!(UserAccountControl:1.2.840.113556.1.4.803:=2))" + "(!(UserAccountControl:1.2.840.113556.1.4.803:=8192)))") + attributes = ["sAMAccountName", "pwdLastSet", "userAccountControl", "objectCategory", + "msDS-AllowedToActOnBehalfOfOtherIdentity", "msDS-AllowedToDelegateTo"] + resp = self.search(search_filter, attributes, 0) answers = [] self.logger.debug(f"Total of records returned {len(resp):d}") for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: + if not isinstance(item, ldapasn1_impacket.SearchResultEntry): continue + mustCommit = False sAMAccountName = "" userAccountControl = 0 @@ -1134,8 +1142,7 @@ def printTable(items, header): objectType = "" rightsTo = [] protocolTransition = 0 - - # After receiving responses we parse through to determine the type of delegation configured on each object + try: for attribute in item["attributes"]: if str(attribute["type"]) == "sAMAccountName": @@ -1154,42 +1161,42 @@ def printTable(items, header): elif str(attribute["type"]) == "msDS-AllowedToDelegateTo": if protocolTransition == 0: delegation = "Constrained" - rightsTo = list(attribute["vals"]) - - # Not an elif as an object could both have rbcd and another type of delegation configured for the same object + rightsTo = [processAttributeValue(val) for val in attribute["vals"]] + + # Not an elif as an object could both have RBCD and another type of delegation if str(attribute["type"]) == "msDS-AllowedToActOnBehalfOfOtherIdentity": rbcdRights = [] rbcdObjType = [] - search_filter = "(&(|" sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=bytes(attribute["vals"][0])) + search_filter = "(&(|" for ace in sd["Dacl"].aces: - search_filter = search_filter + "(objectSid=" + ace["Ace"]["Sid"].formatCanonical() + ")" - search_filter = search_filter + ")(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" + search_filter += "(objectSid=" + ace["Ace"]["Sid"].formatCanonical() + ")" + search_filter += ")(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" delegUserResp = self.search(search_filter, attributes=["sAMAccountName", "objectCategory"], sizeLimit=999) + for item2 in delegUserResp: - if isinstance(item2, ldapasn1_impacket.SearchResultEntry) is not True: + if not isinstance(item2, ldapasn1_impacket.SearchResultEntry): continue rbcdRights.append(str(item2["attributes"][0]["vals"][0])) rbcdObjType.append(str(item2["attributes"][1]["vals"][0]).split("=")[1].split(",")[0]) - - if mustCommit is True: + + if mustCommit: if int(userAccountControl) & UF_ACCOUNTDISABLE: - self.logger.debug("Bypassing disabled account %s " % sAMAccountName) + self.logger.debug(f"Bypassing disabled account {sAMAccountName}") else: for rights, objType in zip(rbcdRights, rbcdObjType): answers.append([rights, objType, "Resource-Based Constrained", sAMAccountName]) - - # Print unconstrained + constrained delegation relationships - if (delegation in ["Unconstrained", "Constrained", "Constrained w/ Protocol Transition"] and mustCommit): + + if delegation in ["Unconstrained", "Constrained", "Constrained w/ Protocol Transition"] and mustCommit: if int(userAccountControl) & UF_ACCOUNTDISABLE: - self.logger.debug("Bypassing disabled account %s " % sAMAccountName) + self.logger.debug(f"Bypassing disabled account {sAMAccountName}") else: - answers = [sAMAccountName, objectType, delegation, rightsTo] + answers.append([sAMAccountName, objectType, delegation, rightsTo]) except Exception as e: - self.logger.error("Skipping item, cannot process due to error %s" % str(e)) - - if len(answers) > 0: + self.logger.error(f"Skipping item, cannot process due to error {e}") + + if answers: printTable(answers, header=["AccountName", "AccountType", "DelegationType", "DelegationRightsTo"]) else: self.logger.fail("No entries found!") From 29de0ccf349eb256796d183bcdfc1ea4a15dec8d Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:26:16 +0300 Subject: [PATCH 06/18] Used parse_result_attributes for parsing Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/ldap.py | 110 ++++++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 52 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 9c2b0c603..794d8a4dc 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -29,6 +29,7 @@ from impacket.ldap import ldap as ldap_impacket from impacket.ldap import ldaptypes from impacket.ldap import ldapasn1 as ldapasn1_impacket +from impacket.ldap.ldapasn1 import AttributeValue from impacket.ldap.ldap import LDAPFilterSyntaxError from impacket.smb import SMB_DIALECT from impacket.smbconnection import SMBConnection, SessionError @@ -1100,41 +1101,46 @@ def processAttributeValue(attribute): def printTable(items, header): colLen = [] + + # Calculating maximum lenght before parsing CN. for i, col in enumerate(header): - rowMaxLen = max(len(str(row[i])) for row in items) + rowMaxLen = max(len(row[1].split(",")[0].split("CN=")[-1]) for row in items) if i == 1 else max(len(str(row[i])) for row in items) colLen.append(max(rowMaxLen, len(col))) # Create the format string for each row outputFormat = " ".join([f"{{{num}:{width}s}}" for num, width in enumerate(colLen)]) + # Print header self.logger.highlight(outputFormat.format(*header)) self.logger.highlight(" ".join(["-" * itemLen for itemLen in colLen])) # Print rows for row in items: - # Burada DelegationRightsTo'yu düzeltmek için join() ekleyin + # Get first CN value. + if "CN=" in row[1]: + row[1] = row[1].split(",")[0].split("CN=")[-1] + + # Added join for DelegationRightsTo row[3] = ", ".join(str(x) for x in row[3]) if isinstance(row[3], list) else row[3] + self.logger.highlight(outputFormat.format(*row)) - + # Building the search filter search_filter = ("(&(|(UserAccountControl:1.2.840.113556.1.4.803:=16777216)" "(UserAccountControl:1.2.840.113556.1.4.803:=524288)" "(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))" "(!(UserAccountControl:1.2.840.113556.1.4.803:=2))" "(!(UserAccountControl:1.2.840.113556.1.4.803:=8192)))") - + attributes = ["sAMAccountName", "pwdLastSet", "userAccountControl", "objectCategory", "msDS-AllowedToActOnBehalfOfOtherIdentity", "msDS-AllowedToDelegateTo"] resp = self.search(search_filter, attributes, 0) - answers = [] self.logger.debug(f"Total of records returned {len(resp):d}") + resp_parse = parse_result_attributes(resp) - for item in resp: - if not isinstance(item, ldapasn1_impacket.SearchResultEntry): - continue - + for item in resp_parse: mustCommit = False sAMAccountName = "" userAccountControl = 0 @@ -1142,50 +1148,50 @@ def printTable(items, header): objectType = "" rightsTo = [] protocolTransition = 0 - + try: - for attribute in item["attributes"]: - if str(attribute["type"]) == "sAMAccountName": - sAMAccountName = str(attribute["vals"][0]) - mustCommit = True - elif str(attribute["type"]) == "userAccountControl": - userAccountControl = str(attribute["vals"][0]) - if int(userAccountControl) & UF_TRUSTED_FOR_DELEGATION: - delegation = "Unconstrained" - rightsTo.append("N/A") - elif int(userAccountControl) & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: - delegation = "Constrained w/ Protocol Transition" - protocolTransition = 1 - elif str(attribute["type"]) == "objectCategory": - objectType = str(attribute["vals"][0]).split("=")[1].split(",")[0] - elif str(attribute["type"]) == "msDS-AllowedToDelegateTo": - if protocolTransition == 0: - delegation = "Constrained" - rightsTo = [processAttributeValue(val) for val in attribute["vals"]] - - # Not an elif as an object could both have RBCD and another type of delegation - if str(attribute["type"]) == "msDS-AllowedToActOnBehalfOfOtherIdentity": - rbcdRights = [] - rbcdObjType = [] - sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=bytes(attribute["vals"][0])) - search_filter = "(&(|" - for ace in sd["Dacl"].aces: - search_filter += "(objectSid=" + ace["Ace"]["Sid"].formatCanonical() + ")" - search_filter += ")(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" - delegUserResp = self.search(search_filter, attributes=["sAMAccountName", "objectCategory"], sizeLimit=999) - - for item2 in delegUserResp: - if not isinstance(item2, ldapasn1_impacket.SearchResultEntry): - continue - rbcdRights.append(str(item2["attributes"][0]["vals"][0])) - rbcdObjType.append(str(item2["attributes"][1]["vals"][0]).split("=")[1].split(",")[0]) - - if mustCommit: - if int(userAccountControl) & UF_ACCOUNTDISABLE: - self.logger.debug(f"Bypassing disabled account {sAMAccountName}") - else: - for rights, objType in zip(rbcdRights, rbcdObjType): - answers.append([rights, objType, "Resource-Based Constrained", sAMAccountName]) + sAMAccountName = item.get("sAMAccountName") + mustCommit = sAMAccountName is not None + + userAccountControl = int(item.get("userAccountControl", 0)) + objectType = item.get("objectCategory") + + if userAccountControl & UF_TRUSTED_FOR_DELEGATION: + delegation = "Unconstrained" + rightsTo.append("N/A") + elif userAccountControl & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: + delegation = "Constrained w/ Protocol Transition" + protocolTransition = 1 + + if item.get("msDS-AllowedToDelegateTo") is not None: + if protocolTransition == 0: + delegation = "Constrained" + rightsTo = item.get("msDS-AllowedToDelegateTo") + + # Not an elif as an object could both have RBCD and another type of delegation + if item.get("msDS-AllowedToActOnBehalfOfOtherIdentity") is not None: + databyte = AttributeValue(item.get("msDS-AllowedToActOnBehalfOfOtherIdentity")) # STR to impacket.ldap.ldapasn1.AttributeValue + rbcdRights = [] + rbcdObjType = [] + sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=bytes(databyte)) + search_filter = "(&(|" + for ace in sd["Dacl"].aces: + search_filter += "(objectSid=" + ace["Ace"]["Sid"].formatCanonical() + ")" + search_filter += ")(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" + delegUserResp = self.search(search_filter, attributes=["sAMAccountName", "objectCategory"], sizeLimit=999) + + for item2 in delegUserResp: + if not isinstance(item2, ldapasn1_impacket.SearchResultEntry): + continue + rbcdRights.append(str(item2["attributes"][0]["vals"][0])) + rbcdObjType.append(str(item2["attributes"][1]["vals"][0]).split("=")[1].split(",")[0]) + + if mustCommit: + if int(userAccountControl) & UF_ACCOUNTDISABLE: + self.logger.debug(f"Bypassing disabled account {sAMAccountName}") + else: + for rights, objType in zip(rbcdRights, rbcdObjType): + answers.append([rights, objType, "Resource-Based Constrained", sAMAccountName]) if delegation in ["Unconstrained", "Constrained", "Constrained w/ Protocol Transition"] and mustCommit: if int(userAccountControl) & UF_ACCOUNTDISABLE: From 5b14c3f999d6c2dc2e79be8ece77113bf4a41b62 Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:39:04 +0300 Subject: [PATCH 07/18] Used parse_result_attributes for parsing RBCD too Signed-off-by: termanix <50464194+termanix@users.noreply.github.com> --- nxc/protocols/ldap.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 794d8a4dc..c6c864b60 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1179,12 +1179,11 @@ def printTable(items, header): search_filter += "(objectSid=" + ace["Ace"]["Sid"].formatCanonical() + ")" search_filter += ")(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" delegUserResp = self.search(search_filter, attributes=["sAMAccountName", "objectCategory"], sizeLimit=999) - - for item2 in delegUserResp: - if not isinstance(item2, ldapasn1_impacket.SearchResultEntry): - continue - rbcdRights.append(str(item2["attributes"][0]["vals"][0])) - rbcdObjType.append(str(item2["attributes"][1]["vals"][0]).split("=")[1].split(",")[0]) + delegUserResp_parse = parse_result_attributes(delegUserResp) + + for rbcd in delegUserResp_parse: + rbcdRights.append(str(rbcd.get("sAMAccountName"))) + rbcdObjType.append(str(rbcd.get("objectCategory"))) if mustCommit: if int(userAccountControl) & UF_ACCOUNTDISABLE: From 6e59002b6fd97f6b6d12bb768007002254a43e9a Mon Sep 17 00:00:00 2001 From: termanix <50464194+termanix@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:48:15 +0300 Subject: [PATCH 08/18] Edit_ldarp_parser --- nxc/parsers/ldap_results.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/nxc/parsers/ldap_results.py b/nxc/parsers/ldap_results.py index 206fad8ba..844343dda 100644 --- a/nxc/parsers/ldap_results.py +++ b/nxc/parsers/ldap_results.py @@ -8,6 +8,15 @@ def parse_result_attributes(ldap_response): continue attribute_map = {} for attribute in entry["attributes"]: - attribute_map[str(attribute["type"])] = str(attribute["vals"][0]) + val_list = [] + for val in attribute["vals"].components: + try: + # Attempt to decode as UTF-8 + decoded_val = val.decode("utf-8") + except (UnicodeDecodeError, AttributeError): + # If it fails, fall back to hexadecimal representation + decoded_val = val.hex() if isinstance(val, bytes) else str(val) + val_list.append(decoded_val) + attribute_map[str(attribute["type"])] = val_list if len(val_list) > 1 else val_list[0] parsed_response.append(attribute_map) - return parsed_response \ No newline at end of file + return parsed_response From c2fe271738218387adea161ec7880b569c5ee6f0 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 18 Oct 2024 20:20:27 -0400 Subject: [PATCH 09/18] Fix ldap result parsing minor code improvements --- nxc/parsers/ldap_results.py | 15 ++++++--------- nxc/protocols/ldap.py | 15 +++++---------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/nxc/parsers/ldap_results.py b/nxc/parsers/ldap_results.py index 9bf77da53..c12be0e10 100644 --- a/nxc/parsers/ldap_results.py +++ b/nxc/parsers/ldap_results.py @@ -1,5 +1,6 @@ from impacket.ldap import ldapasn1 as ldapasn1_impacket + def parse_result_attributes(ldap_response): parsed_response = [] for entry in ldap_response: @@ -12,15 +13,11 @@ def parse_result_attributes(ldap_response): for val in attribute["vals"].components: try: encoding = val.encoding - - print(f"Val: {str(val)}, Type: {type(val)}, Encoding: {encoding}") - print(str(val).encode(encoding).decode("utf-8")) - # Attempt to decode as UTF-8 - decoded_val = val.decode("utf-8") - except (UnicodeDecodeError, AttributeError): - # If it fails, fall back to hexadecimal representation - decoded_val = val.hex() if isinstance(val, bytes) else str(val) - val_list.append(decoded_val) + val_decoded = str(val).encode(encoding).decode("utf-8") + except UnicodeDecodeError: + # If we can't decode the value, we'll just return the bytes + val_decoded = val.__bytes__() + val_list.append(val_decoded) attribute_map[str(attribute["type"])] = val_list if len(val_list) > 1 else val_list[0] parsed_response.append(attribute_map) return parsed_response diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index dd38380e3..e0dcb26da 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1092,12 +1092,7 @@ def find_delegation(self): UF_TRUSTED_FOR_DELEGATION = 0x80000 UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x1000000 UF_ACCOUNTDISABLE = 0x2 - - def processAttributeValue(attribute): - # Extract the payload value from the AttributeValue object - if hasattr(attribute, "payload"): - return str(attribute.payload) - return str(attribute) + SERVER_TRUST_ACCOUNT = 0x2000 def printTable(items, header): colLen = [] @@ -1126,11 +1121,11 @@ def printTable(items, header): self.logger.highlight(outputFormat.format(*row)) # Building the search filter - search_filter = ("(&(|(UserAccountControl:1.2.840.113556.1.4.803:=16777216)" - "(UserAccountControl:1.2.840.113556.1.4.803:=524288)" + search_filter = (f"(&(|(UserAccountControl:1.2.840.113556.1.4.803:={UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION})" + f"(UserAccountControl:1.2.840.113556.1.4.803:={UF_TRUSTED_FOR_DELEGATION})" "(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))" - "(!(UserAccountControl:1.2.840.113556.1.4.803:=2))" - "(!(UserAccountControl:1.2.840.113556.1.4.803:=8192)))") + f"(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE}))" + f"(!(UserAccountControl:1.2.840.113556.1.4.803:={SERVER_TRUST_ACCOUNT})))") attributes = ["sAMAccountName", "pwdLastSet", "userAccountControl", "objectCategory", "msDS-AllowedToActOnBehalfOfOtherIdentity", "msDS-AllowedToDelegateTo"] From e2bec64be2b08a895ae87902deecfc1f14b054b2 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 18 Oct 2024 20:21:28 -0400 Subject: [PATCH 10/18] Hotfix if msDS-AllowedToActOnBehalfOfOtherIdentity has an empty security descriptor --- nxc/protocols/ldap.py | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index e0dcb26da..bcb63ca33 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1127,7 +1127,7 @@ def printTable(items, header): f"(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE}))" f"(!(UserAccountControl:1.2.840.113556.1.4.803:={SERVER_TRUST_ACCOUNT})))") - attributes = ["sAMAccountName", "pwdLastSet", "userAccountControl", "objectCategory", + attributes = ["sAMAccountName", "pwdLastSet", "userAccountControl", "objectCategory", "msDS-AllowedToActOnBehalfOfOtherIdentity", "msDS-AllowedToDelegateTo"] resp = self.search(search_filter, attributes, 0) @@ -1143,7 +1143,7 @@ def printTable(items, header): objectType = "" rightsTo = [] protocolTransition = 0 - + try: sAMAccountName = item.get("sAMAccountName") mustCommit = sAMAccountName is not None @@ -1165,27 +1165,28 @@ def printTable(items, header): # Not an elif as an object could both have RBCD and another type of delegation if item.get("msDS-AllowedToActOnBehalfOfOtherIdentity") is not None: - databyte = AttributeValue(item.get("msDS-AllowedToActOnBehalfOfOtherIdentity")) # STR to impacket.ldap.ldapasn1.AttributeValue + databyte = item.get("msDS-AllowedToActOnBehalfOfOtherIdentity") rbcdRights = [] rbcdObjType = [] sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=bytes(databyte)) - search_filter = "(&(|" - for ace in sd["Dacl"].aces: - search_filter += "(objectSid=" + ace["Ace"]["Sid"].formatCanonical() + ")" - search_filter += ")(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" - delegUserResp = self.search(search_filter, attributes=["sAMAccountName", "objectCategory"], sizeLimit=999) - delegUserResp_parse = parse_result_attributes(delegUserResp) - - for rbcd in delegUserResp_parse: - rbcdRights.append(str(rbcd.get("sAMAccountName"))) - rbcdObjType.append(str(rbcd.get("objectCategory"))) - - if mustCommit: - if int(userAccountControl) & UF_ACCOUNTDISABLE: - self.logger.debug(f"Bypassing disabled account {sAMAccountName}") - else: - for rights, objType in zip(rbcdRights, rbcdObjType): - answers.append([rights, objType, "Resource-Based Constrained", sAMAccountName]) + if len(sd["Dacl"].aces) > 0: + search_filter = "(&(|" + for ace in sd["Dacl"].aces: + search_filter += "(objectSid=" + ace["Ace"]["Sid"].formatCanonical() + ")" + search_filter += ")(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" + delegUserResp = self.search(search_filter, attributes=["sAMAccountName", "objectCategory"], sizeLimit=999) + delegUserResp_parse = parse_result_attributes(delegUserResp) + + for rbcd in delegUserResp_parse: + rbcdRights.append(str(rbcd.get("sAMAccountName"))) + rbcdObjType.append(str(rbcd.get("objectCategory"))) + + if mustCommit: + if int(userAccountControl) & UF_ACCOUNTDISABLE: + self.logger.debug(f"Bypassing disabled account {sAMAccountName}") + else: + for rights, objType in zip(rbcdRights, rbcdObjType): + answers.append([rights, objType, "Resource-Based Constrained", sAMAccountName]) if delegation in ["Unconstrained", "Constrained", "Constrained w/ Protocol Transition"] and mustCommit: if int(userAccountControl) & UF_ACCOUNTDISABLE: @@ -1200,7 +1201,7 @@ def printTable(items, header): printTable(answers, header=["AccountName", "AccountType", "DelegationType", "DelegationRightsTo"]) else: self.logger.fail("No entries found!") - + def trusted_for_delegation(self): # Building the search filter searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=524288)" From 5b48d68afb436d55840b5d862daa65d107e64298 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Fri, 18 Oct 2024 20:27:09 -0400 Subject: [PATCH 11/18] Remove unused import --- nxc/protocols/ldap.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index bcb63ca33..c371179d8 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -29,7 +29,6 @@ from impacket.ldap import ldap as ldap_impacket from impacket.ldap import ldaptypes from impacket.ldap import ldapasn1 as ldapasn1_impacket -from impacket.ldap.ldapasn1 import AttributeValue from impacket.ldap.ldap import LDAPFilterSyntaxError from impacket.smb import SMB_DIALECT from impacket.smbconnection import SMBConnection, SessionError From ef0ca60c39f970f09f4387e048c478921efc1cd9 Mon Sep 17 00:00:00 2001 From: termanix Date: Fri, 8 Nov 2024 01:53:22 -0500 Subject: [PATCH 12/18] mustcommit variable remove --- nxc/protocols/ldap.py | 83 +++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index c371179d8..77027db04 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1135,7 +1135,6 @@ def printTable(items, header): resp_parse = parse_result_attributes(resp) for item in resp_parse: - mustCommit = False sAMAccountName = "" userAccountControl = 0 delegation = "" @@ -1145,53 +1144,53 @@ def printTable(items, header): try: sAMAccountName = item.get("sAMAccountName") - mustCommit = sAMAccountName is not None - - userAccountControl = int(item.get("userAccountControl", 0)) - objectType = item.get("objectCategory") - - if userAccountControl & UF_TRUSTED_FOR_DELEGATION: - delegation = "Unconstrained" - rightsTo.append("N/A") - elif userAccountControl & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: - delegation = "Constrained w/ Protocol Transition" - protocolTransition = 1 - - if item.get("msDS-AllowedToDelegateTo") is not None: - if protocolTransition == 0: - delegation = "Constrained" - rightsTo = item.get("msDS-AllowedToDelegateTo") - - # Not an elif as an object could both have RBCD and another type of delegation - if item.get("msDS-AllowedToActOnBehalfOfOtherIdentity") is not None: - databyte = item.get("msDS-AllowedToActOnBehalfOfOtherIdentity") - rbcdRights = [] - rbcdObjType = [] - sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=bytes(databyte)) - if len(sd["Dacl"].aces) > 0: - search_filter = "(&(|" - for ace in sd["Dacl"].aces: - search_filter += "(objectSid=" + ace["Ace"]["Sid"].formatCanonical() + ")" - search_filter += ")(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" - delegUserResp = self.search(search_filter, attributes=["sAMAccountName", "objectCategory"], sizeLimit=999) - delegUserResp_parse = parse_result_attributes(delegUserResp) - - for rbcd in delegUserResp_parse: - rbcdRights.append(str(rbcd.get("sAMAccountName"))) - rbcdObjType.append(str(rbcd.get("objectCategory"))) - - if mustCommit: + if sAMAccountName: + + userAccountControl = int(item.get("userAccountControl", 0)) + objectType = item.get("objectCategory") + + if userAccountControl & UF_TRUSTED_FOR_DELEGATION: + delegation = "Unconstrained" + rightsTo.append("N/A") + elif userAccountControl & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: + delegation = "Constrained w/ Protocol Transition" + protocolTransition = 1 + + if item.get("msDS-AllowedToDelegateTo") is not None: + if protocolTransition == 0: + delegation = "Constrained" + rightsTo = item.get("msDS-AllowedToDelegateTo") + + # Not an elif as an object could both have RBCD and another type of delegation + if item.get("msDS-AllowedToActOnBehalfOfOtherIdentity") is not None: + databyte = item.get("msDS-AllowedToActOnBehalfOfOtherIdentity") + rbcdRights = [] + rbcdObjType = [] + sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=bytes(databyte)) + if len(sd["Dacl"].aces) > 0: + search_filter = "(&(|" + for ace in sd["Dacl"].aces: + search_filter += "(objectSid=" + ace["Ace"]["Sid"].formatCanonical() + ")" + search_filter += ")(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" + delegUserResp = self.search(search_filter, attributes=["sAMAccountName", "objectCategory"], sizeLimit=999) + delegUserResp_parse = parse_result_attributes(delegUserResp) + + for rbcd in delegUserResp_parse: + rbcdRights.append(str(rbcd.get("sAMAccountName"))) + rbcdObjType.append(str(rbcd.get("objectCategory"))) + + if int(userAccountControl) & UF_ACCOUNTDISABLE: self.logger.debug(f"Bypassing disabled account {sAMAccountName}") else: for rights, objType in zip(rbcdRights, rbcdObjType): answers.append([rights, objType, "Resource-Based Constrained", sAMAccountName]) - if delegation in ["Unconstrained", "Constrained", "Constrained w/ Protocol Transition"] and mustCommit: - if int(userAccountControl) & UF_ACCOUNTDISABLE: - self.logger.debug(f"Bypassing disabled account {sAMAccountName}") - else: - answers.append([sAMAccountName, objectType, delegation, rightsTo]) + if delegation in ["Unconstrained", "Constrained", "Constrained w/ Protocol Transition"]: + if int(userAccountControl) & UF_ACCOUNTDISABLE: + self.logger.debug(f"Bypassing disabled account {sAMAccountName}") + else: + answers.append([sAMAccountName, objectType, delegation, rightsTo]) except Exception as e: self.logger.error(f"Skipping item, cannot process due to error {e}") From a70e3b8c6bb423efc701fd9c95e328c2edd9185a Mon Sep 17 00:00:00 2001 From: termanix Date: Sat, 9 Nov 2024 11:27:04 -0500 Subject: [PATCH 13/18] removed SERVER_TRUST_ACCOUNT for see rbcd to DCs --- nxc/protocols/ldap.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 77027db04..3a996d901 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1091,7 +1091,7 @@ def find_delegation(self): UF_TRUSTED_FOR_DELEGATION = 0x80000 UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x1000000 UF_ACCOUNTDISABLE = 0x2 - SERVER_TRUST_ACCOUNT = 0x2000 + """SERVER_TRUST_ACCOUNT = 0x2000""" def printTable(items, header): colLen = [] @@ -1123,8 +1123,8 @@ def printTable(items, header): search_filter = (f"(&(|(UserAccountControl:1.2.840.113556.1.4.803:={UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION})" f"(UserAccountControl:1.2.840.113556.1.4.803:={UF_TRUSTED_FOR_DELEGATION})" "(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))" - f"(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE}))" - f"(!(UserAccountControl:1.2.840.113556.1.4.803:={SERVER_TRUST_ACCOUNT})))") + f"(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE})))") + # f"(!(UserAccountControl:1.2.840.113556.1.4.803:={SERVER_TRUST_ACCOUNT})))") To listing RBCD to DCs attributes = ["sAMAccountName", "pwdLastSet", "userAccountControl", "objectCategory", "msDS-AllowedToActOnBehalfOfOtherIdentity", "msDS-AllowedToDelegateTo"] @@ -1190,7 +1190,9 @@ def printTable(items, header): if int(userAccountControl) & UF_ACCOUNTDISABLE: self.logger.debug(f"Bypassing disabled account {sAMAccountName}") else: - answers.append([sAMAccountName, objectType, delegation, rightsTo]) + # Check if the entry is invalid, i.e., for "Unconstrained N/A" + if not (delegation == "Unconstrained" and rightsTo == ["N/A"]): + answers.append([sAMAccountName, objectType, delegation, rightsTo]) except Exception as e: self.logger.error(f"Skipping item, cannot process due to error {e}") From 496b002ad21c02b3062ca457bcd07d9abd78f930 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 14 Nov 2024 06:22:38 -0500 Subject: [PATCH 14/18] Remove check for sAMAccountName, there should always be one --- nxc/protocols/ldap.py | 96 +++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 49 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 3a996d901..862a116bc 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1091,7 +1091,7 @@ def find_delegation(self): UF_TRUSTED_FOR_DELEGATION = 0x80000 UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x1000000 UF_ACCOUNTDISABLE = 0x2 - """SERVER_TRUST_ACCOUNT = 0x2000""" + SERVER_TRUST_ACCOUNT = 0x2000 def printTable(items, header): colLen = [] @@ -1124,7 +1124,7 @@ def printTable(items, header): f"(UserAccountControl:1.2.840.113556.1.4.803:={UF_TRUSTED_FOR_DELEGATION})" "(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))" f"(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE})))") - # f"(!(UserAccountControl:1.2.840.113556.1.4.803:={SERVER_TRUST_ACCOUNT})))") To listing RBCD to DCs + # f"(!(UserAccountControl:1.2.840.113556.1.4.803:={SERVER_TRUST_ACCOUNT})))") This would filter out RBCD to DCs attributes = ["sAMAccountName", "pwdLastSet", "userAccountControl", "objectCategory", "msDS-AllowedToActOnBehalfOfOtherIdentity", "msDS-AllowedToDelegateTo"] @@ -1143,56 +1143,54 @@ def printTable(items, header): protocolTransition = 0 try: - sAMAccountName = item.get("sAMAccountName") - if sAMAccountName: - - userAccountControl = int(item.get("userAccountControl", 0)) - objectType = item.get("objectCategory") - - if userAccountControl & UF_TRUSTED_FOR_DELEGATION: - delegation = "Unconstrained" - rightsTo.append("N/A") - elif userAccountControl & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: - delegation = "Constrained w/ Protocol Transition" - protocolTransition = 1 - - if item.get("msDS-AllowedToDelegateTo") is not None: - if protocolTransition == 0: - delegation = "Constrained" - rightsTo = item.get("msDS-AllowedToDelegateTo") - - # Not an elif as an object could both have RBCD and another type of delegation - if item.get("msDS-AllowedToActOnBehalfOfOtherIdentity") is not None: - databyte = item.get("msDS-AllowedToActOnBehalfOfOtherIdentity") - rbcdRights = [] - rbcdObjType = [] - sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=bytes(databyte)) - if len(sd["Dacl"].aces) > 0: - search_filter = "(&(|" - for ace in sd["Dacl"].aces: - search_filter += "(objectSid=" + ace["Ace"]["Sid"].formatCanonical() + ")" - search_filter += ")(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" - delegUserResp = self.search(search_filter, attributes=["sAMAccountName", "objectCategory"], sizeLimit=999) - delegUserResp_parse = parse_result_attributes(delegUserResp) - - for rbcd in delegUserResp_parse: - rbcdRights.append(str(rbcd.get("sAMAccountName"))) - rbcdObjType.append(str(rbcd.get("objectCategory"))) - - - if int(userAccountControl) & UF_ACCOUNTDISABLE: - self.logger.debug(f"Bypassing disabled account {sAMAccountName}") - else: - for rights, objType in zip(rbcdRights, rbcdObjType): - answers.append([rights, objType, "Resource-Based Constrained", sAMAccountName]) - - if delegation in ["Unconstrained", "Constrained", "Constrained w/ Protocol Transition"]: + sAMAccountName = item["sAMAccountName"] + + userAccountControl = int(item["userAccountControl"]) + objectType = item.get("objectCategory") + + if userAccountControl & UF_TRUSTED_FOR_DELEGATION: + delegation = "Unconstrained" + rightsTo.append("N/A") + elif userAccountControl & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: + delegation = "Constrained w/ Protocol Transition" + protocolTransition = 1 + + if item.get("msDS-AllowedToDelegateTo") is not None: + if protocolTransition == 0: + delegation = "Constrained" + rightsTo = item.get("msDS-AllowedToDelegateTo") + + # Not an elif as an object could both have RBCD and another type of delegation + if item.get("msDS-AllowedToActOnBehalfOfOtherIdentity") is not None: + databyte = item.get("msDS-AllowedToActOnBehalfOfOtherIdentity") + rbcdRights = [] + rbcdObjType = [] + sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=bytes(databyte)) + if len(sd["Dacl"].aces) > 0: + search_filter = "(&(|" + for ace in sd["Dacl"].aces: + search_filter += "(objectSid=" + ace["Ace"]["Sid"].formatCanonical() + ")" + search_filter += ")(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" + delegUserResp = self.search(search_filter, attributes=["sAMAccountName", "objectCategory"], sizeLimit=999) + delegUserResp_parse = parse_result_attributes(delegUserResp) + + for rbcd in delegUserResp_parse: + rbcdRights.append(str(rbcd.get("sAMAccountName"))) + rbcdObjType.append(str(rbcd.get("objectCategory"))) + if int(userAccountControl) & UF_ACCOUNTDISABLE: self.logger.debug(f"Bypassing disabled account {sAMAccountName}") else: - # Check if the entry is invalid, i.e., for "Unconstrained N/A" - if not (delegation == "Unconstrained" and rightsTo == ["N/A"]): - answers.append([sAMAccountName, objectType, delegation, rightsTo]) + for rights, objType in zip(rbcdRights, rbcdObjType): + answers.append([rights, objType, "Resource-Based Constrained", sAMAccountName]) + + if delegation in ["Unconstrained", "Constrained", "Constrained w/ Protocol Transition"]: + if int(userAccountControl) & UF_ACCOUNTDISABLE: + self.logger.debug(f"Bypassing disabled account {sAMAccountName}") + else: + # Check if the entry is invalid, i.e., for "Unconstrained N/A" + if not (delegation == "Unconstrained" and rightsTo == ["N/A"]): + answers.append([sAMAccountName, objectType, delegation, rightsTo]) except Exception as e: self.logger.error(f"Skipping item, cannot process due to error {e}") From 0fc09fae53140b2bb3e36365d7378e6d9f50642a Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 14 Nov 2024 06:29:12 -0500 Subject: [PATCH 15/18] Filter only unconstrained delegation on DCs --- nxc/protocols/ldap.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 862a116bc..c7b364672 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1148,7 +1148,8 @@ def printTable(items, header): userAccountControl = int(item["userAccountControl"]) objectType = item.get("objectCategory") - if userAccountControl & UF_TRUSTED_FOR_DELEGATION: + # Filter out DCs, unconstrained delegation to DCs is not a useful information + if userAccountControl & UF_TRUSTED_FOR_DELEGATION and not userAccountControl & SERVER_TRUST_ACCOUNT: delegation = "Unconstrained" rightsTo.append("N/A") elif userAccountControl & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: @@ -1188,9 +1189,7 @@ def printTable(items, header): if int(userAccountControl) & UF_ACCOUNTDISABLE: self.logger.debug(f"Bypassing disabled account {sAMAccountName}") else: - # Check if the entry is invalid, i.e., for "Unconstrained N/A" - if not (delegation == "Unconstrained" and rightsTo == ["N/A"]): - answers.append([sAMAccountName, objectType, delegation, rightsTo]) + answers.append([sAMAccountName, objectType, delegation, rightsTo]) except Exception as e: self.logger.error(f"Skipping item, cannot process due to error {e}") From 573eb600028d628273ef8adab788defa037fab39 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 14 Nov 2024 06:40:07 -0500 Subject: [PATCH 16/18] Small formating changes --- nxc/protocols/ldap.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index c7b364672..b9e5e4b23 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1091,7 +1091,7 @@ def find_delegation(self): UF_TRUSTED_FOR_DELEGATION = 0x80000 UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x1000000 UF_ACCOUNTDISABLE = 0x2 - SERVER_TRUST_ACCOUNT = 0x2000 + UF_SERVER_TRUST_ACCOUNT = 0x2000 def printTable(items, header): colLen = [] @@ -1124,12 +1124,12 @@ def printTable(items, header): f"(UserAccountControl:1.2.840.113556.1.4.803:={UF_TRUSTED_FOR_DELEGATION})" "(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))" f"(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE})))") - # f"(!(UserAccountControl:1.2.840.113556.1.4.803:={SERVER_TRUST_ACCOUNT})))") This would filter out RBCD to DCs + # f"(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_SERVER_TRUST_ACCOUNT})))") This would filter out RBCD to DCs attributes = ["sAMAccountName", "pwdLastSet", "userAccountControl", "objectCategory", "msDS-AllowedToActOnBehalfOfOtherIdentity", "msDS-AllowedToDelegateTo"] - resp = self.search(search_filter, attributes, 0) + resp = self.search(search_filter, attributes) answers = [] self.logger.debug(f"Total of records returned {len(resp):d}") resp_parse = parse_result_attributes(resp) @@ -1149,7 +1149,7 @@ def printTable(items, header): objectType = item.get("objectCategory") # Filter out DCs, unconstrained delegation to DCs is not a useful information - if userAccountControl & UF_TRUSTED_FOR_DELEGATION and not userAccountControl & SERVER_TRUST_ACCOUNT: + if userAccountControl & UF_TRUSTED_FOR_DELEGATION and not userAccountControl & UF_SERVER_TRUST_ACCOUNT: delegation = "Unconstrained" rightsTo.append("N/A") elif userAccountControl & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: @@ -1171,8 +1171,8 @@ def printTable(items, header): search_filter = "(&(|" for ace in sd["Dacl"].aces: search_filter += "(objectSid=" + ace["Ace"]["Sid"].formatCanonical() + ")" - search_filter += ")(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" - delegUserResp = self.search(search_filter, attributes=["sAMAccountName", "objectCategory"], sizeLimit=999) + search_filter += f")(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE})))" + delegUserResp = self.search(search_filter, attributes=["sAMAccountName", "objectCategory"]) delegUserResp_parse = parse_result_attributes(delegUserResp) for rbcd in delegUserResp_parse: From bac7a34285f49a3c069b9017ddbedff7ecf26152 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 14 Nov 2024 06:46:50 -0500 Subject: [PATCH 17/18] Removing disabled account checks, these are already filtered by the ldap query --- nxc/protocols/ldap.py | 12 +++--------- nxc/protocols/ldap/proto_args.py | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index b9e5e4b23..386b55c99 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1179,17 +1179,11 @@ def printTable(items, header): rbcdRights.append(str(rbcd.get("sAMAccountName"))) rbcdObjType.append(str(rbcd.get("objectCategory"))) - if int(userAccountControl) & UF_ACCOUNTDISABLE: - self.logger.debug(f"Bypassing disabled account {sAMAccountName}") - else: - for rights, objType in zip(rbcdRights, rbcdObjType): - answers.append([rights, objType, "Resource-Based Constrained", sAMAccountName]) + for rights, objType in zip(rbcdRights, rbcdObjType): + answers.append([rights, objType, "Resource-Based Constrained", sAMAccountName]) if delegation in ["Unconstrained", "Constrained", "Constrained w/ Protocol Transition"]: - if int(userAccountControl) & UF_ACCOUNTDISABLE: - self.logger.debug(f"Bypassing disabled account {sAMAccountName}") - else: - answers.append([sAMAccountName, objectType, delegation, rightsTo]) + answers.append([sAMAccountName, objectType, delegation, rightsTo]) except Exception as e: self.logger.error(f"Skipping item, cannot process due to error {e}") diff --git a/nxc/protocols/ldap/proto_args.py b/nxc/protocols/ldap/proto_args.py index e97f98458..47314a39c 100644 --- a/nxc/protocols/ldap/proto_args.py +++ b/nxc/protocols/ldap/proto_args.py @@ -17,7 +17,7 @@ def proto_args(parser, parents): vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain", "Options to to play with Kerberos") vgroup.add_argument("--query", nargs=2, help="Query LDAP with a custom filter and attributes") - vgroup.add_argument("--find-delegation", action="store_true", help="Finds delegation relationships within an Active Directory domain.") + vgroup.add_argument("--find-delegation", action="store_true", help="Finds delegation relationships within an Active Directory domain. (Enabled Accounts only)") vgroup.add_argument("--trusted-for-delegation", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION") vgroup.add_argument("--password-not-required", action="store_true", help="Get the list of users with flag PASSWD_NOTREQD") vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1") From f5d5a1b4fea1d4e0b941cc07e983e5d66816570f Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Thu, 14 Nov 2024 14:10:13 -0500 Subject: [PATCH 18/18] Use imported constants instead of redefining --- nxc/protocols/ldap.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 386b55c99..a5401d8d2 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -21,6 +21,7 @@ UF_DONT_REQUIRE_PREAUTH, UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, + UF_SERVER_TRUST_ACCOUNT, ) from impacket.dcerpc.v5.transport import DCERPCTransportFactory from impacket.krb5 import constants @@ -1087,12 +1088,6 @@ def query(self): self.logger.highlight(f"{attr:<20} {vals}") def find_delegation(self): - # Constants for delegation types - UF_TRUSTED_FOR_DELEGATION = 0x80000 - UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x1000000 - UF_ACCOUNTDISABLE = 0x2 - UF_SERVER_TRUST_ACCOUNT = 0x2000 - def printTable(items, header): colLen = []