From dbb65e2b459cc1f0a5bc1c397fa527c139d10a0f Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Thu, 29 Jul 2021 09:04:10 -0400 Subject: [PATCH 1/4] Removed warnings by using typing.Tuple --- pyrdp/core/ssl.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyrdp/core/ssl.py b/pyrdp/core/ssl.py index 817211525..9efaee822 100644 --- a/pyrdp/core/ssl.py +++ b/pyrdp/core/ssl.py @@ -1,12 +1,13 @@ # # Copyright (c) 2014-2020 Sylvain Peyrefitte -# Copyright (c) 2020 GoSecure Inc. +# Copyright (c) 2020-2021 GoSecure Inc. # # This file is part of the PyRDP project. # # Licensed under the GPLv3 or later. # +from typing import Tuple from os import path import OpenSSL @@ -65,7 +66,7 @@ def __init__(self, cachedir, log): self._root = cachedir self.log = log - def clone(self, cert: OpenSSL.crypto.X509) -> (OpenSSL.crypto.PKey, OpenSSL.crypto.X509): + def clone(self, cert: OpenSSL.crypto.X509) -> Tuple[OpenSSL.crypto.PKey, OpenSSL.crypto.X509]: """Clone the provided certificate.""" # Generate a private key for the server. @@ -84,7 +85,7 @@ def clone(self, cert: OpenSSL.crypto.X509) -> (OpenSSL.crypto.PKey, OpenSSL.cryp return key, cert - def lookup(self, cert: OpenSSL.crypto.X509) -> (str, str): + def lookup(self, cert: OpenSSL.crypto.X509) -> Tuple[str, str]: subject = cert.get_subject() parts = dict(subject.get_components()) commonName = parts[b'CN'].decode() From 3656e9a9cdd5aac064442b10e0f0428fbf905b26 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Thu, 29 Jul 2021 09:04:51 -0400 Subject: [PATCH 2/4] Fixed missing variables from JSON SSL logs --- pyrdp/core/ssl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyrdp/core/ssl.py b/pyrdp/core/ssl.py index 9efaee822..ce443d063 100644 --- a/pyrdp/core/ssl.py +++ b/pyrdp/core/ssl.py @@ -92,7 +92,7 @@ def lookup(self, cert: OpenSSL.crypto.X509) -> Tuple[str, str]: base = str(self._root / commonName) if path.exists(base + '.pem'): - self.log.info('Using cached certificate for %s', commonName) + self.log.info('Using cached certificate for %(commonName)s', {'commonName': commonName}) # Recover cache entry from disk. privKey = base + '.pem' @@ -110,6 +110,6 @@ def lookup(self, cert: OpenSSL.crypto.X509) -> Tuple[str, str]: with open(privKey, "wb") as f: f.write(OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, priv)) - self.log.info('Cloned server certificate to %s', certFile) + self.log.info('Cloned server certificate to %(certFile)s', {'certFile': certFile}) return privKey, certFile From 4d5a7c35862b7b4865fb9deb8922e48528d1d07b Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Thu, 29 Jul 2021 09:08:52 -0400 Subject: [PATCH 3/4] Add clientIp to all MITM loggers and output it in all JSON messages --- pyrdp/logging/adapters.py | 10 ++++++++-- pyrdp/logging/formatters.py | 5 +++++ pyrdp/mitm/RDPMITM.py | 18 ++++++++++++++++++ pyrdp/mitm/TCPMITM.py | 3 +++ pyrdp/mitm/X224MITM.py | 3 +++ pyrdp/mitm/state.py | 3 +++ 6 files changed, 40 insertions(+), 2 deletions(-) diff --git a/pyrdp/logging/adapters.py b/pyrdp/logging/adapters.py index 71e3fc24c..8c50e5269 100644 --- a/pyrdp/logging/adapters.py +++ b/pyrdp/logging/adapters.py @@ -1,6 +1,6 @@ # # This file is part of the PyRDP project. -# Copyright (C) 2019 GoSecure Inc. +# Copyright (C) 2019, 2021 GoSecure Inc. # Licensed under the GPLv3 or later. # @@ -32,4 +32,10 @@ def createChild(self, childName: str, sessionID: str = None) -> 'SessionLogger': sessionID = self.extra["sessionID"] logger = logging.getLogger(f"{self.name}.{childName}") - return SessionLogger(logger, sessionID) \ No newline at end of file + sessionLogger = SessionLogger(logger, sessionID) + + # passdown clientIp if present: useful in all JSON log messages + if 'clientIp' in self.extra: + sessionLogger.extra['clientIp'] = self.extra['clientIp'] + + return sessionLogger \ No newline at end of file diff --git a/pyrdp/logging/formatters.py b/pyrdp/logging/formatters.py index 752040c1d..654aa1cc2 100644 --- a/pyrdp/logging/formatters.py +++ b/pyrdp/logging/formatters.py @@ -60,6 +60,11 @@ def format(self, record: logging.LogRecord) -> str: "sessionID": record.sessionID }) + if hasattr(record, "clientIp"): + data.update({ + "clientIp": record.clientIp + }) + if isinstance(record.args, dict): data.update(record.args) diff --git a/pyrdp/mitm/RDPMITM.py b/pyrdp/mitm/RDPMITM.py index 4143b8bb2..04ec699bc 100644 --- a/pyrdp/mitm/RDPMITM.py +++ b/pyrdp/mitm/RDPMITM.py @@ -190,6 +190,7 @@ async def connectToServer(self): Coroutine that connects to the target RDP server and the attacker. Connection to the attacker side has a 1 second timeout to avoid hanging the connection. """ + self.addClientIpToLoggers(self.state.clientIp) serverFactory = AwaitableClientFactory(self.server.tcp) if self.config.transparent: @@ -453,3 +454,20 @@ def ensureOutDir(self): self.config.replayDir.mkdir(exist_ok=True) self.config.fileDir.mkdir(exist_ok=True) self.config.certDir.mkdir(exist_ok=True) + + def addClientIpToLoggers(self, clientIp: str): + """ + Add the client IP address to all relevant loggers. + """ + self.log.extra['clientIp'] = self.state.clientIp + self.clientLog.extra['clientIp'] = self.state.clientIp + self.serverLog.extra['clientIp'] = self.state.clientIp + self.attackerLog.extra['clientIp'] = self.state.clientIp + self.rc4Log.extra['clientIp'] = self.state.clientIp + + self.x224.log.extra['clientIp'] = self.state.clientIp + self.mcs.log.extra['clientIp'] = self.state.clientIp + self.slowPath.log.extra['clientIp'] = self.state.clientIp + + if self.certs: + self.certs.log.extra['clientIp'] = self.state.clientIp \ No newline at end of file diff --git a/pyrdp/mitm/TCPMITM.py b/pyrdp/mitm/TCPMITM.py index f061f12e8..dccec31d4 100644 --- a/pyrdp/mitm/TCPMITM.py +++ b/pyrdp/mitm/TCPMITM.py @@ -82,6 +82,8 @@ def onClientConnection(self): ip = self.client.transport.client[0] port = self.client.transport.client[1] + self.state.clientIp = ip + self.log.extra['clientIp'] = ip self.log.info("New client connected from %(clientIp)s:%(clientPort)i", {"clientIp": ip, "clientPort": port}) @@ -101,6 +103,7 @@ def onClientDisconnection(self, reason): self.statCounter.logReport(self.log) self.server.disconnect(True) + self.state.clientIp = None # For the attacker, we want to make sure we don't abort the connection to make sure that the close event is sent self.attacker.disconnect() diff --git a/pyrdp/mitm/X224MITM.py b/pyrdp/mitm/X224MITM.py index 64c7be4dd..c42f53ce9 100644 --- a/pyrdp/mitm/X224MITM.py +++ b/pyrdp/mitm/X224MITM.py @@ -65,6 +65,9 @@ def onConnectionRequest(self, pdu: X224ConnectionRequestPDU): self.originalNegotiationRequest = parser.parse(pdu.payload) self.state.requestedProtocols = self.originalNegotiationRequest.requestedProtocols + # We assign clientIp here since this is fired before RDPMITM has the chance to update all loggers + self.log.extra['clientIp'] = self.state.clientIp + if self.originalNegotiationRequest.flags is not None and self.originalNegotiationRequest.flags & NegotiationRequestFlags.RESTRICTED_ADMIN_MODE_REQUIRED: self.log.warning("Client has enabled Restricted Admin Mode, which forces Network-Level Authentication (NLA)." " Connection will fail.", {"restrictedAdminActivated": True}) diff --git a/pyrdp/mitm/state.py b/pyrdp/mitm/state.py index 9c40d09d9..1daebd95d 100644 --- a/pyrdp/mitm/state.py +++ b/pyrdp/mitm/state.py @@ -76,6 +76,9 @@ def __init__(self, config: MITMConfig, sessionID: str): self.sessionID = sessionID """The current session ID""" + self.clientIp = None + """The current client IP address""" + self.windowSize = None self.effectiveTargetHost = self.config.targetHost From 09663f2f9c567e7fc16840a5f84f97b249958f93 Mon Sep 17 00:00:00 2001 From: Olivier Bilodeau Date: Thu, 29 Jul 2021 09:23:50 -0400 Subject: [PATCH 4/4] Fixed tests --- test/test_X224MITM.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_X224MITM.py b/test/test_X224MITM.py index 66af54404..e5cbdf18e 100644 --- a/test/test_X224MITM.py +++ b/test/test_X224MITM.py @@ -13,7 +13,7 @@ class X224MITMTest(unittest.TestCase): def setUp(self): - self.mitm = X224MITM(Mock(), Mock(), Mock(), Mock(), MagicMock(), MagicMock(), MagicMock()) + self.mitm = X224MITM(Mock(), Mock(), MagicMock(), Mock(), MagicMock(), MagicMock(), MagicMock()) def test_negotiationFlagsNone_doesntRaise(self): connectionRequest = X224ConnectionRequestPDU(0, 0, 0, 0, b"")