Skip to content

Commit

Permalink
Merge pull request #229 from GoSecure/credssp
Browse files Browse the repository at this point in the history
feat: Added CredSSP support when private key is known
  • Loading branch information
alxbl authored Jun 8, 2020
2 parents ed7c719 + c609f88 commit 95e9d75
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 8 deletions.
36 changes: 36 additions & 0 deletions docs/cert-extraction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Extracting Windows Server Remote Desktop Certificate

This procedure is useful when running honeypots to support CredSSP (using `--auth ssp`).
It requires Administrative privileges on the target server and the use of Mimikatz, so it assumes that you are able to deactivate the Anti-Virus on the target server.


> **WARNING**: Cloning the certificate of the RDP server does not mean that the certificate will be trusted. Certificate trust requires a signed certificate from a CA that is **trusted** by the client. This is not likely to be the case in most scenarios. If you want to do that, you are on your own.
## Steps

1. Turn off AV so mimikatz doesn't get flagged. (Or use excluded directory)
2. Download mimikatz latest release.
3. Go to `Start > Run... > certlm.msc` (optional)
4. Identify the valid certificate under `Remote Desktop > Certificates` and note the thumbprint (optional)
5. Export the Remote Desktop certificates using Mimikatz:

```
crypto::capi
privilege::debug
crypto::cng
crypto::certificates /systemstore:LOCAL_MACHINE /store:"Remote Desktop" /export
```

6. Convert public key to `.pem` using openssl:

```
openssl x509 -inform DER -outform PEM -in pubkey.der -out pubkey.pem
```

7. Remove private key password (password for `.pfx` is "mimikatz")

```
openssl pkcs12 -nodes -in privkey.pfx -out privkey.key
```

You can now run `pyrdp-mitm.py` by specifying `-k privkey.key -c pubkey.pem` and PyRDP will serve the same certificate as the server.
4 changes: 2 additions & 2 deletions pyrdp/layer/segmentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@


class SegmentationObserver(LayerObserver):
def onUnknownHeader(self, header):
def onUnknownHeader(self, header, data: bytes):
pass


Expand Down Expand Up @@ -63,7 +63,7 @@ def recv(self, data: bytes):
layer = self.layers[header]
except KeyError:
if self.observer:
self.observer.onUnknownHeader(header)
self.observer.onUnknownHeader(header, data)
return
else:
raise
Expand Down
13 changes: 13 additions & 0 deletions pyrdp/mitm/RDPMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@
from pyrdp.recording import FileLayer, RecordingFastPathObserver, RecordingSlowPathObserver, \
Recorder

from pyrdp.layer.segmentation import SegmentationObserver


class PacketForwarder(SegmentationObserver):
"""Handles unknown segmentation packets by forwarding them transparently."""
def __init__(self, sink):
self._forwarder = sink

def onUnknownHeader(self, header, data: bytes):
self._forwarder.sendBytes(data)


class RDPMITM:
"""
Expand Down Expand Up @@ -205,6 +216,8 @@ def startTLS(self):

self.client.tcp.startTLS(contextForClient)
self.server.tcp.startTLS(contextForServer)
self.client.segmentation.addObserver(PacketForwarder(self.server.tcp))
self.server.segmentation.addObserver(PacketForwarder(self.client.tcp))

def buildChannel(self, client: MCSServerChannel, server: MCSClientChannel):
"""
Expand Down
17 changes: 11 additions & 6 deletions pyrdp/mitm/X224MITM.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-2020 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

Expand Down Expand Up @@ -71,8 +71,8 @@ def onConnectionRequest(self, pdu: X224ConnectionRequestPDU):
chosenProtocols = self.originalRequest.requestedProtocols

if chosenProtocols is not None:
# Only SSL is implemented, so remove other protocol flags
chosenProtocols &= NegotiationProtocols.SSL
# Tell the server we only support the allowed authentication methods.
chosenProtocols &= self.state.config.authMethods

modifiedRequest = NegotiationRequestPDU(
self.originalRequest.cookie,
Expand All @@ -99,18 +99,23 @@ def onConnectionConfirm(self, pdu: X224ConnectionConfirmPDU):
:param _: the connection confirm PDU
"""

# X224 Response
protocols = NegotiationProtocols.SSL if self.originalRequest.tlsSupported else NegotiationProtocols.NONE
# FIXME: In case the server picks anything other than what we support, PyRDP is
# likely going to be unable to complete the handshake with the server.
# This should not happen since we are intercepting and spoofing the NEG_REQ,
# though.
# protocols = NegotiationProtocols.SSL if self.originalRequest.tlsSupported else NegotiationProtocols.NONE

parser = NegotiationResponseParser()
response = parser.parse(pdu.payload)
if isinstance(response, NegotiationFailurePDU):
self.log.info("The server failed the negotiation. Error: %(error)s", {"error": NegotiationFailureCode.getMessage(response.failureCode)})
payload = pdu.payload
else:
payload = parser.write(NegotiationResponsePDU(NegotiationType.TYPE_RDP_NEG_RSP, 0x00, protocols))
payload = parser.write(NegotiationResponsePDU(NegotiationType.TYPE_RDP_NEG_RSP, 0x00, response.selectedProtocols))
self.client.sendConnectionConfirm(payload, source=0x1234)

# FIXME: This should be done based on what authentication method the server selected, not on what
# the client supports.
if self.originalRequest.tlsSupported:
self.startTLSCallback()
self.state.useTLS = True
Expand Down
11 changes: 11 additions & 0 deletions pyrdp/mitm/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from pyrdp.core.ssl import ServerTLSContext
from pyrdp.logging import configure as configureLoggers, LOGGER_NAMES
from pyrdp.mitm.config import DEFAULTS, MITMConfig
from pyrdp.enum import NegotiationProtocols


def parseTarget(target: str) -> Tuple[str, int]:
Expand Down Expand Up @@ -145,6 +146,7 @@ def buildArgParser():
default="INFO", choices=["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"])
parser.add_argument("-F", "--log-filter",
help="Only show logs from this logger name (accepts '*' wildcards)", default="")
parser.add_argument("--auth", help="Specify allowed authentication mechanisms (Comma-separated, choose from: tls, ssp)", default="tls")
parser.add_argument(
"-s", "--sensor-id", help="Sensor ID (to differentiate multiple instances of the MITM"
" where logs are aggregated at one place)", default="PyRDP")
Expand Down Expand Up @@ -306,5 +308,14 @@ def configure(cmdline=None) -> MITMConfig:
logger.error("--payload-delay was provided but no payload was set.")
sys.exit(1)

# Configure allowed authentication protocols.
for auth in args.auth.split(','):
auth = auth.strip()
if auth == "tls":
config.authMethods |= NegotiationProtocols.SSL
elif auth == "ssp":
# CredSSP implies TLS.
config.authMethods |= (NegotiationProtocols.SSL | NegotiationProtocols.CRED_SSP)

showConfiguration(config)
return config
4 changes: 4 additions & 0 deletions pyrdp/mitm/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from pathlib import Path
from typing import Optional
from pyrdp.core import settings
from pyrdp.enum import NegotiationProtocols


class MITMConfig:
Expand Down Expand Up @@ -78,6 +79,9 @@ def __init__(self):
self.useGdi: bool = False
"""Whether to allow the client to use the GDI rendering pipeline extension."""

self.authMethods: NegotiationProtocols = NegotiationProtocols.SSL
"""Specifies the list of authentication protocols that PyRDP accepts."""

@property
def replayDir(self) -> Path:
"""
Expand Down

0 comments on commit 95e9d75

Please sign in to comment.