def validateKeyAndCertificate(private_key: str, certificate: str) -> Tuple[str, str]: if (private_key is None) != (certificate is None): sys.stderr.write( "You must provide both the private key and the certificate") sys.exit(1) if private_key is None: # Certificates will be generated automatically. return None, None else: key, cert = private_key, certificate try: # Check if OpenSSL accepts the private key and certificate. ServerTLSContext(key, cert) except OpenSSL.SSL.Error as error: from pyrdp.logging import log log.error( "An error occurred when creating the server TLS context. " + "There may be a problem with your private key or certificate (e.g: signature algorithm too weak). " + "Here is the exception: %(error)s", {"error": error}) sys.exit(1) return key, cert
def send(self, data: bytes): """ Save data to the file. """ if not self.file_descriptor.closed: self.file_descriptor.write(data) else: log.error( "Recording file handle closed, cannot write message: %(message)s", {"message": data})
def recv(self, data: bytes): pdu: SecurityPDU = self.mainParser.parse(data) try: self.dispatchPDU(pdu) except KeyboardInterrupt: raise except Exception: if isinstance(pdu, SecurityExchangePDU): log.error("Exception occurred when receiving Security Exchange. Data: %(securityExchangeData)s", {"securityExchangeData": hexlify(data)}) raise
def recv(self, data: bytes): virtualChannelPDU = self.mainParser.parse(data) if virtualChannelPDU.flags & VirtualChannelPDUFlag.CHANNEL_PACKET_COMPRESSED != 0: log.error("Compression flag is set on virtual channel data, it is NOT handled, crash will most likely occur.") flags = virtualChannelPDU.flags if flags & VirtualChannelPDUFlag.CHANNEL_FLAG_FIRST: self.pduBuffer = virtualChannelPDU.payload else: self.pduBuffer += virtualChannelPDU.payload if flags & VirtualChannelPDUFlag.CHANNEL_FLAG_LAST: # Reassembly done, change the payload of the virtualChannelPDU for processing by the observer. virtualChannelPDU.payload = self.pduBuffer self.pduReceived(virtualChannelPDU)
def sendBytes(self, data: bytes): """ Save data to the file. :param data: data to write. """ if not self.fd: self.pending += data if len(self.pending) > FileLayer.FLUSH_THRESHOLD: self.fd = open(str(self.filename), "wb") self.fd.write(self.pending) self.pending = b'' elif not self.fd.closed: self.fd.write(data) else: log.error("Recording file handle closed, cannot write message: %(message)s", {"message": data})
def send(self, data): """ Send data through the socket :type data: bytes """ if self.isConnected: try: log.debug("sending %(arg1)s to %(arg2)s", { "arg1": data, "arg2": self.socket.getpeername() }) self.socket.send(data) except Exception as e: log.error("Cant send data over the network socket: %(data)s", {"data": e}) self.isConnected = False
def parseEvents(self, data): events = [] while len(data) > 0: eventLength = self.readParser.getEventLength(data) eventData = data[: eventLength] data = data[eventLength :] try: event = self.readParser.parse(eventData) except KeyboardInterrupt: raise except Exception: log.error("Exception occurred when receiving: %(data)s", {"data": hexlify(eventData)}) raise events.append(event) return events
def main(): parser = argparse.ArgumentParser() parser.add_argument( "target", help="IP:port of the target RDP machine (ex: 192.168.1.10:3390)") parser.add_argument("-l", "--listen", help="Port number to listen on (default: 3389)", default=3389) parser.add_argument("-o", "--output", help="Output folder", default="pyrdp_output") parser.add_argument( "-i", "--destination-ip", help= "Destination IP address of the PyRDP player.If not specified, RDP events are not sent over the network." ) parser.add_argument( "-d", "--destination-port", help="Listening port of the PyRDP player (default: 3000).", default=3000) parser.add_argument("-k", "--private-key", help="Path to private key (for SSL)") parser.add_argument("-c", "--certificate", help="Path to certificate (for SSL)") parser.add_argument( "-n", "--nla", help="For NLA client authentication (need to provide credentials)", action="store_true") parser.add_argument( "-u", "--username", help="Username that will replace the client's username", default=None) parser.add_argument( "-p", "--password", help="Password that will replace the client's password", default=None) parser.add_argument( "-L", "--log-level", help="Console logging level. Logs saved to file are always verbose.", 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( "-s", "--sensor-id", help= "Sensor ID (to differentiate multiple instances of the MITM where logs are aggregated at one place)", default="PyRDP") parser.add_argument("--payload", help="Command to run automatically upon connection", default=None) parser.add_argument( "--payload-powershell", help="PowerShell command to run automatically upon connection", default=None) parser.add_argument( "--payload-powershell-file", help= "PowerShell script to run automatically upon connection (as -EncodedCommand)", default=None) parser.add_argument( "--payload-delay", help= "Time to wait after a new connection before sending the payload, in milliseconds", default=None) parser.add_argument( "--payload-duration", help= "Amount of time for which input / output should be dropped, in milliseconds. This can be used to hide the payload screen.", default=None) parser.add_argument("--crawl", help="Enable automatic shared drive scraping", action="store_true") parser.add_argument( "--crawler-match-file", help= "File to be used by the crawler to chose what to download when scraping the client shared drives.", default=None) 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") args = parser.parse_args() outDir = Path(args.output) outDir.mkdir(exist_ok=True) logLevel = getattr(logging, args.log_level) prepareLoggers(logLevel, args.log_filter, args.sensor_id, outDir) pyrdpLogger = logging.getLogger(LOGGER_NAMES.MITM) target = args.target if ":" in target: targetHost = target[:target.index(":")] targetPort = int(target[target.index(":") + 1:]) else: targetHost = target targetPort = 3389 if (args.private_key is None) != (args.certificate is None): pyrdpLogger.error( "You must provide both the private key and the certificate") sys.exit(1) elif args.private_key is None: key, certificate = getSSLPaths() handleKeyAndCertificate(key, certificate) else: key, certificate = args.private_key, args.certificate listenPort = int(args.listen) config = MITMConfig() config.targetHost = targetHost config.targetPort = targetPort config.privateKeyFileName = key config.certificateFileName = certificate config.attackerHost = args.destination_ip config.attackerPort = int(args.destination_port) config.replacementUsername = args.username config.replacementPassword = args.password config.outDir = outDir config.enableCrawler = args.crawl config.crawlerMatchFileName = args.crawler_match_file config.crawlerIgnoreFileName = args.crawler_ignore_file config.recordReplays = not args.no_replay payload = None powershell = None if int(args.payload is not None) + int( args.payload_powershell is not None) + int( args.payload_powershell_file is not None) > 1: pyrdpLogger.error( "Only one of --payload, --payload-powershell and --payload-powershell-file may be supplied." ) sys.exit(1) if args.payload is not None: payload = args.payload pyrdpLogger.info("Using payload: %(payload)s", {"payload": args.payload}) elif args.payload_powershell is not None: powershell = args.payload_powershell pyrdpLogger.info("Using powershell payload: %(payload)s", {"payload": args.payload_powershell}) elif args.payload_powershell_file is not None: if not os.path.exists(args.payload_powershell_file): pyrdpLogger.error("Powershell file %(path)s does not exist.", {"path": args.payload_powershell_file}) sys.exit(1) try: with open(args.payload_powershell_file, "r") as f: powershell = f.read() except IOError as e: pyrdpLogger.error( "Error when trying to read powershell file: %(error)s", {"error": e}) sys.exit(1) pyrdpLogger.info("Using payload from powershell file: %(path)s", {"path": args.payload_powershell_file}) if powershell is not None: payload = "powershell -EncodedCommand " + b64encode( powershell.encode("utf-16le")).decode() if payload is not None: if args.payload_delay is None: pyrdpLogger.error( "--payload-delay must be provided if a payload is provided.") sys.exit(1) if args.payload_duration is None: pyrdpLogger.error( "--payload-duration must be provided if a payload is provided." ) sys.exit(1) try: config.payloadDelay = int(args.payload_delay) except ValueError: pyrdpLogger.error( "Invalid payload delay. Payload delay must be an integral number of milliseconds." ) sys.exit(1) if config.payloadDelay < 0: pyrdpLogger.error("Payload delay must not be negative.") sys.exit(1) if config.payloadDelay < 1000: pyrdpLogger.warning( "You have provided a payload delay of less than 1 second. We recommend you use a slightly longer delay to make sure it runs properly." ) try: config.payloadDuration = int(args.payload_duration) except ValueError: pyrdpLogger.error( "Invalid payload duration. Payload duration must be an integral number of milliseconds." ) sys.exit(1) if config.payloadDuration < 0: pyrdpLogger.error("Payload duration must not be negative.") sys.exit(1) config.payload = payload elif args.payload_delay is not None: pyrdpLogger.error( "--payload-delay was provided but no payload was set.") sys.exit(1) try: # Check if OpenSSL accepts the private key and certificate. ServerTLSContext(config.privateKeyFileName, config.certificateFileName) except OpenSSL.SSL.Error as error: log.error( "An error occurred when creating the server TLS context. " + "There may be a problem with your private key or certificate (e.g: signature algorithm too weak). " + "Here is the exception: %(error)s", {"error": error}) sys.exit(1) logConfiguration(config) reactor.listenTCP(listenPort, MITMServerFactory(config)) pyrdpLogger.info("MITM Server listening on port %(port)d", {"port": listenPort}) reactor.run() pyrdpLogger.info("MITM terminated") logConfiguration(config)
def RDPBitmapToQtImage(width, height, bitsPerPixel, isCompressed, data): """ @summary: Bitmap transformation to Qt object @param width: width of bitmap @param height: height of bitmap @param bitsPerPixel: number of bit per pixel @param isCompressed: use RLE compression @param data: bitmap data """ image = None #allocate if bitsPerPixel == 15: if isCompressed: buf = bytearray(width * height * 2) rle.bitmap_decompress(buf, width, height, data, 2) image = QtGui.QImage(buf, width, height, QtGui.QImage.Format_RGB555) else: image = QtGui.QImage(data, width, height, QtGui.QImage.Format_RGB555).transformed( QtGui.QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0)) elif bitsPerPixel == 16: if isCompressed: buf = bytearray(width * height * 2) rle.bitmap_decompress(buf, width, height, data, 2) image = QtGui.QImage(buf, width, height, QtGui.QImage.Format_RGB16) else: image = QtGui.QImage(data, width, height, QtGui.QImage.Format_RGB16).transformed( QtGui.QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0)) elif bitsPerPixel == 24: if isCompressed: buf = bytearray(width * height * 3) rle.bitmap_decompress(buf, width, height, data, 3) # This is a ugly patch because there is a bug in the 24bpp decompression in rle.c # where the red and the blue colors are inverted. Fixing this in python causes a performance # issue, but at least it shows the good colors. buf2 = BytesIO(buf) while buf2.tell() < len(buf2.getvalue()): pixel = buf2.read(3) buf[buf2.tell() - 3] = pixel[2] buf[buf2.tell() - 1] = pixel[0] image = QtGui.QImage(buf, width, height, QtGui.QImage.Format_RGB888) else: image = QtGui.QImage(data, width, height, QtGui.QImage.Format_RGB888).transformed( QtGui.QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0)) elif bitsPerPixel == 32: if isCompressed: buf = bytearray(width * height * 4) rle.bitmap_decompress(buf, width, height, data, 4) image = QtGui.QImage(buf, width, height, QtGui.QImage.Format_RGB32) else: image = QtGui.QImage(data, width, height, QtGui.QImage.Format_RGB32).transformed( QtGui.QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0)) elif bitsPerPixel == 8: if isCompressed: buf = bytearray(width * height * 1) rle.bitmap_decompress(buf, width, height, data, 1) buf2 = convert8bppTo16bpp(buf) image = QtGui.QImage(buf2, width, height, QtGui.QImage.Format_RGB16) else: buf2 = convert8bppTo16bpp(data) image = QtGui.QImage(buf2, width, height, QtGui.QImage.Format_RGB16).transformed( QtGui.QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0)) else: log.error("Receive image in bad format") image = QtGui.QImage(width, height, QtGui.QImage.Format_RGB32) return image
def main(): parser = argparse.ArgumentParser() parser.add_argument("target", help="IP:port of the target RDP machine (ex: 192.168.1.10:3390)") parser.add_argument("-l", "--listen", help="Port number to listen on (default: 3389)", default=3389) parser.add_argument("-o", "--output", help="Output folder", default="pyrdp_output") parser.add_argument("-i", "--destination-ip", help="Destination IP address of the PyRDP player.If not specified, RDP events are not sent over the network.") parser.add_argument("-d", "--destination-port", help="Listening port of the PyRDP player (default: 3000).", default=3000) parser.add_argument("-k", "--private-key", help="Path to private key (for SSL)") parser.add_argument("-c", "--certificate", help="Path to certificate (for SSL)") parser.add_argument("-n", "--nla", help="For NLA client authentication (need to provide credentials)", action="store_true") parser.add_argument("-u", "--username", help="Username that will replace the client's username", default=None) parser.add_argument("-p", "--password", help="Password that will replace the client's password", default=None) parser.add_argument("-L", "--log-level", help="Console logging level. Logs saved to file are always verbose.", 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("-s", "--sensor-id", help="Sensor ID (to differentiate multiple instances of the MITM where logs are aggregated at one place)", default="PyRDP") parser.add_argument("--no-replay", help="Disable replay recording", action="store_true") args = parser.parse_args() outDir = Path(args.output) outDir.mkdir(exist_ok = True) logLevel = getattr(logging, args.log_level) prepareLoggers(logLevel, args.log_filter, args.sensor_id, outDir) pyrdpLogger = logging.getLogger(LOGGER_NAMES.MITM) target = args.target if ":" in target: targetHost = target[: target.index(":")] targetPort = int(target[target.index(":") + 1:]) else: targetHost = target targetPort = 3389 if (args.private_key is None) != (args.certificate is None): pyrdpLogger.error("You must provide both the private key and the certificate") sys.exit(1) elif args.private_key is None: key, certificate = getSSLPaths() handleKeyAndCertificate(key, certificate) else: key, certificate = args.private_key, args.certificate listenPort = int(args.listen) config = MITMConfig() config.targetHost = targetHost config.targetPort = targetPort config.privateKeyFileName = key config.certificateFileName = certificate config.attackerHost = args.destination_ip config.attackerPort = int(args.destination_port) config.replacementUsername = args.username config.replacementPassword = args.password config.outDir = outDir config.recordReplays = not args.no_replay try: # Check if OpenSSL accepts the private key and certificate. ServerTLSContext(config.privateKeyFileName, config.certificateFileName) except OpenSSL.SSL.Error as error: log.error( "An error occurred when creating the server TLS context. " + "There may be a problem with your private key or certificate (e.g: signature algorithm too weak). " + "Here is the exception: %(error)s", {"error": error} ) sys.exit(1) logConfiguration(config) reactor.listenTCP(listenPort, MITMServerFactory(config)) pyrdpLogger.info("MITM Server listening on port %(port)d", {"port": listenPort}) reactor.run() pyrdpLogger.info("MITM terminated") logConfiguration(config)
def RDPBitmapToQtImage(width: int, height: int, bitsPerPixel: int, isCompressed: bool, data: bytes): """ Bitmap transformation to Qt object :param width: width of bitmap :param height: height of bitmap :param bitsPerPixel: number of bit per pixel :param isCompressed: use RLE compression :param data: bitmap data """ image = None buf = None if bitsPerPixel == 15: if isCompressed: buf = rle.bitmap_decompress(data, width, height, 2) image = QImage(buf, width, height, QImage.Format_RGB555) else: buf = data image = QImage(buf, width, height, QImage.Format_RGB555).transformed( QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0)) elif bitsPerPixel == 16: if isCompressed: buf = rle.bitmap_decompress(data, width, height, 2) image = QImage(buf, width, height, QImage.Format_RGB16) else: buf = data image = QImage(buf, width, height, QImage.Format_RGB16).transformed( QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0)) elif bitsPerPixel == 24: if isCompressed: buf = rle.bitmap_decompress(data, width, height, 3) # This is a ugly patch because there is a bug in the 24bpp decompression in rle.c # where the red and the blue colors are inverted. Fixing this in python causes a performance # issue, but at least it shows the good colors. buf2 = BytesIO(buf) while buf2.tell() < len(buf2.getvalue()): pixel = buf2.read(3) buf[buf2.tell() - 3] = pixel[2] buf[buf2.tell() - 1] = pixel[0] image = QImage(buf, width, height, QImage.Format_RGB888) else: buf = data image = QImage(buf, width, height, QImage.Format_RGB888).transformed( QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0)) elif bitsPerPixel == 32: if isCompressed: buf = rle.bitmap_decompress(data, width, height, 4) image = QImage(buf, width, height, QImage.Format_RGB32) else: buf = data image = QImage(buf, width, height, QImage.Format_RGB32).transformed( QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0)) elif bitsPerPixel == 8: if isCompressed: _buf = rle.bitmap_decompress(data, width, height, 1) buf = convert8bppTo16bpp(_buf) image = QImage(buf, width, height, QImage.Format_RGB16) else: buf = convert8bppTo16bpp(data) image = QImage(buf, width, height, QImage.Format_RGB16).transformed( QMatrix(1.0, 0.0, 0.0, -1.0, 0.0, 0.0)) else: log.error("Receive image in bad format") image = QImage(width, height, QImage.Format_RGB32) return (image, buf)