Skip to content

Commit

Permalink
Added support of msDS-AllowedToActOnBehalfOfOtherIdentity, pwdLastSet…
Browse files Browse the repository at this point in the history
… and status on user_to_secretsdump.py
  • Loading branch information
wilfried committed Sep 27, 2023
1 parent 9b11856 commit 60a36c3
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 8 deletions.
34 changes: 29 additions & 5 deletions ntdissector/ntds/ntds.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
from binascii import hexlify, unhexlify
from base64 import b64encode
from ntdissector.utils.crypto import PEK_LIST, format_asn1_to_pem
from ntdissector.utils.sddl import parse_ntSecurityDescriptor
from ntdissector.utils import NTDS_SID, GUID, fileTimeToDateTime, formatDateTime, json_dumps
from ntdissector.utils.constants import (
SAM_ACCOUNT_TYPE,
USER_ACCOUNT_CONTROL,
UUID_FIELDS,
DATETIME_FIELDS,
FILETIME_FIELDS,
Expand Down Expand Up @@ -396,6 +398,15 @@ def __formatSamAccountType(self, obj: dict, as_string: bool = False) -> None:
res.append(t)
obj["sAMAccountType"] = " | ".join(res) if as_string else res

def __formatUserAccountControl(self, obj: dict, as_string: bool = False) -> None:
res = list()
if "userAccountControl" in obj:
for t, v in USER_ACCOUNT_CONTROL.items():
if v & int(obj["userAccountControl"]):
res.append(t)
obj["userAccountControl"] = " | ".join(res) if as_string else res


def __formatObjectClass(self, obj: dict, as_string: bool = False) -> None:
if "objectClass" in obj:
res = list()
Expand Down Expand Up @@ -544,6 +555,14 @@ def __formatKeyCredentialLink(self, obj: dict) -> None:
# redundant, unset attr this maybe?
obj["msDS-KeyCredentialLink-BL"] = tmp

def __formatAllowedToActOnBehalfOfOtherIdentity(self, obj: dict) -> None:
if "msDS-AllowedToActOnBehalfOfOtherIdentity" in obj.keys():
sd = parse_ntSecurityDescriptor(bytes.fromhex(obj["msDS-AllowedToActOnBehalfOfOtherIdentity"]))
sids = list()
for ace in sd["DACL"]["ACEs"]:
sids.append(ace.get("SID", ""))
obj["msDS-AllowedToActOnBehalfOfOtherIdentity"] = sids

def __formatCertificates(self, obj: dict) -> None:
if "cACertificate" in obj:
if isinstance(obj["cACertificate"], list):
Expand Down Expand Up @@ -582,18 +601,22 @@ def __formatLAPSv2(self, obj: dict) -> None:
obj[f"{fn}_"] = f"!ERROR! {e}"

def __formatSecurityDescriptor(self, obj: dict) -> None:
if "nTSecurityDescriptor" in obj:
obj["nTSecurityDescriptor"] = self.securityDescriptors.get(
str(int.from_bytes(bytes.fromhex(obj["nTSecurityDescriptor"]), byteorder="little")),
"!ERROR!", # re-run with dryRun opt to rebuild cache
)
sd_fields = ["nTSecurityDescriptor", "msDS-AllowedToActOnBehalfOfOtherIdentity"]
for sdf in sd_fields:
if sdf in obj:
obj[sdf] = self.securityDescriptors.get(
str(int.from_bytes(bytes.fromhex(obj[sdf]), byteorder="little")),
"!ERROR!", # re-run with dryRun opt to rebuild cache
)


def __formatFields(self, obj: dict) -> None:
self.__formatSID(obj)
self.__formatSecrets(obj)
self.__formatSupplementalCredentialsInfo(obj)
self.__formatTimestamps(obj)
self.__formatSamAccountType(obj, as_string=True)
self.__formatUserAccountControl(obj, as_string=True)
self.__formatObjectClass(obj)
self.__formatUID(obj)
self.__formatLinks(obj)
Expand All @@ -604,6 +627,7 @@ def __formatFields(self, obj: dict) -> None:
self.__formatLAPS(obj)
self.__formatLAPSv2(obj)
self.__formatSecurityDescriptor(obj)
self.__formatAllowedToActOnBehalfOfOtherIdentity(obj)

def __getObjectClass(self, record: Record) -> str or list:
try:
Expand Down
9 changes: 7 additions & 2 deletions ntdissector/tools/user_to_secretsdump.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@ def main():
else:
username = j.get("sAMAccountName")
if "cn" in j.keys() and "unicodePwd" in j.keys():
print("%s:%s:%s:%s:::" % (username, rid, j.get("dBCSPwd", "aad3b435b51404eeaad3b435b51404ee"), j["unicodePwd"]))

if "pwdLastSet" in j.keys():
if "ACCOUNTDISABLE" not in j['userAccountControl']:
status = "Enabled"
else:
status = "Disabled"
print("%s:%s:%s:%s::: (pwdLastSet=%s) (status=%s)" % (username, rid, j.get("dBCSPwd", "aad3b435b51404eeaad3b435b51404ee"), j["unicodePwd"], j['pwdLastSet'], status))

if "cn" in j.keys() and "ntPwdHistory" in j.keys():
i = 0
for h in j["ntPwdHistory"]:
Expand Down
26 changes: 26 additions & 0 deletions ntdissector/utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"Attribute-Name-DN": "ATTb49", # Distinguished-Name
"msDS-IntId": "ATTj591540", # for specific attribute ids (exchange...)
"sAMAccountType": "ATTj590126",
"userAccountControl": "ATTj589832",
# "name": "ATTm3",
"Governs-ID": "ATTc131094", # for Class-Schema records
"Object-Class": "ATTc0",
Expand All @@ -29,6 +30,31 @@
"SAM_ACCOUNT_TYPE_MAX": 0x7FFFFFFF,
}

