Skip to content

Commit 0da04dd

Browse files
authored
Merge pull request Pennyw0rth#381 from termanix/FindDelegation
New LDAP Flag Find Delegation
2 parents 2235050 + f5d5a1b commit 0da04dd

3 files changed

Lines changed: 116 additions & 3 deletions

File tree

nxc/parsers/ldap_results.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from impacket.ldap import ldapasn1 as ldapasn1_impacket
22

3+
34
def parse_result_attributes(ldap_response):
45
parsed_response = []
56
for entry in ldap_response:
@@ -8,7 +9,15 @@ def parse_result_attributes(ldap_response):
89
continue
910
attribute_map = {}
1011
for attribute in entry["attributes"]:
11-
val = [str(val).encode(val.encoding).decode("utf-8") for val in attribute["vals"].components]
12-
attribute_map[str(attribute["type"])] = val if len(val) > 1 else val[0]
12+
val_list = []
13+
for val in attribute["vals"].components:
14+
try:
15+
encoding = val.encoding
16+
val_decoded = str(val).encode(encoding).decode("utf-8")
17+
except UnicodeDecodeError:
18+
# If we can't decode the value, we'll just return the bytes
19+
val_decoded = val.__bytes__()
20+
val_list.append(val_decoded)
21+
attribute_map[str(attribute["type"])] = val_list if len(val_list) > 1 else val_list[0]
1322
parsed_response.append(attribute_map)
14-
return parsed_response
23+
return parsed_response

nxc/protocols/ldap.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
UF_DONT_REQUIRE_PREAUTH,
2222
UF_TRUSTED_FOR_DELEGATION,
2323
UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION,
24+
UF_SERVER_TRUST_ACCOUNT,
2425
)
2526
from impacket.dcerpc.v5.transport import DCERPCTransportFactory
2627
from impacket.krb5 import constants
2728
from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError
2829
from impacket.krb5.types import Principal, KerberosException
2930
from impacket.ldap import ldap as ldap_impacket
31+
from impacket.ldap import ldaptypes
3032
from impacket.ldap import ldapasn1 as ldapasn1_impacket
3133
from impacket.ldap.ldap import LDAPFilterSyntaxError
3234
from impacket.smb import SMB_DIALECT
@@ -1084,6 +1086,107 @@ def query(self):
10841086
vals = vals.replace("SetOf: ", "")
10851087
self.logger.highlight(f"{attr:<20} {vals}")
10861088

