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

feat: Added --no-downgrade mode. #189

Merged
merged 5 commits into from
Mar 16, 2020
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 @@ -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`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of this new section the ToC will need to be regenerated. They used an online tool to do so. It kind of sucks that this is a manual step to be honest. Migrating this file to AsciiDoc would eliminate that problem. That said, just updating the ToC would be fine for now.


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.

alxbl marked this conversation as resolved.
Show resolved Hide resolved
### 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)