Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Log NTLMSSP hashes when using NLA #307

Merged
merged 3 commits into from
Mar 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ For a detailed view of what has changed, refer to the {uri-repo}/commits/master[

*MITM*

* Added NTLMSSP hash logging when NLA is used with NTLM as the authentication protocol. Hashes are logged to `pyrdp_output/logs/ntlmssp.log` (see {uri-issue}307[#307])
* PyRDP will log the value of the `HOST_IP` variable on start if it exists. You can set it to the IP address of the host running PyRDP. This is mostly helpful when you're using PyRDP in Docker and you want the IP of the Docker host in the logs.
* Added detection function for BlueKeep scans / exploit attempts. PyRDP will log the attempt and shut down the connection. The JSON log has an exploitInfo attribute as well as a parserInfo attribute to help investigate what happened.
* Added better logging for parser errors. PyRDP will now log which parser crashed and the data that was fed to that parser to make it crash. This makes it easier to investigate bugs and exploits. In JSON logs, this information shows up in the parserInfo attribute.
Expand Down
3 changes: 2 additions & 1 deletion pyrdp/enum/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2018-2020 GoSecure Inc.
# Copyright (C) 2018-2021 GoSecure Inc.
# Licensed under the GPLv3 or later.
#
# flake8: noqa
Expand All @@ -9,6 +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.player import MouseButton, PlayerPDUType
from pyrdp.enum.rdp import *
from pyrdp.enum.orders import DrawingOrderControlFlags
Expand Down
13 changes: 13 additions & 0 deletions pyrdp/enum/ntlmssp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2021 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

from enum import IntEnum


class NTLMSSPMessageType(IntEnum):
NEGOTIATE_MESSAGE = 1
CHALLENGE_MESSAGE = 2
AUTHENTICATE_MESSAGE = 3
19 changes: 17 additions & 2 deletions pyrdp/logging/formatters.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2018 GoSecure Inc.
# Copyright (C) 2018-2021 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

Expand Down Expand Up @@ -74,6 +74,21 @@ class SSLSecretFormatter(logging.Formatter):
def __init__(self):
super().__init__()

def format(self, record: logging.LogRecord):
def format(self, record: logging.LogRecord) -> str:
return "CLIENT_RANDOM {} {}".format(binascii.hexlify(record.msg).decode(),
binascii.hexlify(record.args[0]).decode())


class NTLMSSPHashFormatter(logging.Formatter):
"""
Custom formatter used to log NTLMSSP hashes.
"""

@staticmethod
def formatNTLMSSPHash(user: str, domain: str, serverChallenge: bytes, proof: bytes, response: bytes) -> str:
return f"{user}::{domain}:{serverChallenge.hex()}:{proof.hex()}:{response.hex()}"

def format(self, record: logging.LogRecord) -> str:
user = record.msg
domain, serverChallenge, proof, response = record.args[0 : 4]
return NTLMSSPHashFormatter.formatNTLMSSPHash(user, domain, serverChallenge, proof, response)
3 changes: 2 additions & 1 deletion pyrdp/logging/log.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2018-2020 GoSecure Inc.
# Copyright (C) 2018-2021 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

Expand All @@ -19,6 +19,7 @@ class LOGGER_NAMES:
MITM_CONNECTIONS = f"{MITM}.connections"
PLAYER = f"{PYRDP}.player"
PLAYER_UI = f"{PLAYER}.ui"
NTLMSSP = f"ntlmssp"

# Independent logger
CRAWLER = "crawler"
Expand Down
26 changes: 9 additions & 17 deletions pyrdp/mitm/RDPMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,40 +17,31 @@
from pyrdp.layer import ClipboardLayer, DeviceRedirectionLayer, LayerChainItem, RawLayer, \
VirtualChannelLayer
from pyrdp.logging import RC4LoggingObserver
from pyrdp.logging.StatCounter import StatCounter
from pyrdp.logging.adapters import SessionLogger
from pyrdp.logging.observers import FastPathLogger, LayerLogger, MCSLogger, SecurityLogger, \
SlowPathLogger, X224Logger
from pyrdp.logging.StatCounter import StatCounter
from pyrdp.mcs import MCSClientChannel, MCSServerChannel
from pyrdp.mitm.AttackerMITM import AttackerMITM
from pyrdp.mitm.ClipboardMITM import ActiveClipboardStealer, PassiveClipboardStealer
from pyrdp.mitm.config import MITMConfig
from pyrdp.mitm.DeviceRedirectionMITM import DeviceRedirectionMITM
from pyrdp.mitm.FastPathMITM import FastPathMITM
from pyrdp.mitm.FileCrawlerMITM import FileCrawlerMITM
from pyrdp.mitm.layerset import RDPLayerSet
from pyrdp.mitm.MCSMITM import MCSMITM
from pyrdp.mitm.MITMRecorder import MITMRecorder
from pyrdp.mitm.PlayerLayerSet import TwistedPlayerLayerSet
from pyrdp.mitm.SecurityMITM import SecurityMITM
from pyrdp.mitm.SlowPathMITM import SlowPathMITM
from pyrdp.mitm.state import RDPMITMState
from pyrdp.mitm.TCPMITM import TCPMITM
from pyrdp.mitm.VirtualChannelMITM import VirtualChannelMITM
from pyrdp.mitm.X224MITM import X224MITM
from pyrdp.mitm.config import MITMConfig
from pyrdp.mitm.layerset import RDPLayerSet
from pyrdp.mitm.state import RDPMITMState
from pyrdp.recording import FileLayer, RecordingFastPathObserver, RecordingSlowPathObserver, \
Recorder

from pyrdp.layer.segmentation import SegmentationObserver


class PacketForwarder(SegmentationObserver):
"""Handles unknown segmentation packets by forwarding them transparently."""
def __init__(self, sink):
self._forwarder = sink

def onUnknownHeader(self, header, data: bytes):
self._forwarder.sendBytes(data)
from pyrdp.security import NTLMSSPState
from pyrdp.security.nla import NLAHandler


class RDPMITM:
Expand Down Expand Up @@ -237,8 +228,9 @@ def doClientTls(self):
self.onTlsReady = None

# Add unknown packet handlers.
self.client.segmentation.addObserver(PacketForwarder(self.server.tcp))
self.server.segmentation.addObserver(PacketForwarder(self.client.tcp))
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")))

def startTLS(self, onTlsReady: typing.Callable[[], None]):
"""
Expand Down
16 changes: 15 additions & 1 deletion pyrdp/mitm/mitm.default.ini
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ handlers = ssl, ssl_console
# Always log SSL master secrets.
level = DEBUG

[logs:loggers:ntlmssp]
handlers = ntlmssp
# Always log NTLMSSP hashes.
level = DEBUG


# -----------------------------------------------------------------
# WARNING:
Expand Down Expand Up @@ -161,6 +166,11 @@ class = logging.StreamHandler
stream = ext://sys.stderr
formatter = ssl

[logs:handlers:ntlmssp]
class = logging.FileHandler
filename = ${vars:output_dir}/${vars:log_dir}/ntlmssp.log
formatter = ntlmssp

# -----------------------------------------------------------------
# Formatters
# -----------------------------------------------------------------
Expand Down Expand Up @@ -194,4 +204,8 @@ sessionID = GLOBAL

# Raw SSL Secret formatting for dumping secrets
[logs:formatters:ssl]
() = pyrdp.logging.formatters.SSLSecretFormatter
() = pyrdp.logging.formatters.SSLSecretFormatter

# NTLMSSP hash formatting for dumping NTLM hashes
[logs:formatters:ntlmssp]
() = pyrdp.logging.formatters.NTLMSSPHashFormatter
3 changes: 2 additions & 1 deletion pyrdp/parser/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2018 GoSecure Inc.
# Copyright (C) 2018-2021 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

Expand All @@ -17,6 +17,7 @@
from pyrdp.parser.rdp.input import SlowPathInputParser
from pyrdp.parser.rdp.licensing import LicensingParser
from pyrdp.parser.rdp.negotiation import NegotiationRequestParser, NegotiationResponseParser
from pyrdp.parser.rdp.ntlmssp import NTLMSSPParser
from pyrdp.parser.rdp.pointer import PointerEventParser
from pyrdp.parser.rdp.security import BasicSecurityParser, FIPSSecurityParser, SignedSecurityParser
from pyrdp.parser.rdp.slowpath import SlowPathParser
Expand Down
88 changes: 88 additions & 0 deletions pyrdp/parser/rdp/ntlmssp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2021 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

from io import BytesIO
from typing import Callable, Dict

from pyrdp.core import Uint16LE, Uint32LE
from pyrdp.parser.parser import Parser
from pyrdp.pdu import NTLMSSPChallengePDU, NTLMSSPAuthenticatePDU, NTLMSSPNegotiatePDU, NTLMSSPPDU


class NTLMSSPParser(Parser):
"""
Parser for NLA/NTLMSSP
TODO: Add other fields to PDUs if necessary
TODO: Implement write if necessary
"""

def __init__(self):
self.handlers: Dict[int, Callable[[bytes, BytesIO], NTLMSSPPDU]] = {
1: self.parseNTLMSSPNegotiate,
2: self.parseNTLMSSPChallenge,
3: self.parseNTLMSSPAuthenticate
}

def findMessage(self, data: bytes) -> int:
"""
Check if data contains an NTLMSSP message.
Returns the offset in data of the start of the message or -1 otherwise.
"""
return data.find(b"NTLMSSP\x00")

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

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

def parseField(self, data: bytes, fields: bytes) -> bytes:
length = Uint16LE.unpack(fields[0: 2])
offset = Uint32LE.unpack(fields[4: 8])

if length != 0:
return data[offset : offset + length]
else:
return b""

def parseNTLMSSPNegotiate(self, data: bytes, stream: BytesIO) -> NTLMSSPNegotiatePDU:
return NTLMSSPNegotiatePDU()

def parseNTLMSSPChallenge(self, data: bytes, stream: BytesIO) -> NTLMSSPChallengePDU:
targetNameFields = stream.read(8)
negotiateFlags = stream.read(4)
serverChallenge = stream.read(8)
reserved = stream.read(8)
targetInfoFields = stream.read(8)
version = stream.read(8)

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

def parseNTLMSSPAuthenticate(self, data: bytes, stream: BytesIO) -> NTLMSSPAuthenticatePDU:
lmChallengeResponseFields = stream.read(8)
ntChallengeResponseFields = stream.read(8)
domainNameFields = stream.read(8)
userNameFields = stream.read(8)
workstationFields = stream.read(8)
encryptedRandomSessionKeyFields = stream.read(8)
negotiationFlags = stream.read(4)
version = stream.read(8)
mic = stream.read(16)

lmChallengeResponse = self.parseField(data, lmChallengeResponseFields)
ntChallengeResponse = self.parseField(data, ntChallengeResponseFields)
domain = self.parseField(data, domainNameFields).decode("utf-16le")
user = self.parseField(data, userNameFields).decode("utf-16le")
workstation = self.parseField(data, workstationFields)
encryptedRandomSessionKey = self.parseField(data, encryptedRandomSessionKeyFields)

proof = ntChallengeResponse[: 16]
response = ntChallengeResponse[16 :]

return NTLMSSPAuthenticatePDU(user, domain, proof, response)
3 changes: 2 additions & 1 deletion pyrdp/pdu/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2018-2020 GoSecure Inc.
# Copyright (C) 2018-2021 GoSecure Inc.
# Licensed under the GPLv3 or later.
#
# flake8: noqa
Expand Down Expand Up @@ -35,6 +35,7 @@
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.pointer import Point, PointerCacheEvent, PointerColorEvent, PointerEvent, PointerNewEvent, \
PointerPositionEvent, PointerSystemEvent
from pyrdp.pdu.rdp.security import SecurityExchangePDU, SecurityPDU
Expand Down
34 changes: 34 additions & 0 deletions pyrdp/pdu/rdp/ntlmssp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2021 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

from pyrdp.enum import NTLMSSPMessageType
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)
self.serverChallenge = serverChallenge


class NTLMSSPAuthenticatePDU(NTLMSSPPDU):
def __init__(self, user: str, domain: str, proof: bytes, response: bytes):
super().__init__(NTLMSSPMessageType.AUTHENTICATE_MESSAGE)
self.user = user
self.domain = domain
self.proof = proof
self.response = response
5 changes: 3 additions & 2 deletions pyrdp/security/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2018 GoSecure Inc.
# Copyright (C) 2018-2021 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

from pyrdp.security.crypto import RC4Crypter
from pyrdp.security.ntlmssp import NTLMSSPState
from pyrdp.security.rc4proxy import RC4CrypterProxy
from pyrdp.security.settings import SecuritySettings, SecuritySettingsObserver
from pyrdp.security.settings import SecuritySettings, SecuritySettingsObserver
Loading