Skip to content

Commit

Permalink
feat: Added --no-downgrade mode. (#189)
Browse files Browse the repository at this point in the history
* feat: Added `--no-downgrade` mode.
* doc: fixed some typos in README.
* fix: Flipped downgrade logic conditions.

* doc: Updated CHANGELOG

Co-Authored-By: Olivier Bilodeau <obilodeau@gosecure.net>

* doc: Updated ToC.

Co-authored-by: Olivier Bilodeau <obilodeau@gosecure.net>
  • Loading branch information
alxbl and obilodeau committed Mar 16, 2020
1 parent b23d424 commit 08183ad
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 30 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ For a detailed view of what has changed, refer to the {uri-repo}/commits/master[
* Default Docker Compose command now `pyrdp-mitm -h` to avoid confusing crash on `docker-compose up` ({uri-issue}173[#173])
* Documentation updates and fixes ({uri-issue}165[#165], {uri-issue}166[#166], {uri-issue}172[#172])
* Added `--disable-active-clipboard` switch to prevent clipboard request injection
* Added `--no-downgrade` switch to prevent protocol downgrading where possible {uri-issue}189[#189]


=== Bug fixes
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ In August 2019, PyRDP was demo'ed at BlackHat Arsenal ([slides](https://docs.goo
- [Choosing when to start the payload](#choosing-when-to-start-the-payload)
- [Choosing when to resume normal activity](#choosing-when-to-resume-normal-activity)
+ [Other MITM arguments](#other-mitm-arguments)
- [--no-downgrade](#--no-downgrade)
* [Using the PyRDP Player](#using-the-pyrdp-player)
+ [Playing a replay file](#playing-a-replay-file)
+ [Listening for live connections](#listening-for-live-connections)
Expand Down Expand Up @@ -306,6 +307,30 @@ After 5 seconds, input / output is restored back to normal.
#### Other MITM arguments
Run `pyrdp-mitm.py --help` for a full list of arguments.

##### `--no-downgrade`

This argument is useful when running PyRDP in Honeypot scenarios to avoid scanner fingerprinting.
When the switch is enabled, PyRDP will not downgrade unsupported extensions and let the traffic through
transparently. The player will likely not be able to successfully replay video traffic, but the following
supported channels should still be accessible:

- Keystroke recording
- Mouse position updates
- Clipboard access (passively)
- Drive access (passively)

This feature is still a work in progress and some downgrading is currently unavoidable to allow the connection
to be established. The following are currently not affected by this switch and will still be disabled:

- FIPS Encryption
- Non-TLS encryption protocols
- ClientInfo compression
- Virtual Channel compression

**NOTE**: If being able to eventually replay the full session is important, a good solution is to record the raw
RDP traffic using Wireshark and keep the TLS master secrets. Whenever PyRDP adds support for additional extensions,
it would then become possible to extract a valid RDP replay file from the raw network capture.

### Using the PyRDP Player
Use `pyrdp-player.py` to run the player.

Expand Down
2 changes: 2 additions & 0 deletions bin/pyrdp-mitm.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def main():
parser.add_argument("--crawler-match-file", help="File to be used by the crawler to chose what to download when scraping the client shared drives.", default=None)
parser.add_argument("--crawler-ignore-file", help="File to be used by the crawler to chose what folders to avoid when scraping the client shared drives.", default=None)
parser.add_argument("--no-replay", help="Disable replay recording", action="store_true")
parser.add_argument("--no-downgrade", help="Disables downgrading of unsupported extensions. This makes PyRDP harder to fingerprint but might impact the player's ability to replay captured traffic.", action="store_true")

args = parser.parse_args()
outDir = Path(args.output)
Expand Down Expand Up @@ -76,6 +77,7 @@ def main():
config.crawlerMatchFileName = args.crawler_match_file
config.crawlerIgnoreFileName = args.crawler_ignore_file
config.recordReplays = not args.no_replay
config.downgrade = not args.no_downgrade
config.disableActiveClipboardStealing = args.disable_active_clipboard


Expand Down
29 changes: 15 additions & 14 deletions pyrdp/mitm/MCSMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,22 +88,23 @@ def onConnectInitial(self, pdu: MCSConnectInitialPDU):
rdpClientDataPDU.securityData.encryptionMethods &= ~EncryptionMethod.ENCRYPTION_FIPS
rdpClientDataPDU.securityData.extEncryptionMethods &= ~EncryptionMethod.ENCRYPTION_FIPS

# This disables the support for the Graphics pipeline extension, which is a completely different way to
# transfer graphics from server to client. https://msdn.microsoft.com/en-us/library/dn366933.aspx
rdpClientDataPDU.coreData.earlyCapabilityFlags &= ~ClientCapabilityFlag.RNS_UD_CS_SUPPORT_DYNVC_GFX_PROTOCOL
if self.state.config.downgrade:
# This disables the support for the Graphics pipeline extension, which is a completely different way to
# transfer graphics from server to client. https://msdn.microsoft.com/en-us/library/dn366933.aspx
rdpClientDataPDU.coreData.earlyCapabilityFlags &= ~ClientCapabilityFlag.RNS_UD_CS_SUPPORT_DYNVC_GFX_PROTOCOL

# Remove 24bpp and 32bpp support, fall back to 16bpp.
# 2018-12-14: This is only there because there is a bug in the pyrdp player where 24bpp
# decompression in rle.c causes random crashes. If this bug is fixed, we could remove this.
rdpClientDataPDU.coreData.supportedColorDepths &= ~SupportedColorDepth.RNS_UD_32BPP_SUPPORT
rdpClientDataPDU.coreData.supportedColorDepths &= ~SupportedColorDepth.RNS_UD_24BPP_SUPPORT
rdpClientDataPDU.coreData.highColorDepth &= ~HighColorDepth.HIGH_COLOR_24BPP
# Remove 24bpp and 32bpp support, fall back to 16bpp.
# 2018-12-14: This is only there because there is a bug in the pyrdp player where 24bpp
# decompression in rle.c causes random crashes. If this bug is fixed, we could remove this.
rdpClientDataPDU.coreData.supportedColorDepths &= ~SupportedColorDepth.RNS_UD_32BPP_SUPPORT
rdpClientDataPDU.coreData.supportedColorDepths &= ~SupportedColorDepth.RNS_UD_24BPP_SUPPORT
rdpClientDataPDU.coreData.highColorDepth &= ~HighColorDepth.HIGH_COLOR_24BPP

if rdpClientDataPDU.coreData.highColorDepth == 0:
# Means the requested color depth was 24bpp, fallback to 16bpp
rdpClientDataPDU.coreData.highColorDepth |= HighColorDepth.HIGH_COLOR_16BPP
if rdpClientDataPDU.coreData.highColorDepth == 0:
# Means the requested color depth was 24bpp, fallback to 16bpp
rdpClientDataPDU.coreData.highColorDepth |= HighColorDepth.HIGH_COLOR_16BPP

rdpClientDataPDU.coreData.earlyCapabilityFlags &= ~ClientCapabilityFlag.RNS_UD_CS_WANT_32BPP_SESSION
rdpClientDataPDU.coreData.earlyCapabilityFlags &= ~ClientCapabilityFlag.RNS_UD_CS_WANT_32BPP_SESSION

self.recorder.record(rdpClientDataPDU, PlayerPDUType.CLIENT_DATA)

Expand Down Expand Up @@ -267,4 +268,4 @@ def onServerDisconnectProviderUltimatum(self, pdu: MCSDisconnectProviderUltimatu
Forward a server disconnect provider ultimatum to the client.
:param pdu: the disconnect provider ultimatum
"""
self.client.sendPDU(pdu)
self.client.sendPDU(pdu)
4 changes: 2 additions & 2 deletions pyrdp/mitm/RDPMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def __init__(self, mainLogger: SessionLogger, crawlerLogger: SessionLogger, conf
self.statCounter = StatCounter()
"""Class to keep track of connection-related statistics such as # of mouse events, # of output events, etc."""

self.state = RDPMITMState()
self.state = RDPMITMState(config)
"""The MITM state"""

self.client = RDPLayerSet()
Expand Down Expand Up @@ -393,4 +393,4 @@ def enableForwarding():
waitForPayload,
enableForwarding
])
sequencer.run()
sequencer.run()
21 changes: 11 additions & 10 deletions pyrdp/mitm/SlowPathMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,22 @@ def onConfirmActive(self, pdu: ConfirmActivePDU):
:param pdu: the confirm active PDU
"""

# Force RDP server to send bitmap events instead of order events.
pdu.parsedCapabilitySets[CapabilityType.CAPSTYPE_ORDER].orderFlags = OrderFlag.NEGOTIATEORDERSUPPORT | OrderFlag.ZEROBOUNDSDELTASSUPPORT
pdu.parsedCapabilitySets[CapabilityType.CAPSTYPE_ORDER].orderSupport = b"\x00" * 32

# Disable virtual channel compression
if CapabilityType.CAPSTYPE_VIRTUALCHANNEL in pdu.parsedCapabilitySets:
pdu.parsedCapabilitySets[CapabilityType.CAPSTYPE_VIRTUALCHANNEL].flags = VirtualChannelCompressionFlag.VCCAPS_NO_COMPR

# Override the bitmap cache capability set with null values.
if CapabilityType.CAPSTYPE_BITMAPCACHE in pdu.parsedCapabilitySets:
pdu.parsedCapabilitySets[CapabilityType.CAPSTYPE_BITMAPCACHE] = Capability(CapabilityType.CAPSTYPE_BITMAPCACHE, b"\x00" * 36)
if self.state.config.downgrade:
# Force RDP server to send bitmap events instead of order events.
pdu.parsedCapabilitySets[CapabilityType.CAPSTYPE_ORDER].orderFlags = OrderFlag.NEGOTIATEORDERSUPPORT | OrderFlag.ZEROBOUNDSDELTASSUPPORT
pdu.parsedCapabilitySets[CapabilityType.CAPSTYPE_ORDER].orderSupport = b"\x00" * 32

# Override the bitmap cache capability set with null values.
if CapabilityType.CAPSTYPE_BITMAPCACHE in pdu.parsedCapabilitySets:
pdu.parsedCapabilitySets[CapabilityType.CAPSTYPE_BITMAPCACHE] = Capability(CapabilityType.CAPSTYPE_BITMAPCACHE, b"\x00" * 36)

# Disable surface commands
if CapabilityType.CAPSETTYPE_SURFACE_COMMANDS in pdu.parsedCapabilitySets:
pdu.parsedCapabilitySets[CapabilityType.CAPSETTYPE_SURFACE_COMMANDS].cmdFlags = 0
# Disable surface commands
if CapabilityType.CAPSETTYPE_SURFACE_COMMANDS in pdu.parsedCapabilitySets:
pdu.parsedCapabilitySets[CapabilityType.CAPSETTYPE_SURFACE_COMMANDS].cmdFlags = 0

def onDemandActive(self, pdu: DemandActivePDU):
"""
Expand Down
5 changes: 4 additions & 1 deletion pyrdp/mitm/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ def __init__(self):
self.recordReplays: bool = True
"""Whether replays should be recorded or not"""

self.downgrade: bool = True
"""Whether to actively downgrade unsupported extensions."""

self.payload: str = ""
"""Payload to send automatically upon connection"""

Expand Down Expand Up @@ -77,4 +80,4 @@ def fileDir(self) -> Path:
"""
Get the directory for intercepted files.
"""
return self.outDir / "files"
return self.outDir / "files"
11 changes: 8 additions & 3 deletions pyrdp/mitm/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,21 @@
from pyrdp.parser import createFastPathParser
from pyrdp.pdu import ClientChannelDefinition
from pyrdp.security import RC4CrypterProxy, SecuritySettings
from pyrdp.mitm.config import MITMConfig


class RDPMITMState:
"""
State object for the RDP MITM. This is for data that needs to be shared across components.
"""

def __init__(self):
def __init__(self, config: MITMConfig):
self.requestedProtocols: Optional[NegotiationProtocols] = None
"""The original request protocols"""

self.config = config
"""The MITM configuration."""

self.useTLS = False
"""Whether the connection uses TLS or not"""

Expand Down Expand Up @@ -93,5 +97,6 @@ def createFastPathLayer(self, mode: ParserMode) -> FastPathLayer:
:param mode: the mode of the layer (client or server)
"""

parser = createFastPathParser(self.useTLS, self.securitySettings.encryptionMethod, self.crypters[mode], mode)
return FastPathLayer(parser)
parser = createFastPathParser(
self.useTLS, self.securitySettings.encryptionMethod, self.crypters[mode], mode)
return FastPathLayer(parser)

0 comments on commit 08183ad

Please sign in to comment.