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

Add clientIp to all JSON logs #326

Merged
merged 4 commits into from
Aug 3, 2021
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
11 changes: 6 additions & 5 deletions pyrdp/core/ssl.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -84,14 +85,14 @@ 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()
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'
Expand All @@ -109,6 +110,6 @@ def lookup(self, cert: OpenSSL.crypto.X509) -> (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
10 changes: 8 additions & 2 deletions pyrdp/logging/adapters.py
Original file line number Diff line number Diff line change
@@ -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.
#

Expand Down Expand Up @@ -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)
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
5 changes: 5 additions & 0 deletions pyrdp/logging/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
18 changes: 18 additions & 0 deletions pyrdp/mitm/RDPMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions pyrdp/mitm/TCPMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})

Expand All @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions pyrdp/mitm/X224MITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
3 changes: 3 additions & 0 deletions pyrdp/mitm/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/test_X224MITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"")
Expand Down