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 CredSSP support when private key is known #229

Merged
merged 4 commits into from
Jun 8, 2020
Merged
Show file tree
Hide file tree
Changes from 2 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
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 certrificate 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.
alxbl marked this conversation as resolved.
Show resolved Hide resolved

## 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")
alxbl marked this conversation as resolved.
Show resolved Hide resolved
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
3 changes: 3 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,8 @@ def __init__(self):
self.useGdi: bool = False
"""Whether to allow the client to use the GDI rendering pipeline extension."""

self.authMethods: NegotiationProtocols = NegotiationProtocols.SSL
alxbl marked this conversation as resolved.
Show resolved Hide resolved

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