USER_ACCOUNT_CONTROL = {
"SCRIPT": 0x0001,
"ACCOUNTDISABLE": 0x0002,
"HOMEDIR_REQUIRED": 0x0008,
"LOCKOUT": 0x0010,
"PASSWD_NOTREQD": 0x0020,
"PASSWD_CANT_CHANGE": 0x0040,
"ENCRYPTED_TEXT_PWD_ALLOWED": 0x0080,
"TEMP_DUPLICATE_ACCOUNT": 0x0100,
"NORMAL_ACCOUNT": 0x0200,
"INTERDOMAIN_TRUST_ACCOUNT": 0x0800,
"WORKSTATION_TRUST_ACCOUNT": 0x1000,
"SERVER_TRUST_ACCOUNT": 0x2000,
"DONT_EXPIRE_PASSWORD": 0x10000,
"MNS_LOGON_ACCOUNT": 0x20000,
"SMARTCARD_REQUIRED": 0x40000,
"TRUSTED_FOR_DELEGATION": 0x80000,
"NOT_DELEGATED": 0x100000,
"USE_DES_KEY_ONLY": 0x200000,
"DONT_REQ_PREAUTH": 0x400000,
"PASSWORD_EXPIRED": 0x800000,
"TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000,
"PARTIAL_SECRETS_ACCOUNT": 0x04000000,
}

UUID_FIELDS = ["objectGUID", "currentValue", "msFVE-RecoveryGuid", "msFVE-VolumeGuid"]

DATETIME_FIELDS = ["dSCorePropagationData", "whenChanged", "whenCreated"]
Expand Down
206 changes: 206 additions & 0 deletions ntdissector/utils/sddl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
#!/usr/bin/env python3

"""
A module used to handle binary ntSecurityDescriptor from Active Directory LDAP.
"""

from struct import unpack

from ldap3.protocol.formatters.formatters import format_sid, format_uuid_le

SDDLTypeFlags = {
'Self Relative' : 0b1000000000000000,
'RM Control Valid' : 0b0100000000000000,
'SACL Protected' : 0b0010000000000000,
'DACL Protected' : 0b0001000000000000,
'SACL Auto Inherit' : 0b0000100000000000,
'DACL Auto Inherit' : 0b0000010000000000,
'SACL Auto Inherit Required' : 0b0000001000000000,
'DACL Auto Inherit Required' : 0b0000000100000000,
'Server Security' : 0b0000000010000000,
'DACL Trusted' : 0b0000000001000000,
'SACL Defaulted' : 0b0000000000100000,
'SACL Present' : 0b0000000000010000,
'DACL Defaulted' : 0b0000000000001000,
'DACL Present' : 0b0000000000000100,
'Group Defaulted' : 0b0000000000000010,
'Owner Defaulted' : 0b0000000000000001,
}

SID_SIZE = 28


def parse_ntSecurityDescriptor(input_buffer):
""" Parses a ntSecurityDescriptor.
"""
out = dict()
fields = ('Revision', 'Raw Type', 'Offset to owner SID', 'Offset to group SID', 'Offset to SACL', 'Offset to DACL')

for k, v in zip(fields, unpack('<HHIIII', input_buffer[:20])):
out[k] = v

out['Type'] = parse_sddl_type(out['Raw Type'])

for x in ('Owner', 'Group'):
offset = out['Offset to %s SID' % (x.lower())]
out['%s SID' % x] = format_sid(input_buffer[offset:offset + SID_SIZE])

if out['Type']['SACL Present']:
out['SACL'] = parse_acl(input_buffer[out['Offset to SACL']:])

if out['Type']['DACL Present']:
out['DACL'] = parse_acl(input_buffer[out['Offset to DACL']:])

return out


def resolve_flags(bfr, flags):
"""
Helper to resolve flag values and names.
Arguments:
#bfr:integer
The buffer containing flags.
#flags:dict
The dictionary of flag names and values.
"""
return {k: v & bfr != 0 for k, v in flags.items()}


