import datetime
import re
from winacl.dtyp.sid import SID
from winacl.dtyp.guid import GUID
from winacl.dtyp.security_descriptor import SECURITY_DESCRIPTOR
from msldap import logger
from msldap.protocol.messages import Attribute, Change, PartialAttribute
MSLDAP_DT_WIN_EPOCH = datetime.datetime(1601, 1, 1)
#this regex and the function that uses it is from ldap3
time_format = re.compile(
r'''
^
(?P<Year>[0-9]{4})
(?P<Month>0[1-9]|1[0-2])
(?P<Day>0[1-9]|[12][0-9]|3[01])
(?P<Hour>[01][0-9]|2[0-3])
(?:
(?P<Minute>[0-5][0-9])
(?P<Second>[0-5][0-9]|60)?
)?
(?:
[.,]
(?P<Fraction>[0-9]+)
)?
(?:
Z
|
(?:
(?P<Offset>[+-])
(?P<OffHour>[01][0-9]|2[0-3])
(?P<OffMinute>[0-5][0-9])?
)
)
$
''',
re.VERBOSE
)
def x2sd(x):
return SECURITY_DESCRIPTOR.from_bytes(x[0])
def x2sid(x):
return str(SID.from_bytes(x[0]))
def list_x2sid(x):
t = []
for s in x:
t.append(str(SID.from_bytes(s)))
return t
def list_bool_one(x):
x = x[0].decode()
if x == 'FALSE':
return False
return True
def x2guid(x):
return str(GUID.from_bytes(x[0]))
def list_str(x):
return [e.decode() for e in x ]
def list_str_enc(x):
return [e.encode() for e in x ]
def list_int(x):
return [int(e) for e in x ]
def list_int_enc(x):
return [str(e).encode() for e in x ]
def list_int_one(x):
return int(x[0])
def list_int_one_enc(x):
return [str(x[0]).encode()]
def list_str_one(x):
return x[0].decode()
def list_str_one_enc(x):
return [x[0].encode()]
def list_str_one_utf16le_enc(x):
return [x[0].encode('utf-16-le')]
def list_bytes_one(x):
return x[0]
def list_bytes_one_enc(x):
return x
def bytes2timedelta(x):
return int2timedelta([int.from_bytes(x[0], byteorder='little', signed=True)])
def int2timedelta(x):
x = int(x[0])
if x == '-9223372036854775808':
return datetime.timedelta.max
return datetime.timedelta(microseconds=(x / 10.))
def int2dt(x):
x = int(x[0])
if x == 9223372036854775807:
return datetime.datetime.max.replace(tzinfo=datetime.timezone.utc)
us = x / 10.
return (MSLDAP_DT_WIN_EPOCH + datetime.timedelta(microseconds=us)).replace(tzinfo=datetime.timezone.utc)
def ts2dt(x):
try:
x = x[0].decode()
match = time_format.fullmatch(x)
if match is None:
return x
matches = match.groupdict()
offset = datetime.timedelta(
hours=int(matches['OffHour'] or 0),
minutes=int(matches['OffMinute'] or 0)
)
if matches['Offset'] == '-':
offset *= -1
# Python does not support leap second in datetime (!)
if matches['Second'] == '60':
matches['Second'] = '59'
# According to RFC, fraction may be applied to an Hour/Minute (!)
fraction = float('0.' + (matches['Fraction'] or '0'))
if matches['Minute'] is None:
fraction *= 60
minute = int(fraction)
fraction -= minute
else:
minute = int(matches['Minute'])
if matches['Second'] is None:
fraction *= 60
second = int(fraction)
fraction -= second
else:
second = int(matches['Second'])
microseconds = int(fraction * 1000000)
return datetime.datetime(
int(matches['Year']),
int(matches['Month']),
int(matches['Day']),
int(matches['Hour']),
minute,
second,
microseconds,
datetime.timezone(offset),
)
except Exception: # exceptions should be investigated, anyway the formatter return the raw_value
pass
return x
def list_ts2dt(x):
t = []
for a in x:
t.append(ts2dt((a, None)))
return t
def list_ts2dt_one(x):
return ts2dt(x[0])
LDAP_ATTRIBUTE_TYPES = {
'supportedCapabilities' : list_str,
'serverName' : list_str_one,
'ldapServiceName': list_str_one,
'dnsHostName' : list_str_one,
'supportedSASLMechanisms' : list_str,
'supportedLDAPPolicies' : list_str,
'supportedLDAPVersion' : list_int,
'supportedControl' : list_str,
'rootDomainNamingContext' : list_str_one,
'configurationNamingContext' : list_str_one,
'schemaIDGUID' : x2guid,
'lDAPDisplayName' : list_str_one,
'schemaNamingContext' : list_str_one,
'defaultNamingContext' : list_str_one,
'adminDescription' : list_str_one,
'adminDisplayName' : list_str_one,
'namingContexts' : list_str,
'dsServiceName' : list_str_one,
'subschemaSubentry' : list_str_one,
'distinguishedName' : list_str_one,
'objectCategory' : list_str_one,
'userPrincipalName' : list_str_one,
'sAMAccountType' : list_int_one,
'sAMAccountName' : list_str_one,
'logonCount' : list_int_one,
'accountExpires' : int2dt,
'objectSid' : x2sid,
'primaryGroupID' : list_int_one,
'pwdLastSet' : int2dt,
'lastLogon' : int2dt,
'lastLogoff' : int2dt,
'lastLogonTimestamp' : int2dt,
'badPasswordTime' : int2dt,
'countryCode' : list_int_one,
'codePage' : list_int_one,
'badPwdCount' : list_int_one,
'userAccountControl' : list_int_one,
'objectGUID' : x2guid,
'name' : list_str_one,
'displayName' : list_str_one,
'whenChanged' : ts2dt,
'whenCreated' : ts2dt,
'distinguishedName' : list_str_one,
'givenName' : list_str_one,
'cn' : list_str_one,
'objectClass' : list_str,
'dc' : list_str_one,
'msDS-Behavior-Version' : list_int_one,
'masteredBy' : list_str_one,
'systemFlags' : list_int_one,
'rIDManagerReference' : list_str_one,
'auditingPolicy' : list_bytes_one,
'uASCompat' : list_int_one,
'serverState' : list_int_one,
'nextRid' : list_int_one,
'minPwdLength' : list_int_one,
'minPwdAge' : int2timedelta,
'lockoutThreshold' : list_int_one,
'lockOutObservationWindow' : list_int_one,
'lockoutDuration' : list_int_one,
'forceLogoff' : int2timedelta,
'creationTime' : int2dt,
'maxPwdAge' : int2timedelta,
'pwdHistoryLength' : list_int_one,
'pwdProperties' : list_int_one,
'uSNChanged' : list_int_one,
'uSNCreated' : list_int_one,
'instanceType' : list_int_one,
'memberOf' : list_str,
'description' : list_str_one,
'servicePrincipalName' : list_str,
'sn' : list_str_one,
'initials' : list_str_one,
#'nTSecurityDescriptor' : x2sd,
'nTSecurityDescriptor' : list_bytes_one,
'tokenGroups' : list_x2sid,
'localPolicyFlags' : list_int_one,
'msDS-SupportedEncryptionTypes' : list_int_one,
'isCriticalSystemObject' : list_bool_one,
'dNSHostName' : list_str_one,
'operatingSystemVersion' : list_str_one,
'operatingSystem' : list_str_one,
'ou' : list_str_one,
'showInAdvancedViewOnly' : list_bool_one,
'gPLink' : list_str_one,
'gPCFileSysPath' : list_str_one,
'flags' : list_int_one,
'versionNumber' : list_int_one,
'gPCFunctionalityVersion' : list_int_one,
'gPCMachineExtensionNames' : list_str,
'gPCUserExtensionNames' : list_str,
'groupType' : list_int_one,
'member' : list_str,
'adminCount' : list_int_one,
'msDS-AllowedToDelegateTo' : list_str,
'dSCorePropagationData' : ts2dt,
'trustDirection' : list_int_one,
'trustType' : list_int_one,
'trustAttributes' : list_int_one,
'flatName' : list_str_one,
'trustPosixOffset' : list_int_one,
'trustPartner' : list_str_one,
'securityIdentifier' : list_bytes_one,
'versionNumber' : list_int_one,
'unicodePwd' : list_str_one,
'ms-Mcs-AdmPwd' : list_str_one,
'msDS-AllowedToActOnBehalfOfOtherIdentity' : list_bytes_one,
'cACertificate': list_bytes_one,
'certificateTemplates': list_str,
'cACertificateDN': list_str_one,
'msPKI-Enrollment-Servers': list_str, # it's actually a 5-part multi string split by '\n'
'revision' : list_int_one,
'pKIKeyUsage' : list_bytes_one,
'pKIDefaultKeySpec' : list_str_one,
'pKIMaxIssuingDepth' : list_int_one,
'pKICriticalExtensions' : list_str_one,
'pKIExpirationPeriod' : bytes2timedelta,
'pKIOverlapPeriod' : bytes2timedelta,
'pKIExtendedKeyUsage' : list_str,
'msPKI-RA-Signature' : list_int_one,
'msPKI-Enrollment-Flag' : list_int_one,
'msPKI-Private-Key-Flag' : list_int_one,
'msPKI-Certificate-Name-Flag' : list_int_one,
'msPKI-Minimal-Key-Size' : list_int_one,
'msPKI-Template-Schema-Version': list_int_one,
'msPKI-Template-Minor-Revision': list_int_one,
'msPKI-Cert-Template-OID' : list_str_one,
'msPKI-Certificate-Application-Policy' : list_str,
'msPKI-RA-Application-Policies' : list_str, #I'm guessing here
}
LDAP_ATTRIBUTE_TYPES_ENC = {
'objectClass' : list_str_enc,
'sn' : list_str_one_enc,
'gidNumber' : list_int_one_enc,
'unicodePwd' : list_str_one_utf16le_enc,
'lockoutTime' : list_int_one_enc,
'sAMAccountName' : list_str_one_enc,
'userAccountControl' : list_int_one_enc,
'displayName' : list_str_one_enc,
'userPrincipalName' : list_str_one_enc,
'servicePrincipalName' : list_str_enc,
'msds-additionaldnshostname' : list_str_enc,
'gPCMachineExtensionNames' : list_str_enc,
'gPCUserExtensionNames' : list_str_enc,
'versionNumber' : list_int_one_enc,
'member' : list_str_enc,
'msDS-AllowedToActOnBehalfOfOtherIdentity' : list_bytes_one_enc,
'nTSecurityDescriptor' : list_bytes_one_enc,
'msPKI-Certificate-Name-Flag' : list_int_one_enc
}
def encode_attributes(x):
"""converts a dict to attributelist"""
res = []
for k in x:
if k not in LDAP_ATTRIBUTE_TYPES_ENC:
raise Exception('Unknown conversion type for key "%s"' % k)
res.append(Attribute({
'type' : k.encode(),
'attributes' : LDAP_ATTRIBUTE_TYPES_ENC[k](x[k])
}))
return res
def convert_attributes(x):
t = {}
for e in x:
#print(e)
k = e['type'].decode()
#print('k: %s' % k)
if k in LDAP_ATTRIBUTE_TYPES:
t[k] = LDAP_ATTRIBUTE_TYPES[k](e['attributes'])
else:
logger.debug('Unknown type! %s data: %s' % (k, e['attributes']))
t[k] = e['attributes']
return t
def convert_result(x):
#print(x)
#import traceback
#traceback.print_stack()
return {
'objectName' : x['objectName'].decode(),
'attributes' : convert_attributes(x['attributes'])
}
def encode_changes(x):
res = []
for k in x:
if k not in LDAP_ATTRIBUTE_TYPES_ENC:
raise Exception('Unknown conversion type for key "%s"' % k)
for mod, value in x[k]:
res.append(Change({
'operation' : mod,
'modification' : PartialAttribute({
'type' : k.encode(),
'attributes' : LDAP_ATTRIBUTE_TYPES_ENC[k](value)
})
}))
return res