1089+
def find_delegation(self):
1090+
def printTable(items, header):
1091+
colLen = []
1092+
1093+
# Calculating maximum lenght before parsing CN.
1094+
for i, col in enumerate(header):
1095+
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)
1096+
colLen.append(max(rowMaxLen, len(col)))
1097+
1098+
# Create the format string for each row
1099+
outputFormat = " ".join([f"{{{num}:{width}s}}" for num, width in enumerate(colLen)])
1100+
1101+
# Print header
1102+
self.logger.highlight(outputFormat.format(*header))
1103+
self.logger.highlight(" ".join(["-" * itemLen for itemLen in colLen]))
1104+
1105+
# Print rows
1106+
for row in items:
1107+
# Get first CN value.
1108+
if "CN=" in row[1]:
1109+
row[1] = row[1].split(",")[0].split("CN=")[-1]
1110+
1111+
# Added join for DelegationRightsTo
1112+
row[3] = ", ".join(str(x) for x in row[3]) if isinstance(row[3], list) else row[3]
1113+
1114+
self.logger.highlight(outputFormat.format(*row))
1115+
1116+
# Building the search filter
1117+
search_filter = (f"(&(|(UserAccountControl:1.2.840.113556.1.4.803:={UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION})"
1118+
f"(UserAccountControl:1.2.840.113556.1.4.803:={UF_TRUSTED_FOR_DELEGATION})"
1119+
"(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))"
1120+
f"(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE})))")
1121+
# f"(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_SERVER_TRUST_ACCOUNT})))") This would filter out RBCD to DCs
1122+
1123+
attributes = ["sAMAccountName", "pwdLastSet", "userAccountControl", "objectCategory",
1124+
"msDS-AllowedToActOnBehalfOfOtherIdentity", "msDS-AllowedToDelegateTo"]
1125+
1126+
resp = self.search(search_filter, attributes)
1127+
answers = []
1128+
self.logger.debug(f"Total of records returned {len(resp):d}")
1129+
resp_parse = parse_result_attributes(resp)
1130+
1131+
for item in resp_parse:
1132+
sAMAccountName = ""
1133+
userAccountControl = 0
1134+
delegation = ""
1135+
objectType = ""
1136+
rightsTo = []
1137+
protocolTransition = 0
1138+
1139+
try:
1140+
sAMAccountName = item["sAMAccountName"]
1141+
1142+
userAccountControl = int(item["userAccountControl"])
1143+
objectType = item.get("objectCategory")
1144+
1145+
# Filter out DCs, unconstrained delegation to DCs is not a useful information
1146+
if userAccountControl & UF_TRUSTED_FOR_DELEGATION and not userAccountControl & UF_SERVER_TRUST_ACCOUNT:
1147+
delegation = "Unconstrained"
1148+
rightsTo.append("N/A")
1149+
elif userAccountControl & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION:
1150+
delegation = "Constrained w/ Protocol Transition"
1151+
protocolTransition = 1
1152+
1153+
if item.get("msDS-AllowedToDelegateTo") is not None:
1154+
if protocolTransition == 0:
1155+
delegation = "Constrained"
1156+
rightsTo = item.get("msDS-AllowedToDelegateTo")
1157+
1158+
# Not an elif as an object could both have RBCD and another type of delegation
1159+
if item.get("msDS-AllowedToActOnBehalfOfOtherIdentity") is not None:
1160+
databyte = item.get("msDS-AllowedToActOnBehalfOfOtherIdentity")
1161+
rbcdRights = []
1162+
rbcdObjType = []
1163+
sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=bytes(databyte))
1164+
if len(sd["Dacl"].aces) > 0:
1165+
search_filter = "(&(|"
1166+
for ace in sd["Dacl"].aces:
1167+
search_filter += "(objectSid=" + ace["Ace"]["Sid"].formatCanonical() + ")"
1168+
search_filter += f")(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE})))"
1169+
delegUserResp = self.search(search_filter, attributes=["sAMAccountName", "objectCategory"])
1170+
delegUserResp_parse = parse_result_attributes(delegUserResp)
1171+
1172+
for rbcd in delegUserResp_parse:
1173+
rbcdRights.append(str(rbcd.get("sAMAccountName")))
1174+
rbcdObjType.append(str(rbcd.get("objectCategory")))
1175+
1176+
for rights, objType in zip(rbcdRights, rbcdObjType):
1177+
answers.append([rights, objType, "Resource-Based Constrained", sAMAccountName])
1178+
1179+
if delegation in ["Unconstrained", "Constrained", "Constrained w/ Protocol Transition"]:
1180+
answers.append([sAMAccountName, objectType, delegation, rightsTo])
1181+
1182+
except Exception as e:
1183+
self.logger.error(f"Skipping item, cannot process due to error {e}")
1184+
1185+
if answers:
1186+
printTable(answers, header=["AccountName", "AccountType", "DelegationType", "DelegationRightsTo"])
1187+
else:
1188+
self.logger.fail("No entries found!")
1189+
10871190
def trusted_for_delegation(self):
10881191
# Building the search filter
10891192
searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=524288)"

nxc/protocols/ldap/proto_args.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def proto_args(parser, parents):
1717

1818
vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain", "Options to to play with Kerberos")
1919
vgroup.add_argument("--query", nargs=2, help="Query LDAP with a custom filter and attributes")
20+
vgroup.add_argument("--find-delegation", action="store_true", help="Finds delegation relationships within an Active Directory domain. (Enabled Accounts only)")
2021
vgroup.add_argument("--trusted-for-delegation", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION")
2122
vgroup.add_argument("--password-not-required", action="store_true", help="Get the list of users with flag PASSWD_NOTREQD")
2223
vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1")

0 commit comments

Comments
 (0)