def parse_sddl_type(typeflags):
"""
Parses SDDL Type flags.
"""
return resolve_flags(typeflags, SDDLTypeFlags)


def parse_acl(input_buffer):
"""
Parses ACL from SDDL.
Returns a list of ACEs.
"""
out = dict()
fields = ('Revision', 'Size', 'Num ACEs')

for k, v in zip(fields, unpack('<HHI', input_buffer[:8])):
out[k] = v

out['ACEs'] = parse_aces(input_buffer[8:8 + out['Size']], out['Num ACEs'])
return out


def parse_aces(input_buffer, count):
"""
Parses the list of ACEs.
"""
out = []
while len(out) < count:
ace = dict()
fields = ('Raw Type', 'Raw Flags', 'Size', 'Raw Access Required')
for k, v in zip(fields, unpack('<BBHI', input_buffer[:8])):
ace[k] = v

ace['Type'] = parse_sddl_dacl_ace_type(ace['Raw Type'])

ace['Access Required'] = parse_ace_access(ace['Raw Access Required'])

offset = 8

if ace['Type'].endswith('Object'):
fields = ('Raw Object Flags', )
for k, v in zip(fields, unpack('<I', input_buffer[8:12])):
ace[k] = v
ace['Object Flags'] = parse_ace_object_flags(ace['Raw Object Flags'])

offset = 12
if ace['Object Flags']['Object Type Present']:
ace['GUID'] = format_uuid_le(input_buffer[offset:offset + 16])
offset += 16
if ace['Object Flags']['Inherited Object Type Present']:
ace['Inherited GUID'] = format_uuid_le(input_buffer[offset:offset + 16])
offset += 16

ace['SID'] = format_sid(input_buffer[offset:ace['Size']])

ace['SID'] = format_sid(input_buffer[offset:ace['Size']])

input_buffer = input_buffer[ace['Size']:]

out.append(ace)
return out


ACEAccessFlags = {
'Generic Read' : 0b10000000000000000000000000000000,
'Generic Write' : 0b01000000000000000000000000000000,
'Generic Execute' : 0b00100000000000000000000000000000,
'Generic All' : 0b00010000000000000000000000000000,
'Maximum Allowed' : 0b00000010000000000000000000000000,
'Access SACL' : 0b00000000100000000000000000000000,
'Synchronise' : 0b00000000000100000000000000000000,
'Write Owner' : 0b00000000000010000000000000000000,
'Write DAC' : 0b00000000000001000000000000000000,
'Read Control' : 0b00000000000000100000000000000000,
'Delete' : 0b00000000000000010000000000000000,
'Ads Control Access' : 0b00000000000000000000000100000000,
'Ads List Object' : 0b00000000000000000000000010000000,
'Ads Delete Tree' : 0b00000000000000000000000001000000,
'Ads Write Prop' : 0b00000000000000000000000000100000,
'Ads Read Prop' : 0b00000000000000000000000000010000,
'Ads Self Write' : 0b00000000000000000000000000001000,
'Ads List' : 0b00000000000000000000000000000100,
'Ads Delete Child' : 0b00000000000000000000000000000010,
'Ads Create Child' : 0b00000000000000000000000000000001
}


def parse_ace_access(input_buffer):
"""
Parses access flags in an ACE.
"""
return resolve_flags(input_buffer, ACEAccessFlags)


ACEObjectFlags = {
'Object Type Present' : 0b00000000000000000000000000000010,
'Inherited Object Type Present' : 0b00000000000000000000000000000001
}


def parse_ace_object_flags(input_buffer):
"""
Parses flags in an ACE containing an object.
"""
return resolve_flags(input_buffer, ACEObjectFlags)


ACEType = {
0x00: 'Access Allowed',
0x01: 'Access Denied',
0x02: 'System Audit',
0x03: 'System Alarm',
0x04: 'Access Allowed Compound',
0x05: 'Access Allowed Object',
0x06: 'Access Denied Object',
0x07: 'System Audit Object',
0x08: 'System Alarm Object',
0x09: 'Access Allowed Callback',
0x0A: 'Access Denied Callback',
0x0B: 'Access Allowed Callback Object',
0x0C: 'Access Denied Callback Object',
0x0D: 'System Audit Callback',
0x0E: 'System Alarm Callback',
0x0F: 'System Audit Callback Object',
0x10: 'System Alarm Callback Object',
0x11: 'System Mandatory Label',
0x12: 'System Resource Attribute',
0x13: 'System Scoped Policy ID'
}


def parse_sddl_dacl_ace_type(ace_type):
"""
Parses the type of an ACE.
"""
return ACEType[ace_type]
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ python_dateutil==2.8.2
pyasn1-modules>=0.3.0
six
termcolor
tqdm>=4.65
tqdm>=4.65
ldap3

0 comments on commit 60a36c3

Please sign in to comment.