diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index e93414681..33b184512 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -24,6 +24,7 @@ For a detailed view of what has changed, refer to the {uri-repo}/commits/master[ * 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] +* Added `--no-files` switch to prevent extracting transferred files {uri-issue}195[#195] === Bug fixes diff --git a/bin/pyrdp-mitm.py b/bin/pyrdp-mitm.py index 2c42e6a19..fc61f5056 100755 --- a/bin/pyrdp-mitm.py +++ b/bin/pyrdp-mitm.py @@ -50,6 +50,7 @@ def main(): 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") + parser.add_argument("--no-files", help="Do not extract files transferred between the client and server.", action="store_true") args = parser.parse_args() outDir = Path(args.output) @@ -78,6 +79,7 @@ def main(): config.crawlerIgnoreFileName = args.crawler_ignore_file config.recordReplays = not args.no_replay config.downgrade = not args.no_downgrade + config.extractFiles = not args.no_files config.disableActiveClipboardStealing = args.disable_active_clipboard diff --git a/pyrdp/mitm/DeviceRedirectionMITM.py b/pyrdp/mitm/DeviceRedirectionMITM.py index 61cad4820..bdd961f9e 100644 --- a/pyrdp/mitm/DeviceRedirectionMITM.py +++ b/pyrdp/mitm/DeviceRedirectionMITM.py @@ -1,6 +1,6 @@ # # This file is part of the PyRDP project. -# Copyright (C) 2019 GoSecure Inc. +# Copyright (C) 2019-2020 GoSecure Inc. # Licensed under the GPLv3 or later. # @@ -56,7 +56,7 @@ class DeviceRedirectionMITM(Subject): def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLayer, log: LoggerAdapter, - config: MITMConfig, statCounter: StatCounter, state: RDPMITMState): + statCounter: StatCounter, state: RDPMITMState): """ :param client: device redirection layer for the client side :param server: device redirection layer for the server side @@ -70,7 +70,6 @@ def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLaye self.state = state self.log = log self.statCounter = statCounter - self.config = config self.currentIORequests: Dict[int, DeviceIORequestPDU] = {} self.openedFiles: Dict[int, FileProxy] = {} self.openedMappings: Dict[int, FileMapping] = {} @@ -78,11 +77,14 @@ def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLaye self.fileMapPath = self.config.outDir / "mapping.json" self.forgedRequests: Dict[int, DeviceRedirectionMITM.ForgedRequest] = {} - self.responseHandlers: Dict[MajorFunction, callable] = { - MajorFunction.IRP_MJ_CREATE: self.handleCreateResponse, - MajorFunction.IRP_MJ_READ: self.handleReadResponse, - MajorFunction.IRP_MJ_CLOSE: self.handleCloseResponse, - } + if self.config.extractFiles: + self.responseHandlers: Dict[MajorFunction, callable] = { + MajorFunction.IRP_MJ_CREATE: self.handleCreateResponse, + MajorFunction.IRP_MJ_READ: self.handleReadResponse, + MajorFunction.IRP_MJ_CLOSE: self.handleCloseResponse, + } + else: + self.responseHandlers = {} self.client.createObserver( onPDUReceived=self.onClientPDUReceived, @@ -102,6 +104,10 @@ def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLaye except json.JSONDecodeError: self.log.error("Failed to decode file mapping, overwriting previous file") + @property + def config(self): + return self.state.config + def saveMapping(self): """ Save the file mapping to a file in JSON format. @@ -220,7 +226,6 @@ def handleCreateResponse(self, request: DeviceCreateRequestPDU, response: Device onFileClosed = lambda _: self.log.debug("Closing file %(path)s", {"path": mapping.localPath}) ) - def handleReadResponse(self, request: DeviceReadRequestPDU, response: DeviceReadResponsePDU): """ Write the data that was read at the appropriate offset in the file proxy. @@ -286,7 +291,6 @@ def handleCloseResponse(self, request: DeviceCloseRequestPDU, _: DeviceCloseResp self.saveMapping() - def handleClientLogin(self): """ Handle events that should be triggered when a client logs in. @@ -302,7 +306,6 @@ def handleClientLogin(self): 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 @@ -316,7 +319,6 @@ def findNextRequestID(self) -> int: return completionID - def sendForgedFileRead(self, deviceID: int, path: str) -> int: """ Send a forged requests for reading a file. Returns a request ID that can be used by the caller to keep track of @@ -325,6 +327,10 @@ def sendForgedFileRead(self, deviceID: int, path: str) -> int: :param path: path of the file to download. The path should use '\' instead of '/' to separate directories. """ + if not self.config.extractFiles: + self.log.info('Ignored attempt to forge file reads because file extraction is disabled.') + return + self.statCounter.increment(STAT.DEVICE_REDIRECTION_FORGED_FILE_READ) completionID = self.findNextRequestID() @@ -334,7 +340,6 @@ def sendForgedFileRead(self, deviceID: int, path: str) -> int: request.send() return completionID - def sendForgedDirectoryListing(self, deviceID: int, path: str) -> int: """ Send a forged directory listing request. Returns a request ID that can be used by the caller to keep track of which @@ -345,6 +350,10 @@ def sendForgedDirectoryListing(self, deviceID: int, path: str) -> int: \Documents\* """ + if not self.config.extractFiles: + self.log.info('Ignored attempt to forge directory listing because file extraction is disabled.') + return + self.statCounter.increment(STAT.DEVICE_REDIRECTION_FORGED_DIRECTORY_LISTING) completionID = self.findNextRequestID() @@ -578,4 +587,4 @@ def handleDirectoryListingComplete(self, _: DeviceQueryDirectoryResponsePDU): self.mitm.observer.onDirectoryListingComplete(self.deviceID, self.requestID) # Once we're done, we can close the file. - self.sendCloseRequest() \ No newline at end of file + self.sendCloseRequest() diff --git a/pyrdp/mitm/RDPMITM.py b/pyrdp/mitm/RDPMITM.py index 3c0e4cf48..41fa411db 100644 --- a/pyrdp/mitm/RDPMITM.py +++ b/pyrdp/mitm/RDPMITM.py @@ -313,7 +313,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, self.statCounter, self.state) + deviceRedirection = DeviceRedirectionMITM(clientLayer, serverLayer, self.getLog(MCSChannelName.DEVICE_REDIRECTION), self.statCounter, self.state) self.channelMITMs[client.channelID] = deviceRedirection if self.config.enableCrawler: diff --git a/pyrdp/mitm/config.py b/pyrdp/mitm/config.py index e363216e0..23d1f53d1 100644 --- a/pyrdp/mitm/config.py +++ b/pyrdp/mitm/config.py @@ -41,6 +41,9 @@ def __init__(self): self.outDir: Path = None """The output directory""" + self.extractFiles: bool = True + """Whether to extract file transferred between the client and server.""" + self.recordReplays: bool = True """Whether replays should be recorded or not"""