Repository: adityatelange/evil-winrm-py Branch: main Commit: e956d38568ae Files: 21 Total size: 128.4 KB Directory structure: gitextract_ce49hs3q/ ├── .github/ │ └── workflows/ │ └── wiki.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs/ │ ├── README.md │ ├── development.md │ ├── install.md │ ├── knowledgebase.md │ ├── release.md │ ├── sample/ │ │ └── krb5.conf │ └── usage.md ├── evil_winrm_py/ │ ├── __init__.py │ ├── _ps/ │ │ ├── __init__.py │ │ ├── exec.ps1 │ │ ├── fetch.ps1 │ │ ├── loaddll.ps1 │ │ └── send.ps1 │ ├── evil_winrm_py.py │ └── pypsrp_ewp/ │ ├── __init__.py │ └── wsman.py └── setup.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/wiki.yml ================================================ name: Publish wiki on: push: branches: [main] paths: - docs/** - .github/workflows/wiki.yml concurrency: group: publish-wiki cancel-in-progress: true permissions: contents: write jobs: publish-wiki: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: Andrew-Chen-Wang/github-wiki-action@v5.0.1 with: path: docs ignore: | *.conf ================================================ 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 # UV # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. #uv.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 # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/latest/usage/project/#working-with-version-control .pdm.toml .pdm-python .pdm-build/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __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 maintained 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/ # Ruff stuff: .ruff_cache/ # PyPI configuration file .pypirc # Test Files test_*.py ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2025 Aditya Telange 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: README.md ================================================
ewp-logo

evil-winrm-py

[![PyPI version](https://img.shields.io/pypi/v/evil-winrm-py)](https://pypi.org/project/evil-winrm-py/) ![Python](https://img.shields.io/badge/python-3.9+-blue.svg) ![License](https://img.shields.io/github/license/adityatelange/evil-winrm-py) ![PyPI - Downloads](https://img.shields.io/pypi/dm/evil-winrm-py?label=pypi%20downloads) [![Github Wiki](https://img.shields.io/badge/github-wiki%2Fdocs-blue)](https://github.com/adityatelange/evil-winrm-py/wiki)
`evil-winrm-py` is a python-based tool for executing commands on remote Windows machines using the WinRM (Windows Remote Management) protocol. It provides an interactive shell with enhanced features like file upload/download, command history, and colorized output. It supports various authentication methods including NTLM, Pass-the-Hash, Certificate, and Kerberos. ![](https://raw.githubusercontent.com/adityatelange/evil-winrm-py/refs/tags/v1.6.0/assets/terminal.png) > [!NOTE] > This tool is designed strictly for educational, ethical use, and authorized penetration testing. Always ensure you have explicit authorization before accessing any system. Unauthorized access or misuse of this tool is both illegal and unethical. ## Motivation The original evil-winrm is written in Ruby, which can be a hurdle for some users. Rewriting it in Python makes it more accessible and easier to use, while also allowing us to leverage Python’s rich ecosystem for added features and flexibility. I also wanted to learn more about winrm and its internals, so this project will also serve as a learning experience for me. ## Features - Execute commands on remote Windows machines via an interactive shell. - Download files from the remote host to the local machine. - Upload files from the local machine to the remote host. - Progress bar for file transfers with speed and time estimation. - Stable and reliable file transfer including support for large files with MD5 checksum verification. - Auto-complete local and remote file paths (even those with spaces) with `Tab` completion. - Auto-complete PowerShell cmdlets/helpers with `Tab` completion. 🆕 - Load PowerShell functions from local scripts into the interactive shell. 🆕 - Run local PowerShell scripts on the remote host. 🆕 - Load local DLLs (in-memory) as PowerShell modules on the remote host. 🆕 - Upload and execute local EXEs (in-memory) on the remote host. 🆕 - List the running services (except system services) on the remote host. 🆕 - Enable logging and debugging for better traceability. - Navigate command history using `up`/`down` arrow keys. - Display colorized output for improved readability. - Lightweight and Python-based for ease of use. - Keyboard Interrupt (Ctrl+C / Ctrl+D) support to terminate long-running commands gracefully. Includes support for: - NTLM authentication. - Pass-the-Hash authentication. - Certificate authentication. - Kerberos authentication with custom SPN prefix and hostname options. - SSL to secure communication with the remote host. - custom WSMan URIs. - custom user agent for the WinRM client. Detailed documentation can be found in the [docs](https://github.com/adityatelange/evil-winrm-py/blob/main/docs) directory. ## Installation (Windows/Linux) #### Installation of Kerberos prerequisites on Linux ```bash sudo apt install gcc python3-dev libkrb5-dev krb5-pkinit # Optional: krb5-user ``` ### Install `evil-winrm-py` > You may use [pipx](https://pipx.pypa.io/stable/) or [uv](https://docs.astral.sh/uv/) instead of pip to install evil-winrm-py. `pipx`/`uv` is a tool to install and run Python applications in isolated environments, which helps prevent dependency conflicts by keeping the tool's dependencies separate from your system's Python packages. ```bash pip install evil-winrm-py pip install evil-winrm-py[kerberos] # for kerberos support on Linux # Note: building gssapi and krb5 packages may take some time, so be patient. ``` or if you want to install with latest commit from the main branch you can do so by cloning the repository and installing it with `pip`/`pipx`/`uv`: ```bash git clone https://github.com/adityatelange/evil-winrm-py cd evil-winrm-py pip install . ``` ### Update ```bash pip install --upgrade evil-winrm-py ``` ### Uninstall ```bash pip uninstall evil-winrm-py ``` Check [Installation Guide](https://github.com/adityatelange/evil-winrm-py/blob/main/docs/install.md) for more details. ## Availability on Unix distributions [![Packaging status](https://repology.org/badge/vertical-allrepos/evil-winrm-py.svg)](https://repology.org/project/evil-winrm-py/versions) For above mentioned distributions, you can install `evil-winrm-py` directly from their package managers. Thanks to the package maintainers for packaging and maintaining `evil-winrm-py` in their respective distributions. ## Usage Details on how to use `evil-winrm-py` can be found in the [Usage Guide](https://github.com/adityatelange/evil-winrm-py/blob/main/docs/usage.md). ```bash usage: evil-winrm-py [-h] -i IP [-u USER] [-p PASSWORD] [-H HASH] [--priv-key-pem PRIV_KEY_PEM] [--cert-pem CERT_PEM] [--uri URI] [--ua UA] [--port PORT] [--spn-prefix SPN_PREFIX] [--spn-hostname SPN_HOSTNAME] [-k] [--no-pass] [--ssl] [--log] [--debug] [--no-colors] [--version] options: -h, --help show this help message and exit -i, --ip IP remote host IP or hostname -u, --user USER username -p, --password PASSWORD password -H, --hash HASH nthash --priv-key-pem PRIV_KEY_PEM local path to private key PEM file --cert-pem CERT_PEM local path to certificate PEM file --uri URI wsman URI (default: /wsman) --ua UA user agent for the WinRM client (default: "Microsoft WinRM Client") --port PORT remote host port (default 5985) --spn-prefix SPN_PREFIX specify spn prefix --spn-hostname SPN_HOSTNAME specify spn hostname -k, --kerberos use kerberos authentication --no-pass do not prompt for password --ssl use ssl --log log session to file --debug enable debug logging --no-colors disable colors --version show version For more information about this project, visit https://github.com/adityatelange/evil-winrm-py For user guide, visit https://github.com/adityatelange/evil-winrm-py/blob/main/docs/usage.md ``` Example: ```bash evil-winrm-py -i 192.168.1.100 -u Administrator -p P@ssw0rd --ssl ``` ## Menu Commands (inside evil-winrm-py shell) ```bash Menu: [+] services - Show the running services (except system services) [+] upload - Upload a file [+] download - Download a file [+] loadps .ps1 - Load PowerShell functions from a local script [+] runps .ps1 - Run a local PowerShell script on the remote host [+] loaddll .dll - Load a local DLL (in-memory) as a module on the remote host [+] runexe .exe [args] - Upload and execute (in-memory) a local EXE on the remote host [+] menu - Show this menu [+] clear, cls - Clear the screen [+] exit - Exit the shell Note: Use absolute paths for upload/download for reliability. ``` ## Credits - Original evil-winrm project - https://github.com/Hackplayers/evil-winrm - PowerShell Remoting Protocol for Python - https://github.com/jborean93/pypsrp - Prompt Toolkit - https://github.com/prompt-toolkit/python-prompt-toolkit - tqdm - https://github.com/tqdm/tqdm - Thanks to [Github Coplilot](https://github.com/features/copilot) and [Google Gemini](https://gemini.google.com/app) for code suggestions and improvements. ## Stargazers over time [![Stargazers over time](https://starchart.cc/adityatelange/evil-winrm-py.svg?background=%23ffffff00&axis=%23858585&line=%236b63ff)](https://starchart.cc/adityatelange/evil-winrm-py) ================================================ FILE: docs/README.md ================================================ ## Quick Links - **[Usage Guide](./usage.md)** - **[Installation Guide](./install.md)** - [Knowledge Base](./knowledgebase.md) - [Development Guide](./development.md) - [Release Guide](./release.md) ================================================ FILE: docs/development.md ================================================ # Development Environment Setup ## Setup Download the repository. ```bash git clone https://github.com/adityatelange/evil-winrm-py cd evil-winrm-py ``` Create a virtual environment (optional but recommended): ```bash python3 -m venv venv source venv/bin/activate ``` Install the required packages: ```bash pip install pypsrp[kerberos]==0.8.1 prompt_toolkit==3.0.51 tqdm==4.67.1 ``` ## Create a test file ```python # File: test.py from evil_winrm_py.evil_winrm_py import main if __name__ == "__main__": main() ``` ## Run the test file ```bash python test.py -h ``` ================================================ FILE: docs/install.md ================================================ # Installation Guide `evil-winrm-py` is available on: - PyPI - https://pypi.org/project/evil-winrm-py/ - Github - https://github.com/adityatelange/evil-winrm-py - Kali Linux - https://pkg.kali.org/pkg/evil-winrm-py - Parrot OS - https://gitlab.com/parrotsec/packages/evil-winrm-py ## For Kali Linux and Parrot OS Users If you are using Kali Linux or Parrot OS, you can install `evil-winrm-py` directly from the package manager: ```bash sudo apt update sudo apt install evil-winrm-py ``` --- ## Installation of Kerberos Dependencies on Linux ```bash sudo apt install gcc python3-dev libkrb5-dev krb5-pkinit # Optional: krb5-user ``` > [!NOTE] > `[kerberos]` is an optional dependency that includes the necessary packages for Kerberos authentication support. If you do not require Kerberos authentication, you can install `evil-winrm-py` without this extra. ## Using `pip` You can install the package directly from PyPI using pip: ```bash pip install evil-winrm-py[kerberos] ``` Installing latest development version directly from GitHub: ```bash pip install 'evil-winrm-py[kerberos] @ git+https://github.com/adityatelange/evil-winrm-py' ``` ## Using `pipx` For a more isolated installation, you can use pipx: ```bash pipx install evil-winrm-py[kerberos] ``` Installing latest development version directly from GitHub: ```bash pipx install 'evil-winrm-py[kerberos] @ git+https://github.com/adityatelange/evil-winrm-py' ``` ## Using `uv` If you prefer using `uv`, you can install the package with the following command: ```bash uv tool install evil-winrm-py[kerberos] ``` Installing latest development version directly from GitHub: ```bash uv tool install git+https://github.com/adityatelange/evil-winrm-py[kerberos] ``` ================================================ FILE: docs/knowledgebase.md ================================================ # Knowledge Base ## Negotiate authentication A negotiated, single sign on type of authentication that is the Windows implementation of [Simple and Protected GSSAPI Negotiation Mechanism (SPNEGO)](https://learn.microsoft.com/en-us/windows/win32/winrm/windows-remote-management-glossary). SPNEGO negotiation determines whether authentication is handled by Kerberos or NTLM. Kerberos is the preferred mechanism. Negotiate authentication on Windows-based systems is also called Windows Integrated Authentication. Reference: - https://learn.microsoft.com/en-us/windows/win32/winrm/windows-remote-management-glossary#:~:text=A%20negotiated%2C%20single,Windows%20Integrated%20Authentication - https://learn.microsoft.com/en-us/windows/win32/winrm/authentication-for-remote-connections#negotiate-authentication ## WinRM - Types of Authentication 1. Basic Authentication 2. Digest Authentication 3. Kerberos Authentication 4. Negotiate Authentication 5. NTLM Authentication 6. Certificate Authentication 7. CredSSP Authentication Reference: https://learn.microsoft.com/en-us/windows/win32/winrm/authentication-for-remote-connections Enable Auth ```powershell Set-Item -Path WSMan:\localhost\Service\Auth\Certificate -Value $true ``` ## Configure WinRM HTTPS with self-signed certificate ```powershell # https://gist.github.com/gregjhogan/dbe0bfa277d450c049e0bbdac6142eed $cert = New-SelfSignedCertificate -CertstoreLocation Cert:\LocalMachine\My -DnsName $env:COMPUTERNAME Enable-PSRemoting -SkipNetworkProfileCheck -Force New-Item -Path WSMan:\LocalHost\Listener -Transport HTTPS -Address * -CertificateThumbPrint $cert.Thumbprint –Force New-NetFirewallRule -DisplayName "Windows Remote Management (HTTPS-In)" -Name "Windows Remote Management (HTTPS-In)" -Profile Any -LocalPort 5986 -Protocol TCP ``` Reference: https://learn.microsoft.com/en-us/windows/win32/winrm/installation-and-configuration-for-windows-remote-management - **Get the current WinRM configuration** ```powershell winrm get winrm/config ``` - **Enumerate WinRM listeners** ```powershell winrm enumerate winrm/config/listener ``` ## Configure WinRM Certificate Authentication Certificate authentication is a method of authenticating to a remote computer using a certificate. The certificate must be installed on the remote computer and the client must have access to the private key of the certificate. **Enable Certificate Authentication** ```powershell Set-Item -Path WSMan:\localhost\Service\Auth\Certificate -Value $true ``` **Generate a certificate using PowerShell** ```powershell # Set the username to the name of the user the certificate will be mapped to $username = 'local-user' $clientParams = @{ CertStoreLocation = 'Cert:\CurrentUser\My' NotAfter = (Get-Date).AddYears(1) Provider = 'Microsoft Software Key Storage Provider' Subject = "CN=$username" TextExtension = @("2.5.29.37={text}1.3.6.1.5.5.7.3.2","2.5.29.17={text}upn=$username@localhost") Type = 'Custom' } $cert = New-SelfSignedCertificate @clientParams $certKeyName = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey( $cert).Key.UniqueName # Exports the public cert.pem and key cert.pfx Set-Content -Path "cert.pem" -Value @( "-----BEGIN CERTIFICATE-----" [Convert]::ToBase64String($cert.RawData) -replace ".{64}", "$&`n" "-----END CERTIFICATE-----" ) $certPfxBytes = $cert.Export('Pfx', '') [System.IO.File]::WriteAllBytes("$pwd\cert.pfx", $certPfxBytes) # Removes the private key and cert from the store after exporting $keyPath = [System.IO.Path]::Combine($env:AppData, 'Microsoft', 'Crypto', 'Keys', $certKeyName) Remove-Item -LiteralPath "Cert:\CurrentUser\My\$($cert.Thumbprint)" -Force Remove-Item -LiteralPath $keyPath -Force ``` We now have `cert.pem` and `cert.pfx` files. **Import Certificate to the Certificate Store** ```powershell $store = Get-Item -LiteralPath Cert:\LocalMachine\Root $store.Open('ReadWrite') $store.Add($cert) $store.Dispose() ``` **Mapping Certificate to a Local Account** ```powershell # Will prompt for the password of the user. $credential = Get-Credential local-user $certChain = [System.Security.Cryptography.X509Certificates.X509Chain]::new() [void]$certChain.Build($cert) $caThumbprint = $certChain.ChainElements.Certificate[-1].Thumbprint $certMapping = @{ Path = 'WSMan:\localhost\ClientCertificate' Subject = $cert.GetNameInfo('UpnName', $false) Issuer = $caThumbprint Credential = $credential Force = $true } New-Item @certMapping ``` **Convert to PEM format** ```bash openssl pkcs12 \ -in cert.pfx \ -nocerts \ -nodes \ -passin pass: | sed -ne '/-BEGIN PRIVATE KEY-/,/-END PRIVATE KEY-/p' > priv-key.pem ``` User `local-user` can now auth using private key `priv_key.pem` and public key `cert.pem`. Reference: https://docs.ansible.com/ansible/latest/os_guide/windows_winrm_certificate.html ================================================ FILE: docs/release.md ================================================ # Releasing a new version on PyPI Read More: https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/ ## Setup ```bash python3 -m pip install --upgrade build python3 -m pip install --upgrade twine ``` ## Bump version ```bash # File: evil_winrm_py/__init__.py __version__ = "X.Y.Z" # update this to the new version ``` ## Sceenshot Creating screenshots for the README using the [freeze](https://github.com/charmbracelet/freeze) tool. ```bash freeze --execute "evil-winrm-py -h" -o assets/terminal.png --padding 5 --border.radius 4 # --wrap 120 ``` Update the screenshot tag in the README file. ```diff # File: evil_winrm_py/README.md -![](https://raw.githubusercontent.com/adityatelange/evil-winrm-py/refs/tags/v1.4.0/assets/terminal.png) +![](https://raw.githubusercontent.com/adityatelange/evil-winrm-py/refs/tags/v1.4.1/assets/terminal.png) ``` ## Build ```bash python3 -m build ``` ## Upload ```bash python3 -m twine upload dist/evil_winrm_py-$VERSION* # example: python3 -m twine upload dist/evil_winrm_py-0.0.2* ``` ================================================ FILE: docs/sample/krb5.conf ================================================ # Sample Kerberos configuration file # Location: /etc/krb5.conf or //krb5.conf [libdefaults] default_realm = SEVENKINGDOMS.LOCAL dns_lookup_realm = false dns_lookup_kdc = false [realms] SEVENKINGDOMS.LOCAL = { kdc = kingslanding.sevenkingdoms.local admin_server = kingslanding.sevenkingdoms.local default_domain = sevenkingdoms.local } [domain_realm] .sevenkingdoms.local = SEVENKINGDOMS.LOCAL sevenkingdoms.local = SEVENKINGDOMS.LOCAL ================================================ FILE: docs/usage.md ================================================ # Usage Guide ## Authentication Methods ### NTLM Authentication ```bash evil-winrm-py -i -u -p ``` ### Kerberos Authentication Kerberos authentication supports both password-based and ticket-based authentication. #### Generate hosts file entry Use `netexec` to generate a hosts file entry for the target domain. ```bash netexec smb sevenkingdoms.local --generate-hosts-file hosts.txt ``` Copy the content of `hosts.txt` to your `/etc/hosts` file. > [!IMPORTANT] > If you are adding an entry manually, ensure you follow the correct format for subdomains and fully qualified domain names (FQDNs). Kerberos uses SPNEGO, which relies on a specific algorithm to resolve hostnames. For more details, see [SPNEGO algorithm to resolve host names](https://www.ibm.com/docs/en/samfm/8.0.1?topic=spnego-algorithm-resolve-host-names). > > The format is as follows: > > ``` > fully_qualified_hostname short_name > kingslanding.sevenkingdoms.local sevenkingdoms.local kingslanding > ``` #### Generate krb5.conf file Use `netexec` to generate a `krb5.conf` file for the target domain. ```bash netexec smb sevenkingdoms.local --generate-krb5-file krb5.conf ``` Sample `krb5.conf` file can be found [here](https://github.com/adityatelange/evil-winrm-py/blob/main/docs/sample/krb5.conf). #### Password-based Kerberos Authentication This will request a Kerberos ticket and store it in memory for the session. ```bash evil-winrm-py -i -u -p --kerberos ``` #### Ticket-based Kerberos Authentication If you already have a Kerberos ticket (e.g., from `kinit`), you can use it directly without providing a password. Specify the `KRB5CCNAME` and `KRB5_CONFIG` environment variables to point to your Kerberos ticket cache and configuration file, respectively. ```bash export KRB5CCNAME=/path/to/your/krb5cc_file export KRB5_CONFIG=/path/to/your/krb5.conf # By default, the ticket cache is stored in `/tmp/krb5cc_` on Unix-like systems. # By default, the Kerberos configuration file is located at `/etc/krb5.conf` on Unix-like systems. ``` Then, you can run the command without providing a username or password: ```bash evil-winrm-py -i --kerberos ``` > [!IMPORTANT] > Make sure when you use a cache ticket, the `SPN` i.e `Service principal` is set correctly. The `SPN` is usually in the format of `http/` or `cifs/`. The hostname should _always_ be in lowercase. The tool also supports direct authentication (without setting `KRB5CCNAME`) when passing username and password, which will request a ticket for the user and use it for authentication. ```bash evil-winrm-py -i -u -p --kerberos ``` Optionally, you can specify the Kerberos realm and SPN prefix/hostname If you have a Kerberos ticket, you can use it with the following options: ```bash evil-winrm-py -i -u --kerberos --no-pass --spn-prefix --spn-hostname ``` ### Pass-the-Hash Authentication If you have the NTLM hash of the user's password, you can use it for authentication without needing the plaintext password. ```bash evil-winrm-py -i -u -H ``` ### Certificate Authentication If you want to use certificate-based authentication, you can specify the private key and certificate files in PEM format. ```bash evil-winrm-py -i -u --priv-key-pem --cert-pem ``` ## Connection Options ### Using SSL This will use port 5986 for SSL connections by default. If you want to use a different port, you can specify it with [custom port option](#using-custom-port). ```bash evil-winrm-py -i -u -p --ssl ``` ### Using Custom URI If the target server has a custom WinRM URI, you can specify it using the `--uri` option. This is useful if the WinRM service is hosted on a different path than the default. ```bash evil-winrm-py -i -u -p --uri ``` ### Using Custom Port If the target server is using a non-standard port for WinRM, you can specify the port using the `--port` option. The default port for WinRM over HTTP is 5985, and for HTTPS it is 5986. ```bash evil-winrm-py -i -u -p --port ``` ## Logging and Debugging Logging will create a log file in the current directory named `evil-winrm-py.log`. ```bash evil-winrm-py -i -u -p --log ``` ### Debugging If Debug mode is enabled, it will also log debug information, including debug messages and stack traces from libraries used by the tool. ```bash evil-winrm-py -i -u -p --debug ``` Debugging for kerberos authentication can be enabled by setting the `KRB5_TRACE` environment variable to a file path where you want to log the Kerberos debug information. ```bash export KRB5_TRACE=/path/to/kerberos_debug.log ``` or you can set it to `stdout` to print the debug information to the console. ```bash export KRB5_TRACE=/dev/stdout evil-winrm-py -i -u -p --kerberos ``` ## Interactive Shell Once you have successfully authenticated, you will be dropped into an interactive shell where you can execute commands on the remote Windows machine. ```bash _ _ _ _____ _(_| |_____ __ _(_)_ _ _ _ _ __ ___ _ __ _ _ / -_\ V | | |___\ V V | | ' \| '_| ' |___| '_ | || | \___|\_/|_|_| \_/\_/|_|_||_|_| |_|_|_| | .__/\_, | |_| |__/ v1.3.0 [*] Connecting to '192.168.1.100' as 'Administrator' evil-winrm-py PS C:\Users\Administrator\Documents> █ ``` You can execute commands just like you would in a normal Windows command prompt. To exit the interactive shell, type `exit` or press `Ctrl+D`. If you want to cancel a command that is currently running, you can use `Ctrl+C`. ### Menu Commands Inside the interactive shell, you can use the following commands: ```bash Menu: [+] services - Show the running services (except system services) [+] upload - Upload a file [+] download - Download a file [+] loadps .ps1 - Load PowerShell functions from a local script [+] runps .ps1 - Run a local PowerShell script on the remote host [+] loaddll .dll - Load a local DLL (in-memory) as a module on the remote host [+] runexe .exe [args] - Upload and execute (in-memory) a local EXE on the remote host [+] menu - Show this menu [+] clear, cls - Clear the screen [+] exit - Exit the shell Note: Use absolute paths for upload/download for reliability. ``` ### Show Running Services You can list the running services (except system services) on the remote host using the `services` command. This will display a list of services that are currently running, which can be useful for post-exploitation tasks. ```bash evil-winrm-py PS C:\Users\Administrator\Documents> services ``` ### File Transfer You can upload and download files using the following commands: ```bash evil-winrm-py PS C:\Users\Administrator\Documents> upload ``` ```bash evil-winrm-py PS C:\Users\Administrator\Documents> download ``` ### Loading PowerShell Scripts (Dot Sourcing) You can load PowerShell functions from a local script file into the interactive shell using the `loadps` command. This allows you to use custom PowerShell functions defined in your script. This method is known as "dot sourcing". This can be helpful when using tools like `PowerView` or `PowerUp` that provide a set of PowerShell functions for post-exploitation tasks. ```bash evil-winrm-py PS C:\Users\Administrator\Documents> loadps .ps1 ``` These functions will be added to Command Suggestions so you can use them directly using the `Tab` key for auto-completion. The help command can be used to get more information about the available commands in the interactive shell. ```bash evil-winrm-py PS C:\Users\Administrator\Documents> Get-Help # or help ``` ### Running Local PowerShell Scripts You can run a local PowerShell script on the remote host using the `runps` command. This will read the contents of the specified PowerShell script file and execute it on the remote machine. ```bash evil-winrm-py PS C:\Users\Administrator\Documents> runps .ps1 ``` ### Loading Local DLLs as PowerShell Modules You can load a local DLL file as a module on the remote host using the `loaddll` command. This will upload the specified DLL file in-memory and load it as a module. Note that this uses .NET's Reflection to load the DLL, so it may not work with all DLL files. This can be helpful when using tools like [ADModule](https://github.com/samratashok/ADModule). These Commands/Commandlets will be added to Command Suggestions so you can use them directly using the `Tab` key for auto-completion. ```bash evil-winrm-py PS C:\Users\Administrator\Documents> loaddll .dll ``` ### Executing Local EXEs on the Remote Host You can upload and execute a local EXE file on the remote host using the `runexe` command. This will upload the specified EXE file in-memory and execute it with optional arguments. Note that this uses .NET's Reflection to load and execute the EXE, so it may not work with all EXE files. This can be helpful when using tools present in [SharpCollection](https://github.com/Flangvik/SharpCollection). ```bash evil-winrm-py PS C:\Users\Administrator\Documents> runexe .exe [args] ``` ## Additional Options ### Using No Colors If you want to disable colored output in the terminal, you can use the `--no-colors` option. This is useful for logging or when your terminal does not support colors. ```bash evil-winrm-py -i -u -p --no-colors ``` ### Using No Password Prompt ```bash evil-winrm-py -i -u --no-pass ``` ================================================ FILE: evil_winrm_py/__init__.py ================================================ __version__ = "1.6.0" ================================================ FILE: evil_winrm_py/_ps/__init__.py ================================================ ================================================ FILE: evil_winrm_py/_ps/exec.ps1 ================================================ # This script is part of evil-winrm-py project https://github.com/adityatelange/evil-winrm-py # It runs a dotnet executable in memory from a Base64 string and captures its output. # --- Define Parameters --- param ( [Parameter(Mandatory=$true, Position=0)] [string]$Base64Exe, # Base64 encoded executable content [Parameter(Mandatory=$false, Position=1)] [string[]]$Args # Arguments to pass to the executable ) # --- Decode Base64 and Load Assembly --- $exeBytes = [System.Convert]::FromBase64String($Base64Exe) $assembly = [System.Reflection.Assembly]::Load($exeBytes) # --- Execute the Entry Point and Capture Output --- $entryPoint = $assembly.EntryPoint if ($entryPoint -eq $null) { Write-Error "Error: The provided executable does not have an entry point." exit 1 } # Redirect STDOUT and STDERR $stdout = New-Object System.IO.StringWriter $stderr = New-Object System.IO.StringWriter $oldOut = [Console]::Out $oldErr = [Console]::Error [Console]::SetOut($stdout) [Console]::SetError($stderr) # Invoke the entry point method and pass arguments if any $result = $entryPoint.Invoke($null, @(,($Args))) # Capture outputs $stdOutContent = $stdout.ToString() $stdErrContent = $stderr.ToString() # Log outputs as Plain text if ($stdOutContent -ne "") { Write-Output $stdOutContent.Trim() } if ($stdErrContent -ne "") { Write-Error $stdErrContent.Trim() } if ($result) { Write-Output $result.Trim() } # Restore original STDOUT and STDERR [Console]::SetOut($oldOut) [Console]::SetError($oldErr) ================================================ FILE: evil_winrm_py/_ps/fetch.ps1 ================================================ # This script is part of evil-winrm-py project https://github.com/adityatelange/evil-winrm-py # It reads a file in chunks, converts each chunk to Base64, and outputs metadata and chunks as JSON. # --- Define Parameters --- param ( [Parameter(Mandatory=$true, Position=0)] [string]$FilePath ) # --- Configuration --- $bufferSize = 65536 # Read in 64 KB chunks # --- Variables for disposal --- $fileStream = $null # Initialize as null to handle disposal $fileInfo = $null # To store file information # --- Pre-check and initial metadata --- if (-not (Test-Path -Path $FilePath -PathType Leaf)) { [PSCustomObject]@{ Type = "Error" Message = "Error: The specified file path does not exist or is not a file: '$FilePath'" } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout exit 1 # Exit the script with an error code } try { $fileInfo = Get-Item -Path $FilePath $fileSize = $fileInfo.Length # Total file size in bytes $totalChunks = [System.Math]::Ceiling($fileSize / $bufferSize) # Calculate total chunks, rounding up $fileHash = (Get-FileHash -Path $FilePath -Algorithm MD5).Hash # Output initial file metadata as JSON [PSCustomObject]@{ Type = "Metadata" FilePath = $FilePath FileSize = $fileSize ChunkSize = $bufferSize TotalChunks = $totalChunks FileHash = $fileHash FileName = $fileInfo.Name } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout } catch { [PSCustomObject]@{ Type = "Error" Message = "Error getting file information or outputting metadata: $($_.Exception.Message)" } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout exit 1 } # --- File Reading and Processing for Base64 Chunks --- try { $fileStream = New-Object System.IO.FileStream($FilePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) $buffer = New-Object byte[] $bufferSize $chunkCounter = 0 $totalBytesRead = 0 while (($bytesRead = $fileStream.Read($buffer, 0, $buffer.Length)) -gt 0) { $chunkCounter++ # Increment chunk counter # 1. Convert bytes to Base64 $chunkBytes = New-Object byte[] $bytesRead [System.Array]::Copy($buffer, 0, $chunkBytes, 0, $bytesRead) $base64Chunk = [System.Convert]::ToBase64String($chunkBytes) # 2. Output the Base64 chunk as a JSON object [PSCustomObject]@{ Type = "Chunk" ChunkNumber = $chunkCounter Base64Data = $base64Chunk } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout $totalBytesRead += $bytesRead } } catch { [PSCustomObject]@{ Type = "Error" Message = "Error during Base64 chunk processing: $($_.Exception.Message)" } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout } finally { if ($fileStream) { $fileStream.Dispose() } } ================================================ FILE: evil_winrm_py/_ps/loaddll.ps1 ================================================ # This script is part of evil-winrm-py project https://github.com/adityatelange/evil-winrm-py # It loads a dll in memory as a PowerShell module from a Base64 string and lists its exported functions. # --- Define Parameters --- param ( [Parameter(Mandatory=$true, Position=0)] [string]$Base64Dll # Base64 encoded dll content ) # --- Decode Base64 and Load Assembly --- $dllBytes = [System.Convert]::FromBase64String($Base64Dll) $assembly = [System.Reflection.Assembly]::Load($dllBytes) # --- Output Assembly Metadata --- $assemblyName = $assembly.GetName().Name [PSCustomObject]@{ Type = "Metadata" Name = $assemblyName } | ConvertTo-Json -Compress | Write-Output # --- Import Modules from the Assembly --- try { Import-Module -Assembly $assembly -ErrorAction Stop } catch { [PSCustomObject]@{ Type = "Error" Message = "Failed to import module: $($_.Exception.Message)" } | ConvertTo-Json -Compress | Write-Output exit 1 } # --- List Exported Functions --- $loadedModule = Get-Module -Name "dynamic_code_module_$assemblyName*" if ($loadedModule -ne $null) { $exportedFunctions = $loadedModule.ExportedCommands.Keys [PSCustomObject]@{ Type = "Metadata" Funcs = $exportedFunctions } | ConvertTo-Json -Compress | Write-Output } else { [PSCustomObject]@{ Type = "Error" Message = "Could not find the loaded module." } | ConvertTo-Json -Compress | Write-Output exit 1 } ================================================ FILE: evil_winrm_py/_ps/send.ps1 ================================================ # This script is part of evil-winrm-py project https://github.com/adityatelange/evil-winrm-py # It reads a Base64 encoded chunk of bytes, writes it to a file, and optionally appends to an existing file. # It also calculates the MD5 hash of the file after writing if required. # --- Define Parameters --- param ( [Parameter(Mandatory=$true, Position=0)] [string]$Base64Chunk, # The Base64 encoded chunk of bytes [Parameter(Mandatory=$true, Position=1)] [int]$ChunkType = 0, # 0 for new file, 1 for appending to existing file [Parameter(Mandatory=$false, Position=2)] [string]$TempFilePath, # The temporary file path to write/append the bytes to [Parameter(Mandatory=$false, Position=3)] [string]$FilePath, # The file path to write/append the bytes to [Parameter(Mandatory=$false, Position=4)] [string]$FileHash # The MD5 hash of the file ) # --- Variables for disposal --- $fileStream = $null # Initialize as null for safety in finally block # --- Pre-checks --- # IF chunkPosition is 0 or 3 its a new file if ($ChunkType -eq 0 -or $ChunkType -eq 3) { # If this is the first chunk, create a unique temporary file path $TempFilePath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName()) # Output initial file metadata as JSON [PSCustomObject]@{ Type = "Metadata" TempFilePath = $TempFilePath } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout } # --- Main Logic --- try { # Decode the Base64 chunk into bytes $chunkBytes = [System.Convert]::FromBase64String($Base64Chunk) # Open the file in Append mode. # If the file doesn't exist, it will be created. # If it exists, new bytes will be added to the end. $fileStream = New-Object System.IO.FileStream( $TempFilePath, [System.IO.FileMode]::Append, # Use Append mode [System.IO.FileAccess]::Write ) # Write the decoded bytes to the file # $ChunkSize here is critical and should be the actual length of $chunkBytes for this specific chunk $fileStream.Write($chunkBytes, 0, $chunkBytes.Length) # Use $chunkBytes.Length for safety $fileStream.Close() } catch { $FullExceptionMessage = "$($_.Exception.GetType().FullName): $($_.Exception.Message)" [PSCustomObject]@{ Type = "Error" Message = "Error processing chunk or writing to file: $FullExceptionMessage" } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout } finally { # Ensure the file stream is closed to release the file lock and flush buffers if ($fileStream) { $fileStream.Dispose() } } # --- Calculate checksum --- # Caculate the MD5 hash of the file after writing if ChunkType is 1 or 3 if ($ChunkType -eq 1 -or $ChunkType -eq 3) { try { if ($TempFilePath) { # If a file hash is provided, verify it $calculatedHash = (Get-FileHash -Path $TempFilePath -Algorithm MD5).Hash if ($calculatedHash -eq $FileHash) { # If the hash matches, move the temporary file to the final destination [System.IO.File]::Delete($FilePath) [System.IO.File]::Move($TempFilePath, $FilePath) $fileInfo = Get-Item -Path $FilePath $fileSize = $fileInfo.Length # Total file size in bytes $fileHash = (Get-FileHash -Path $FilePath -Algorithm MD5).Hash # Output initial file metadata as JSON [PSCustomObject]@{ Type = "Metadata" FilePath = $FilePath FileSize = $fileSize FileHash = $fileHash FileName = $fileInfo.Name } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout } else { [PSCustomObject]@{ Type = "Error" Message = "File hash mismatch. Expected: $FileHash, Calculated: $calculatedHash" } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout } } else { [PSCustomObject]@{ Type = "Error" Message = "File hash not provided for verification." } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout } } catch { $FullExceptionMessage = "$($_.Exception.GetType().FullName): $($_.Exception.Message)" [PSCustomObject]@{ Type = "Error" Message = "Error processing chunk or writing to file: $FullExceptionMessage" } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout } } ================================================ FILE: evil_winrm_py/evil_winrm_py.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ evil-winrm-py https://github.com/adityatelange/evil-winrm-py """ import argparse import base64 import hashlib import json import logging import os import re import shutil import signal import sys import tempfile import textwrap import time import traceback from importlib import resources from pathlib import Path from prompt_toolkit import PromptSession, prompt from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.document import Document from prompt_toolkit.filters import has_completions from prompt_toolkit.formatted_text import ANSI from prompt_toolkit.history import FileHistory from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.shortcuts import clear from pypsrp.complex_objects import PSInvocationState from pypsrp.exceptions import AuthenticationError, WinRMTransportError, WSManFaultError from pypsrp.powershell import PowerShell, RunspacePool from requests.exceptions import ConnectionError from spnego.exceptions import NoCredentialError, OperationNotAvailableError, SpnegoError from tqdm import tqdm # check if kerberos is installed try: from gssapi.creds import Credentials as GSSAPICredentials from gssapi.exceptions import ExpiredCredentialsError, MissingCredentialsError from gssapi.raw import Creds as RawCreds from krb5._exceptions import Krb5Error is_kerb_available = True except ImportError: is_kerb_available = False # If kerberos is not available, define a dummy exception class Krb5Error(Exception): pass from evil_winrm_py import __version__ from evil_winrm_py.pypsrp_ewp.wsman import WSManEWP # --- Constants --- LOG_PATH = Path.cwd().joinpath("evil_winrm_py.log") HISTORY_FILE = Path.home().joinpath(".evil_winrm_py_history") HISTORY_LENGTH = 1000 MENU_COMMANDS = { "services": { "syntax": "services", "info": "Show the running services (except system services)", }, "upload": { "syntax": "upload ", "info": "Upload a file", }, "download": { "syntax": "download ", "info": "Download a file", }, "loadps": { "syntax": "loadps .ps1", "info": "Load PowerShell functions from a local script", }, "runps": { "syntax": "runps .ps1", "info": "Run a local PowerShell script on the remote host", }, "loaddll": { "syntax": "loaddll .dll", "info": "Load a local DLL (in-memory) as a module on the remote host", }, "runexe": { "syntax": "runexe .exe [args]", "info": "Upload and execute (in-memory) a local EXE on the remote host", }, "menu": { "syntax": "menu", "info": "Show this menu", }, "clear": { "syntax": "clear, cls", "info": "Clear the screen", }, "exit": { "syntax": "exit", "info": "Exit the shell", }, } COMMAND_SUGGESTIONS = [] # --- Colors --- # ANSI escape codes for colored output RESET = "\033[0m" RED = "\033[31m" GREEN = "\033[32m" YELLOW = "\033[33m" BLUE = "\033[34m" MAGENTA = "\033[35m" CYAN = "\033[36m" BOLD = "\033[1m" # --- Logging Setup --- log = logging.getLogger(__name__) # --- Helper Functions --- class DelayedKeyboardInterrupt: """ A context manager to delay the handling of a SIGINT (Ctrl+C) signal until the enclosed block of code has completed execution. This is useful for ensuring that critical sections of code are not interrupted by a keyboard interrupt, while still allowing the signal to be handled after the block finishes. """ def __enter__(self): self.signal_received = False self.old_handler = signal.getsignal(signal.SIGINT) def handler(sig, frame): print(RED + "\n[-] Caught Ctrl+C. Stopping current command..." + RESET) self.signal_received = (sig, frame) signal.signal(signal.SIGINT, handler) def __exit__(self, type, value, traceback): signal.signal(signal.SIGINT, self.old_handler) if self.signal_received: # raise the signal after the task is done self.old_handler(*self.signal_received) def run_ps_cmd(r_pool: RunspacePool, command: str) -> tuple[str, list, bool]: """Runs a PowerShell command and returns the output, streams, and error status.""" log.info("Executing command: {}".format(command)) ps = PowerShell(r_pool) ps.add_cmdlet("Invoke-Expression").add_parameter("Command", command) ps.add_cmdlet("Out-String").add_parameter("Stream") ps.invoke() return "\n".join(ps.output), ps.streams, ps.had_errors def get_prompt(r_pool: RunspacePool) -> str: """Returns the prompt string for the interactive shell.""" output, streams, had_errors = run_ps_cmd( r_pool, "$pwd.Path" ) # Get current working directory if not had_errors: return f"{RED}evil-winrm-py{RESET} {YELLOW}{BOLD}PS{RESET} {output}> " return "PS ?> " # Fallback prompt def show_menu() -> None: """Displays the help menu for interactive commands.""" print(BOLD + "\nMenu:" + RESET) for command in MENU_COMMANDS.values(): print(f"{CYAN}[+] {command['syntax']:<55} - {command['info']}{RESET}") print("Note: Use absolute paths for upload/download for reliability.\n") def get_directory_and_partial_name(text: str, sep: str) -> tuple[str, str]: """ Parses the input text to find the directory prefix and the partial name. """ if sep not in ["\\", "/"]: raise ValueError("Separator must be either '\\' or '/'") # Find the last unquoted slash or backslash last_sep_index = text.rfind(sep) if last_sep_index == -1: # No separator found, the whole text is the partial name in the current directory directory_prefix = "" partial_name = text else: split_at = last_sep_index + 1 directory_prefix = text[:split_at] partial_name = text[split_at:] return directory_prefix, partial_name def _ps_single_quote(value: str) -> str: """Wraps a value in single quotes for PowerShell, escaping embedded quotes.""" escaped = value.replace("'", "''") return f"'{escaped}'" def get_remote_path_suggestions( r_pool: RunspacePool, directory_prefix: str, partial_name: str, dirs_only: bool = False, ) -> list[str]: """ Returns a list of remote path suggestions based on the current directory and the partial name entered by the user. """ exp = "FullName" attrs = "" if not re.match(r"^[a-zA-Z]:", directory_prefix): # If the path doesn't start with a drive letter, prepend the current directory pwd, streams, had_errors = run_ps_cmd( r_pool, "$pwd.Path" ) # Get current working directory directory_prefix = f"{pwd}\\{directory_prefix}" exp = "Name" if dirs_only: attrs = "-Attributes Directory" command = f'Get-ChildItem -LiteralPath "{directory_prefix}" -Filter "{partial_name}*" {attrs} -Fo | select -Exp {exp}' ps = PowerShell(r_pool) ps.add_cmdlet("Invoke-Expression").add_parameter("Command", command) ps.add_cmdlet("Out-String").add_parameter("Stream") ps.invoke() return ps.output def get_remote_command_suggestions( r_pool: RunspacePool, command_prefix: str ) -> list[str]: """ Returns a list of remote PowerShell command names (cmdlets/aliases) that start with the provided prefix. """ prefix_literal = _ps_single_quote(command_prefix or "") ps_script = textwrap.dedent( f""" $prefix = {prefix_literal}; if ([string]::IsNullOrEmpty($prefix)) {{ $pattern = '*'; }} else {{ $pattern = "$prefix*"; }} $cmds = Get-Command -Name $pattern -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name; if (-not $cmds) {{ $cmds = Get-Alias -Name $pattern -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name; }} $cmds | Sort-Object -Unique """ ).strip() output, _, had_errors = run_ps_cmd(r_pool, ps_script) if had_errors: return [] suggestions = [line.strip() for line in output.splitlines() if line.strip()] return suggestions def get_local_path_suggestions( directory_prefix: str, partial_name: str, extension: str = None ) -> list[str]: """ Returns a list of local path suggestions based on path entered by the user. Optionally filters files by extension (e.g., ".ps1"). """ suggestions = [] # Expand the tilde to the user's home directory home = str(Path.home()) try: entries = Path(directory_prefix).expanduser().iterdir() for entry in entries: if entry.match(f"{partial_name}*"): if entry.is_dir(): entry = ( f"{entry}{os.sep}" # Append a trailing slash for directories ) if directory_prefix.startswith("~"): # If the directory prefix starts with ~, replace home with ~ entry = str(entry).replace(home, "~", 1) suggestions.append(str(entry)) else: if (extension is None) or ( entry.suffix.lower() == extension.lower() ): if directory_prefix.startswith("~"): # If the directory prefix starts with ~, replace home with ~ entry = str(entry).replace(home, "~", 1) suggestions.append(str(entry)) except (FileNotFoundError, NotADirectoryError, PermissionError): pass finally: if extension: # Sort suggestions alphabetically, prioritizing those that match the extension return sorted(suggestions, key=lambda x: not x.endswith(extension)) return suggestions class CommandPathCompleter(Completer): """ Completer for command paths in the interactive shell. This completer suggests command names based on the user's input. """ def __init__(self, r_pool: RunspacePool): self.r_pool = r_pool def get_completions(self, document: Document, complete_event): dirs_only = False # Whether to suggest only directories text_before_cursor = document.text_before_cursor.lstrip() tokens = text_before_cursor.split(maxsplit=1) if not tokens: # Empty input, suggest all commands for cmd_sugg in list(MENU_COMMANDS.keys()) + COMMAND_SUGGESTIONS: yield Completion(cmd_sugg, start_position=0, display=cmd_sugg) return command_typed_part = tokens[0] # Handle .\name or ./name as first-token paths (run from current remote directory) if command_typed_part.startswith(".\\") or command_typed_part.startswith("./"): path_being_completed = command_typed_part # strip surrounding quotes if any if path_being_completed.startswith('"') and path_being_completed.endswith( '"' ): path_being_completed = path_being_completed.strip('"') directory_prefix, partial_name = get_directory_and_partial_name( path_being_completed, sep="\\" ) suggestions = get_remote_path_suggestions( self.r_pool, directory_prefix, partial_name ) for sugg_path in suggestions: text_to_insert_in_prompt = f".\\" + sugg_path if " " in sugg_path: text_to_insert_in_prompt = f'& ".\\{sugg_path}"' yield Completion( text_to_insert_in_prompt, start_position=-len(command_typed_part), display=sugg_path, ) return # Case 1: Completing the command name itself # There's only one token and no trailing space. if len(tokens) == 1 and not text_before_cursor.endswith(" "): # User is typing the command, -> "downl" seen_commands = set() for cmd_sugg in list(MENU_COMMANDS.keys()) + COMMAND_SUGGESTIONS: if cmd_sugg.startswith(command_typed_part): seen_commands.add(cmd_sugg.lower()) yield Completion( cmd_sugg + " ", # Full suggested command start_position=-len( command_typed_part ), # Replace the typed part display=cmd_sugg, ) remote_cmds = get_remote_command_suggestions( self.r_pool, command_typed_part ) lower_prefix = command_typed_part.lower() for remote_cmd in remote_cmds: cmd_lower = remote_cmd.lower() if lower_prefix and not cmd_lower.startswith(lower_prefix): continue if cmd_lower in seen_commands: continue seen_commands.add(cmd_lower) yield Completion( remote_cmd + " ", start_position=-len(command_typed_part), display=remote_cmd, ) return # Case 2: Completing a path argument path_typed_segment = "" # What the user has typed for the current path argument if len(tokens) == 2: path_typed_segment = tokens[1] actual_command_name = command_typed_part.strip().lower() args = quoted_command_split(path_typed_segment.strip()) suggestions = [] current_arg_text_being_completed = "" directory_prefix = partial_name = "" if actual_command_name == "upload": # syntax: upload num_args_present = len(args) if num_args_present == 0: # User typed "upload " # Completing the 1st argument (local_path), currently empty current_arg_text_being_completed = "" directory_prefix, partial_name = get_directory_and_partial_name( current_arg_text_being_completed, sep=os.sep ) suggestions = get_local_path_suggestions(directory_prefix, partial_name) elif num_args_present == 1: # We have one argument part, e.g., "upload arg1" or "upload local_path " if path_typed_segment.endswith(" "): # 1st argument (local_path) is complete # Completing the 2nd argument (remote_path), currently empty current_arg_text_being_completed = "" directory_prefix, partial_name = get_directory_and_partial_name( current_arg_text_being_completed, sep="\\" ) suggestions = get_remote_path_suggestions( self.r_pool, directory_prefix, partial_name ) else: # Still completing the 1st argument (local_path), e.g., "upload arg1" current_arg_text_being_completed = path_being_completed = args[0] if path_being_completed.startswith('"'): path_being_completed = current_arg_text_being_completed.strip( '"' ) directory_prefix, partial_name = get_directory_and_partial_name( path_being_completed, sep=os.sep ) suggestions = get_local_path_suggestions( directory_prefix, partial_name ) elif num_args_present == 2: # We have two argument parts # e.g., "upload local_path arg2" or "upload local_path remote_path " if path_typed_segment.endswith(" "): # 2nd argument (remote_path) is complete. No more suggestions for "upload". pass else: # Completing the 2nd argument (remote_path), e.g., "upload local_path arg2" current_arg_text_being_completed = path_being_completed = args[1] if path_being_completed.startswith('"'): path_being_completed = current_arg_text_being_completed.strip( '"' ) directory_prefix, partial_name = get_directory_and_partial_name( path_being_completed, sep="\\" ) suggestions = get_remote_path_suggestions( self.r_pool, directory_prefix, partial_name ) else: # More than 2 arguments, e.g., "upload local_path remote_path extra_arg" pass elif actual_command_name == "download": # syntax: download num_args_present = len(args) if num_args_present == 0: # User typed "download " # Completing 1st arg (remote_path), empty current_arg_text_being_completed = "" directory_prefix, partial_name = get_directory_and_partial_name( current_arg_text_being_completed, sep="\\" ) suggestions = get_remote_path_suggestions( self.r_pool, directory_prefix, partial_name ) elif num_args_present == 1: # We have "download arg1" or "download local_path " if path_typed_segment.endswith(" "): # First arg (remote_path) is complete. Completing 2nd arg (local_path), empty. current_arg_text_being_completed = "" directory_prefix, partial_name = get_directory_and_partial_name( current_arg_text_being_completed, sep=os.sep ) suggestions = get_local_path_suggestions( directory_prefix, partial_name ) else: # Still completing 1st arg (remote_path) current_arg_text_being_completed = path_being_completed = args[0] if path_being_completed.startswith('"'): path_being_completed = current_arg_text_being_completed.strip( '"' ) directory_prefix, partial_name = get_directory_and_partial_name( path_being_completed, sep="\\" ) suggestions = get_remote_path_suggestions( self.r_pool, directory_prefix, partial_name ) elif num_args_present == 2: # We have two argument parts # e.g., "download remote_path arg2" or "download remote_path local_path " if path_typed_segment.endswith(" "): # 2nd argument (local_path) is complete. No more suggestions for "download". pass else: # Completing 2nd arg (local_path) current_arg_text_being_completed = path_being_completed = args[1] if path_being_completed.startswith('"'): path_being_completed = current_arg_text_being_completed.strip( '"' ) directory_prefix, partial_name = get_directory_and_partial_name( path_being_completed, sep=os.sep ) suggestions = get_local_path_suggestions( directory_prefix, partial_name ) else: # More than 2 arguments, e.g., "download remote_path local_path extra_arg" pass elif actual_command_name in ["loadps", "runps"]: # syntax: loadps num_args_present = len(args) if num_args_present == 0: # User typed "loadps " # Completing the 1st argument (local_path), currently empty current_arg_text_being_completed = "" directory_prefix, partial_name = get_directory_and_partial_name( current_arg_text_being_completed, sep=os.sep ) suggestions = get_local_path_suggestions( directory_prefix, partial_name, extension=".ps1" ) elif num_args_present == 1: # We have "loadps arg1" or "loadps local_path " if path_typed_segment.endswith(" "): # 1st argument (local_path) is complete, currently empty. current_arg_text_being_completed = "" directory_prefix, partial_name = get_directory_and_partial_name( current_arg_text_being_completed, sep=os.sep ) suggestions = get_local_path_suggestions( directory_prefix, partial_name, extension=".ps1" ) else: # Still completing the 1st argument (local_path) current_arg_text_being_completed = path_being_completed = args[0] if path_being_completed.startswith('"'): path_being_completed = current_arg_text_being_completed.strip( '"' ) directory_prefix, partial_name = get_directory_and_partial_name( path_being_completed, sep=os.sep ) suggestions = get_local_path_suggestions( directory_prefix, partial_name, extension=".ps1" ) else: # More than 1 argument, e.g., "loadps local_path extra_arg" pass elif actual_command_name in ["loaddll"]: # syntax: loaddll num_args_present = len(args) if num_args_present == 0: # User typed "loaddll " # Completing the 1st argument (local_path), currently empty current_arg_text_being_completed = "" directory_prefix, partial_name = get_directory_and_partial_name( current_arg_text_being_completed, sep=os.sep ) suggestions = get_local_path_suggestions( directory_prefix, partial_name, extension=".dll" ) elif num_args_present == 1: # We have "loaddll arg1" or "loaddll local_path " if path_typed_segment.endswith(" "): # 1st argument (local_path) is complete, currently empty. current_arg_text_being_completed = "" directory_prefix, partial_name = get_directory_and_partial_name( current_arg_text_being_completed, sep=os.sep ) suggestions = get_local_path_suggestions( directory_prefix, partial_name, extension=".dll" ) else: # Still completing the 1st argument (local_path) current_arg_text_being_completed = path_being_completed = args[0] if path_being_completed.startswith('"'): path_being_completed = current_arg_text_being_completed.strip( '"' ) directory_prefix, partial_name = get_directory_and_partial_name( path_being_completed, sep=os.sep ) suggestions = get_local_path_suggestions( directory_prefix, partial_name, extension=".dll" ) else: # More than 1 argument, e.g., "loaddll local_path extra_arg" pass elif actual_command_name in ["runexe"]: # syntax: runexe num_args_present = len(args) if num_args_present == 0: # User typed "runexe " # Completing the 1st argument (local_path), currently empty current_arg_text_being_completed = "" directory_prefix, partial_name = get_directory_and_partial_name( current_arg_text_being_completed, sep=os.sep ) suggestions = get_local_path_suggestions( directory_prefix, partial_name, extension=".exe" ) elif num_args_present == 1: # We have "runexe arg1" or "runexe local_path " if path_typed_segment.endswith(" "): # 1st argument (local_path) is complete, currently empty. current_arg_text_being_completed = "" directory_prefix, partial_name = get_directory_and_partial_name( current_arg_text_being_completed, sep=os.sep ) suggestions = get_local_path_suggestions( directory_prefix, partial_name, extension=".exe" ) else: # Still completing the 1st argument (local_path) current_arg_text_being_completed = path_being_completed = args[0] if path_being_completed.startswith('"'): path_being_completed = current_arg_text_being_completed.strip( '"' ) directory_prefix, partial_name = get_directory_and_partial_name( path_being_completed, sep=os.sep ) suggestions = get_local_path_suggestions( directory_prefix, partial_name, extension=".exe" ) else: # More than 1 argument, e.g., "runexe local_path extra_arg" pass else: if actual_command_name == "cd": dirs_only = True current_arg_text_being_completed = path_being_completed = path_typed_segment if path_being_completed.startswith('"'): path_being_completed = current_arg_text_being_completed.strip('"') directory_prefix, partial_name = get_directory_and_partial_name( path_being_completed, sep="\\" ) suggestions = get_remote_path_suggestions( self.r_pool, directory_prefix, partial_name, dirs_only ) for sugg_path in suggestions: # If the path doesn't start with a drive letter, prepend the directory_prefix if ( not re.match(r"^[a-zA-Z]:", directory_prefix) and directory_prefix and directory_prefix.endswith("\\") ): sugg_path = f"{directory_prefix}{sugg_path}" text_to_insert_in_prompt = sugg_path if " " in sugg_path: # If the path contains spaces, quote it text_to_insert_in_prompt = f'"{sugg_path}"' yield Completion( text_to_insert_in_prompt, start_position=-len( current_arg_text_being_completed ), # Use the length of quoted part display=sugg_path, ) def get_ps_script(script_name: str) -> str: """ Returns the content of a PowerShell script from the package resources. """ try: with resources.path("evil_winrm_py._ps", script_name) as script_path: return script_path.read_text() except FileNotFoundError: print(RED + f"[-] Script {script_name} not found." + RESET) log.error(f"Script {script_name} not found.") return "" def quoted_command_split(command: str) -> list[str]: """ Splits a command string into parts, respecting quoted strings. This is useful for handling paths with spaces or special characters. """ actual_command_parts = [] continuation = False cursor = 0 command_parts = command.split(" ") for part in command_parts: if not part: continue if continuation: actual_command_parts[cursor] = actual_command_parts[cursor] + " " + part if part.endswith('"'): continuation = False cursor += 1 else: if part.startswith('"'): actual_command_parts += [part] continuation = True elif part.find('"') != -1: # #TODO: decide later how to handle this case pass else: actual_command_parts += [part] cursor += 1 return actual_command_parts def download_file(r_pool: RunspacePool, remote_path: str, local_path: str) -> None: ps = PowerShell(r_pool) script = get_ps_script("fetch.ps1") ps.add_script(script) ps.add_parameter("FilePath", remote_path) ps.begin_invoke() ts = int(time.time()) tmp_file_path = Path(tempfile.gettempdir()) / f"evil-winrm-py.file_{ts}.tmp" try: # Create a temporary file to store the downloaded data with open(tmp_file_path, "ab+") as bin: cursor = 0 metadata = {} while ps.state == PSInvocationState.RUNNING: with DelayedKeyboardInterrupt(): ps.poll_invoke() output = ps.output if cursor == 0: line = json.loads(output[0]) if line["Type"] == "Error": print(RED + f"[-] Error: {line['Message']}" + RESET) log.error(f"Error: {line['Message']}") return elif line["Type"] == "Metadata": metadata = line pbar = tqdm( total=metadata["FileSize"], unit="B", unit_scale=True, unit_divisor=1024, desc=f"Downloading {remote_path}", dynamic_ncols=True, mininterval=0.1, ) for line in output[cursor:]: line = json.loads(line) if line["Type"] == "Chunk": Base64Data = line["Base64Data"] chunk = base64.b64decode(Base64Data) bin.write(chunk) pbar.update(metadata["ChunkSize"]) if line["Type"] == "Error": print(RED + f"[-] Error: {line['Message']}" + RESET) log.error(f"Error: {line['Message']}") return cursor = len(output) pbar.close() bin.close() if ps.had_errors: if ps.streams.error: for error in ps.streams.error: print(error) except KeyboardInterrupt: if "pbar" in locals() and pbar: pbar.leave = ( False # Make the progress bar disappear on close after interrupt ) pbar.close() Path(tmp_file_path).unlink(missing_ok=True) if ps.state == PSInvocationState.RUNNING: log.info("Stopping command execution.") ps.stop() return # Verify the downloaded file's hash hexdigest = hashlib.md5(open(tmp_file_path, "rb").read()).hexdigest() if metadata["FileHash"].lower() == hexdigest: # If the hash matches, rename the temporary file to the final name tmp_file_path = Path(tmp_file_path) try: shutil.move(tmp_file_path, local_path) except Exception as e: print(RED + f"[-] Error saving file: {e}" + RESET) log.error(f"Error saving file: {e}") return print( GREEN + "[+] File downloaded successfully and saved as: " + local_path + RESET ) log.info("File downloaded successfully and saved as: {}".format(local_path)) else: print(RED + "[-] File hash mismatch. Downloaded file may be corrupted." + RESET) log.error("File hash mismatch. Downloaded file may be corrupted.") def upload_file(r_pool: RunspacePool, local_path: str, remote_path: str) -> None: hexdigest = hashlib.md5(open(local_path, "rb").read()).hexdigest().upper() with open(local_path, "rb") as bin: file_size = Path(local_path).stat().st_size chunk_size_bytes = 65536 # 64 KB total_chunks = (file_size + chunk_size_bytes - 1) // chunk_size_bytes metadata = {"FileHash": ""} # Declare a psuedo metadata pbar = tqdm( total=file_size, unit="B", unit_scale=True, unit_divisor=1024, desc=f"Uploading {local_path}", dynamic_ncols=True, mininterval=0.1, ) try: temp_file_path = "" for i in range(total_chunks): start_offset = i * chunk_size_bytes bin.seek(start_offset) chunk = bin.read(chunk_size_bytes) if not chunk: # End of file break elif i == 0: chunk_type = 0 # First chunk, tells PS script to create file if len(chunk) < chunk_size_bytes: chunk_type = 3 elif i == total_chunks - 1: chunk_type = 1 # Last chunk, tells PS script to calculate hash else: chunk_type = 2 # Intermediate chunk base64_chunk = base64.b64encode(chunk).decode("utf-8") script = get_ps_script("send.ps1") with DelayedKeyboardInterrupt(): ps = PowerShell(r_pool) ps.add_script(script) ps.add_parameter("Base64Chunk", base64_chunk) ps.add_parameter("ChunkType", chunk_type) if chunk_type == 1: # If it's the last chunk, we provide the file path and hash ps.add_parameter("TempFilePath", temp_file_path) ps.add_parameter("FilePath", remote_path) ps.add_parameter("FileHash", hexdigest) elif chunk_type == 2: ps.add_parameter("TempFilePath", temp_file_path) elif chunk_type == 3: ps.add_parameter("FilePath", remote_path) ps.add_parameter("FileHash", hexdigest) ps.begin_invoke() while ps.state == PSInvocationState.RUNNING: ps.poll_invoke() output = ps.output for line in output: line = json.loads(line) if line["Type"] == "Metadata": metadata = line if "TempFilePath" in metadata: temp_file_path = metadata["TempFilePath"] if line["Type"] == "Error": print(RED + f"[-] Error: {line['Message']}" + RESET) log.error(f"Error: {line['Message']}") pbar.leave = False # Make the progress bar disappear on close return if ps.had_errors: if ps.streams.error: for error in ps.streams.error: print(error) if chunk_type == 3: pbar.update(file_size) else: pbar.update(chunk_size_bytes) pbar.close() # Verify the downloaded file's hash if metadata["FileHash"] == hexdigest: print( GREEN + "[+] File uploaded successfully as: " + metadata["FilePath"] + RESET ) log.info( "File uploaded successfully as: {}".format(metadata["FilePath"]) ) else: print( RED + "[-] File hash mismatch. Uploaded file may be corrupted." + RESET ) log.error("File hash mismatch. Uploaded file may be corrupted.") except KeyboardInterrupt: if "pbar" in locals() and pbar: pbar.leave = ( False # Make the progress bar disappear on close after interrupt ) pbar.close() if ps.state == PSInvocationState.RUNNING: log.info("Stopping command execution.") ps.stop() def _read_text_auto_encoding(path) -> str: """ Reads file with enc utf-8-sig, utf-8, utf-16, and latin-1. Tries multiple encodings to read the file and returns the content as a string. Raises UnicodeDecodeError/Exception if all encodings fail. """ text_file_encodings = ["utf-8-sig", "utf-8", "utf-16", "latin-1"] for enc in text_file_encodings: try: with open(path, "r", encoding=enc) as f: text = f.read() log.debug(f"Read '{path}' using encoding {enc}") return text except UnicodeDecodeError: continue except Exception as e: raise raise UnicodeDecodeError("All preferred encodings failed for file: {}".format(path)) def load_ps(r_pool: RunspacePool, local_path: str): ps = PowerShell(r_pool) try: try: script = _read_text_auto_encoding(local_path) except Exception as e: print(RED + f"[-] Error reading ps script file: {e}" + RESET) log.error(f"Error reading ps script file: {e}") return # Remove block comments (<#...#>) to avoid matching commented-out functions content = re.sub(r"<#.*?#>", "", script, flags=re.DOTALL) # Find all function names in the script pattern = r"function\s+([a-zA-Z0-9_-]+)\s*(?={|$)" function_names = re.findall(pattern, content, re.MULTILINE | re.IGNORECASE) ps.add_script(f". {{ {script} }}") # Dot sourcing the script ps.begin_invoke() while ps.state == PSInvocationState.RUNNING: with DelayedKeyboardInterrupt(): ps.poll_invoke() if ps.streams.error: print(RED + "[-] Failed to load PowerShell script." + RESET) log.error(f"Failed to load PowerShell script '{local_path}'.") for error in ps.streams.error: print(RED + error._to_string + RESET) log.error("Error: {}".format(error._to_string)) log.error("\tCategoryInfo: {}".format(error.message)) log.error("\tFullyQualifiedErrorId: {}".format(error.fq_error)) else: print(GREEN + "[+] PowerShell script loaded successfully." + RESET) log.info(f"PowerShell script '{local_path}' loaded successfully.") global COMMAND_SUGGESTIONS # Update the command suggestions with the function names new_suggestions = [] for func in function_names: if func not in COMMAND_SUGGESTIONS: new_suggestions += [func] if new_suggestions: COMMAND_SUGGESTIONS += new_suggestions print( CYAN + "[*] New commands available (use TAB to autocomplete):" + RESET ) print(", ".join(new_suggestions)) except KeyboardInterrupt: if ps.state == PSInvocationState.RUNNING: log.info("Stopping command execution.") ps.stop() def run_ps(r_pool: RunspacePool, local_path: str) -> None: """Runs a local PowerShell script on the remote host.""" ps = PowerShell(r_pool) try: try: script = _read_text_auto_encoding(local_path) except Exception as e: print(RED + f"[-] Error reading ps script file: {e}" + RESET) log.error(f"Error reading ps script file: {e}") return ps.add_script(script) ps.begin_invoke() cursor = 0 while ps.state == PSInvocationState.RUNNING: with DelayedKeyboardInterrupt(): ps.poll_invoke() output = ps.output for line in output[cursor:]: print(line) cursor = len(output) if ps.streams.error: print(RED + "[-] Failed to run PowerShell script." + RESET) log.error(f"Failed to run PowerShell script '{local_path}'.") for error in ps.streams.error: print(RED + error._to_string + RESET) log.error("Error: {}".format(error._to_string)) log.error("\tCategoryInfo: {}".format(error.message)) log.error("\tFullyQualifiedErrorId: {}".format(error.fq_error)) else: print(GREEN + "[+] PowerShell script ran successfully." + RESET) log.info(f"PowerShell script '{local_path}' ran successfully.") except KeyboardInterrupt: if ps.state == PSInvocationState.RUNNING: log.info("Stopping command execution.") ps.stop() def load_dll(r_pool: RunspacePool, local_path: str) -> None: """Uploads in-memory and loads a local DLL on the remote host, then invokes a specified function.""" ps = PowerShell(r_pool) try: with open(local_path, "rb") as dll_file: dll_data = dll_file.read() base64_dll = base64.b64encode(dll_data).decode("utf-8") script = get_ps_script("loaddll.ps1") ps.add_script(script) ps.add_parameter("Base64Dll", base64_dll) ps.begin_invoke() cursor = 0 name = "" while ps.state == PSInvocationState.RUNNING: with DelayedKeyboardInterrupt(): ps.poll_invoke() output = ps.output for line in output[cursor:]: line = json.loads(line) if line["Type"] == "Error": print(RED + f"[-] Error: {line['Message']}" + RESET) log.error(f"Error: {line['Message']}") return elif line["Type"] == "Metadata": if "Name" in line: name = line["Name"] print(GREEN + f"[+] Loading '{name}' as a module..." + RESET) log.info(f"Loading '{name}' as a module...") elif "Funcs" in line: print( CYAN + "[*] New commands available available (use TAB to autocomplete):" + RESET ) print(", ".join(line["Funcs"])) global COMMAND_SUGGESTIONS new_suggestions = [] for func in line["Funcs"]: if func not in COMMAND_SUGGESTIONS: new_suggestions += [func] if new_suggestions: COMMAND_SUGGESTIONS += new_suggestions cursor = len(output) if ps.streams.error: print(RED + "[-] Failed to load DLL" + RESET) log.error(f"Failed to load DLL '{local_path}'") for error in ps.streams.error: print(RED + error._to_string + RESET) log.error("Error: {}".format(error._to_string)) log.error("\tCategoryInfo: {}".format(error.message)) log.error("\tFullyQualifiedErrorId: {}".format(error.fq_error)) else: print(GREEN + f"[+] DLL '{name}' loaded successfully." + RESET) log.info(f"DLL '{local_path}' loaded successfully.") except KeyboardInterrupt: if ps.state == PSInvocationState.RUNNING: log.info("Stopping command execution.") ps.stop() def run_exe(r_pool: RunspacePool, local_path: str, args: str = "") -> None: """Uploads in-memory and runs a local executable on the remote host.""" ps = PowerShell(r_pool) file_path = Path(local_path) file_size = file_path.stat().st_size print( BLUE + f"[*] Uploading in-memory ({file_size} bytes) and executing..." + RESET ) log.info(f"Uploading in-memory {file_size} bytes and executing...") try: with open(local_path, "rb") as exe_file: exe_data = exe_file.read() base64_exe = base64.b64encode(exe_data).decode("utf-8") script = get_ps_script("exec.ps1") ps.add_script(script) ps.add_parameter("Base64Exe", base64_exe) ps.add_parameter("Args", args.split(" ")) ps.begin_invoke() cursor = 0 while ps.state == PSInvocationState.RUNNING: with DelayedKeyboardInterrupt(): ps.poll_invoke() output = ps.output for line in output[cursor:]: print(line) cursor = len(output) if ps.streams.error: print(RED + "[-] Failed to run executable." + RESET) log.error(f"Failed to run executable '{local_path}'.") for error in ps.streams.error: print(RED + error._to_string + RESET) log.error("Error: {}".format(error._to_string)) log.error("\tCategoryInfo: {}".format(error.message)) log.error("\tFullyQualifiedErrorId: {}".format(error.fq_error)) else: print(GREEN + "[+] Executable ran successfully." + RESET) log.info(f"Executable '{local_path}' ran successfully.") except KeyboardInterrupt: if ps.state == PSInvocationState.RUNNING: log.info("Stopping command execution.") ps.stop() def interactive_shell(r_pool: RunspacePool) -> None: """Runs the interactive pseudo-shell.""" log.info("Starting interactive PowerShell session...") # Set up history file if not HISTORY_FILE.exists(): Path(HISTORY_FILE).touch() prompt_history = FileHistory(HISTORY_FILE) prompt_session = PromptSession(history=prompt_history) # Set up command completer completer = CommandPathCompleter(r_pool) # Set up key bindings kb = KeyBindings() @kb.add("enter", filter=has_completions) def _(event): """Accept the highlighted completion without executing the command.""" event.current_buffer.apply_completion( event.current_buffer.complete_state.current_completion or Completion("", 0) ) while True: try: try: prompt_text = ANSI(get_prompt(r_pool)) except (KeyboardInterrupt, EOFError): return command = prompt_session.prompt( prompt_text, completer=completer, complete_while_typing=False, key_bindings=kb, ) if not command: continue # Normalize command input command_lower = str(command).strip().lower() # Check for exit command if command_lower == "exit": log.info("Exiting interactive shell.") return elif command_lower in ["clear", "cls"]: log.info("Clearing the screen.") clear() # Clear the screen continue elif command_lower == "menu": log.info("Displaying menu.") show_menu() continue elif command_lower == "services": log.info("Displaying services.") get_services_command = ( "Get-ItemProperty 'Registry::HKLM\\System\\CurrentControlSet\\Services\\*' -ErrorAction " "SilentlyContinue | Where-Object { $_.ImagePath -and ($_.ImagePath -notmatch 'system') } " "| Select-Object @{n='Service';e={$_.PSChildName}}, @{n='Path';e={$_.ImagePath}}" ) services, streams, had_errors = run_ps_cmd(r_pool, get_services_command) if not services: print(RED + "[-] Can not retrieve service information" + RESET) continue print(services) continue elif command_lower.startswith("download"): command_parts = quoted_command_split(command) if len(command_parts) < 3: print( RED + "[-] Usage: download " + RESET ) continue remote_path = command_parts[1].strip('"') local_path = command_parts[2].strip('"').strip("'") remote_file, streams, had_errors = run_ps_cmd( r_pool, f"(Resolve-Path -Path '{remote_path}').Path" ) if not remote_file: print( RED + f"[-] Remote file '{remote_path}' does not exist or you do not have permission to access it." + RESET ) continue file_name = remote_file.split("\\")[-1] if Path(local_path).expanduser().is_dir() or local_path.endswith( os.sep ): local_path = ( Path(local_path).expanduser().resolve().joinpath(file_name) ) else: local_path = Path(local_path).expanduser().resolve() download_file(r_pool, remote_file, str(local_path)) continue elif command_lower.startswith("upload"): command_parts = quoted_command_split(command) if len(command_parts) < 3: print(RED + "[-] Usage: upload " + RESET) continue local_path = command_parts[1].strip('"').strip("'") remote_path = command_parts[2].strip('"') if not Path(local_path).expanduser().exists(): print( RED + f"[-] Local file '{local_path}' does not exist." + RESET ) continue file_name = local_path.split(os.sep)[-1] if not re.match(r"^[a-zA-Z]:", remote_path): # If the path doesn't start with a drive letter, prepend the current directory pwd, streams, had_errors = run_ps_cmd(r_pool, "$pwd.Path") if remote_path == ".": remote_path = f"{pwd}\\{file_name}" else: remote_path = f"{pwd}\\{remote_path}" if remote_path.endswith("\\"): remote_path = f"{remote_path}{file_name}" upload_file( r_pool, str(Path(local_path).expanduser().resolve()), remote_path ) continue elif command_lower.startswith("loadps"): command_parts = quoted_command_split(command) if len(command_parts) < 2: print(RED + "[-] Usage: loadps " + RESET) continue local_path = command_parts[1].strip('"') local_path = Path(local_path).expanduser().resolve() if not local_path.exists(): print( RED + f"[-] Local PowerShell script '{local_path}' does not exist." + RESET ) continue elif local_path.suffix.lower() != ".ps1": print( RED + "[-] Please provide a valid PowerShell script file with .ps1 extension." + RESET ) continue load_ps(r_pool, local_path) continue elif command_lower.startswith("runps"): command_parts = quoted_command_split(command) if len(command_parts) < 2: print(RED + "[-] Usage: runps " + RESET) continue local_path = command_parts[1].strip('"') local_path = Path(local_path).expanduser().resolve() if not local_path.exists(): print( RED + f"[-] Local PowerShell script '{local_path}' does not exist." + RESET ) continue elif local_path.suffix.lower() != ".ps1": print( RED + "[-] Please provide a valid PowerShell script file with .ps1 extension." + RESET ) continue run_ps(r_pool, local_path) continue elif command_lower.startswith("loaddll"): command_parts = quoted_command_split(command) if len(command_parts) < 2: print(RED + "[-] Usage: loaddll " + RESET) continue local_path = command_parts[1].strip('"') local_path = Path(local_path).expanduser().resolve() if not local_path.exists(): print(RED + f"[-] Local dll '{local_path}' does not exist." + RESET) continue elif local_path.suffix.lower() != ".dll": print( RED + "[-] Please provide a valid dll file with .dll extension." + RESET ) continue load_dll(r_pool, local_path) continue elif command_lower.startswith("runexe"): command_parts = quoted_command_split(command) if len(command_parts) < 2: print(RED + "[-] Usage: runexe [args]" + RESET) continue local_path = command_parts[1].strip('"') local_path = Path(local_path).expanduser().resolve() if not local_path.exists(): print( RED + f"[-] Local executable '{local_path}' does not exist." + RESET ) continue elif local_path.suffix.lower() != ".exe": print( RED + "[-] Please provide a valid executable file with .exe extension." + RESET ) continue args = " ".join(command_parts[2:]) if len(command_parts) > 2 else "" run_exe(r_pool, local_path, args) continue else: try: ps = PowerShell(r_pool) ps.add_cmdlet("Invoke-Expression").add_parameter("Command", command) ps.add_cmdlet("Out-String").add_parameter("Stream") ps.begin_invoke() log.info("Executing command: {}".format(command)) cursor = 0 while ps.state == PSInvocationState.RUNNING: with DelayedKeyboardInterrupt(): ps.poll_invoke() output = ps.output for line in output[cursor:]: print(line) cursor = len(output) log.info("Command execution completed.") log.info("Output: {}".format("\n".join(output))) if ps.streams.error: for error in ps.streams.error: print(RED + error._to_string + RESET) log.error("Error: {}".format(error._to_string)) log.error("\tCategoryInfo: {}".format(error.message)) log.error( "\tFullyQualifiedErrorId: {}".format(error.fq_error) ) except KeyboardInterrupt: if ps.state == PSInvocationState.RUNNING: log.info("Stopping command execution.") ps.stop() except KeyboardInterrupt: print("\nCaught Ctrl+C. Type 'exit' or press Ctrl+D to exit.") continue # Allow user to continue or type exit except EOFError: return # Exit on Ctrl+D # --- Main Function --- def main(): print( """ _ _ _ _____ _(_| |_____ __ _(_)_ _ _ _ _ __ ___ _ __ _ _ / -_\\ V | | |___\\ V V | | ' \\| '_| ' |___| '_ | || | \\___|\\_/|_|_| \\_/\\_/|_|_||_|_| |_|_|_| | .__/\\_, | |_| |__/ v{}\n""".format( __version__ ) ) parser = argparse.ArgumentParser( epilog="For more information about this project, visit https://github.com/adityatelange/evil-winrm-py" "\nFor user guide, visit https://github.com/adityatelange/evil-winrm-py/blob/main/docs/usage.md", formatter_class=argparse.RawTextHelpFormatter, ) parser.add_argument( "-i", "--ip", required=True, help="remote host IP or hostname", ) parser.add_argument("-u", "--user", help="username") parser.add_argument("-p", "--password", help="password") parser.add_argument("-H", "--hash", help="nthash") parser.add_argument( "--priv-key-pem", help="local path to private key PEM file", ) parser.add_argument( "--cert-pem", help="local path to certificate PEM file", ) parser.add_argument("--uri", default="wsman", help="wsman URI (default: /wsman)") parser.add_argument( "--ua", default="Microsoft WinRM Client", help='user agent for the WinRM client (default: "Microsoft WinRM Client")', ) parser.add_argument( "--port", type=int, default=5985, help="remote host port (default 5985)" ) if is_kerb_available: parser.add_argument( "--spn-prefix", help="specify spn prefix", ) parser.add_argument( "--spn-hostname", help="specify spn hostname", ) parser.add_argument( "-k", "--kerberos", action="store_true", help="use kerberos authentication" ) parser.add_argument( "--no-pass", action="store_true", help="do not prompt for password" ) parser.add_argument("--ssl", action="store_true", help="use ssl") parser.add_argument("--log", action="store_true", help="log session to file") parser.add_argument("--debug", action="store_true", help="enable debug logging") parser.add_argument("--no-colors", action="store_true", help="disable colors") parser.add_argument( "--version", action="version", version=__version__, help="show version" ) args = parser.parse_args() # Set Default values auth = "ntlm" # this can be 'negotiate' encryption = "auto" username = args.user # --- Run checks on provided arguments --- if args.no_colors: global RESET, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, BOLD RESET = RED = GREEN = YELLOW = BLUE = MAGENTA = CYAN = BOLD = "" if args.cert_pem or args.priv_key_pem: auth = "certificate" encryption = "never" args.ssl = True args.no_pass = True if not args.cert_pem or not args.priv_key_pem: print( RED + "[-] Both cert.pem and priv-key.pem must be provided for certificate authentication." + RESET ) sys.exit(1) if args.hash and args.password: print(RED + "[-] You cannot use both password and hash." + RESET) sys.exit(1) if args.hash: ntlm_hash_pattern = r"^[0-9a-fA-F]{32}$" if re.match(ntlm_hash_pattern, args.hash): args.password = "00000000000000000000000000000000:{}".format(args.hash) else: print(RED + "[-] Invalid NTLM hash format." + RESET) sys.exit(1) if args.uri: if args.uri.startswith("/"): args.uri = args.uri.lstrip("/") if args.ssl and (args.port == 5985): args.port = 5986 if args.log or args.debug: level = logging.INFO # Disable all loggers except the root logger if args.debug: print(BLUE + "[*] Debug logging enabled." + RESET) level = logging.DEBUG os.environ["KRB5_TRACE"] = str(LOG_PATH) # Enable Kerberos trace logging else: # Disable all loggers except the root logger for name in logging.root.manager.loggerDict: if not name.startswith("evil_winrm_py"): logging.getLogger(name).disabled = True # Set up logging to a file try: logging.basicConfig( level=level, format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", filename=LOG_PATH, ) print(BLUE + "[*] Logging session to {}".format(LOG_PATH) + RESET) except PermissionError as pe: print( RED + "[-] Permission denied to write to log file '{}'." " Please check the permissions or run with elevated privileges.".format( LOG_PATH ) + RESET ) log.disabled = True else: log.disabled = True # --- Initialize WinRM Session --- log.info("--- Evil-WinRM-Py v{} started ---".format(__version__)) try: if is_kerb_available: if args.kerberos: auth = "kerberos" args.spn_prefix = ( args.spn_prefix or "http" ) # can also be cifs, ldap, HOST if not args.user: try: cred = GSSAPICredentials(RawCreds()) username = cred.name except MissingCredentialsError: print( MAGENTA + "[%] No credentials cache found for Kerberos authentication." + RESET ) sys.exit(1) except ExpiredCredentialsError as ece: print( RED + "[-] The Kerberos credentials have expired. " + RESET ) log.error("Expired credentials error: {}".format(ece)) sys.exit(1) # User needs to set environment variables `KRB5CCNAME` and `KRB5_CONFIG` as per requirements # example: export KRB5CCNAME=/tmp/krb5cc_1000 # example: export KRB5_CONFIG=/etc/krb5.conf elif args.spn_prefix or args.spn_hostname: args.spn_prefix = args.spn_hostname = None # Reset to None print( MAGENTA + "[%] SPN prefix/hostname is only used with Kerberos authentication." + RESET ) else: args.spn_prefix = args.spn_hostname = None if args.no_pass: args.password = None elif args.user and not args.password: args.password = prompt("Password: ", is_password=True) if not args.password: args.password = None if username: log.info( "[*] Connecting to '{}:{}' as '{}'" "".format(args.ip, args.port, username, auth) ) print( BLUE + "[*] Connecting to '{}:{}' as '{}'" "".format(args.ip, args.port, username) + RESET ) else: log.info("[*] Connecting to '{}:{}'".format(args.ip, args.port)) print(BLUE + "[*] Connecting to '{}:{}'".format(args.ip, args.port) + RESET) with WSManEWP( server=args.ip, port=args.port, auth=auth, encryption=encryption, username=args.user, password=args.password, ssl=args.ssl, cert_validation=False, path=args.uri, negotiate_service=args.spn_prefix, negotiate_hostname_override=args.spn_hostname, certificate_key_pem=args.priv_key_pem, certificate_pem=args.cert_pem, user_agent=args.ua, ) as wsman: with RunspacePool(wsman) as r_pool: interactive_shell(r_pool) except (KeyboardInterrupt, EOFError): sys.exit(0) except WinRMTransportError as wte: print(RED + "[-] {}".format(wte) + RESET) log.error("WinRM transport error: {}".format(wte)) sys.exit(1) except ConnectionError as ce: print( RED + "[-] Failed to connect to the remote host: {}:{}" "".format(args.ip, args.port) + RESET ) log.error("Connection error: {}".format(ce)) sys.exit(1) except AuthenticationError as ae: print(RED + "[-] {}".format(ae) + RESET) log.error("Authentication failed: {}".format(ae)) sys.exit(1) except WSManFaultError as wfe: print(RED + "[-] {}".format(wfe) + RESET) log.error("WSMan fault error: {}".format(wfe)) sys.exit(1) except Krb5Error as ke: print(RED + "[-] {}".format(ke) + RESET) log.error("Kerberos error: {}".format(ke)) sys.exit(1) except (OperationNotAvailableError, NoCredentialError) as se: print(RED + "[-] {}".format(se._context_message) + RESET) print(RED + "[-] {}".format(se._BASE_MESSAGE) + RESET) log.error("SpnegoError error: {}".format(se)) sys.exit(1) except SpnegoError as se: print(RED + "[-] {}".format(se._context_message) + RESET) print(RED + "[-] {}".format(se.message) + RESET) log.error("SpnegoError error: {}".format(se)) sys.exit(1) except Exception as e: traceback.print_exc() log.exception("An unexpected error occurred: {}".format(e), exc_info=True) sys.exit(1) finally: log.info("--- Evil-WinRM-Py v{} ended ---".format(__version__)) ================================================ FILE: evil_winrm_py/pypsrp_ewp/__init__.py ================================================ ================================================ FILE: evil_winrm_py/pypsrp_ewp/wsman.py ================================================ # -*- coding: utf-8 -*- # This file is part of evil-winrm-py. # Following code is a modified version of pypsrp's wsman.py # It has been adapted to work with evil-winrm-py. # Original source: https://github.com/jborean93/pypsrp/blob/master/src/pypsrp/wsman.py # Copyright: (c) 2018, Jordan Borean (@jborean93) # MIT License (see LICENSE or https://opensource.org/licenses/MIT) import logging import typing import uuid import xml.etree.ElementTree as ET from pypsrp._utils import get_hostname from pypsrp.encryption import WinRMEncryption from pypsrp.exceptions import WinRMTransportError from pypsrp.wsman import ( AUTH_KWARGS, NAMESPACES, SUPPORTED_AUTHS, WSMan, _TransportHTTP, requests, ) from urllib3.util.retry import Retry try: from requests_credssp import HttpCredSSPAuth except ImportError as err: # pragma: no cover _requests_credssp_import_error = ( "Cannot use CredSSP auth as requests-credssp is not installed: %s" % err ) class HttpCredSSPAuth(object): # type: ignore[no-redef] # https://github.com/python/mypy/issues/1153 def __init__(self, *args, **kwargs): raise ImportError(_requests_credssp_import_error) log = logging.getLogger(__name__) class WSManEWP(WSMan): """Override WSMan class to customize some stuff""" def __init__( self, server: str, max_envelope_size: int = 153600, operation_timeout: int = 20, port: typing.Optional[int] = None, username: typing.Optional[str] = None, password: typing.Optional[str] = None, ssl: bool = True, path: str = "wsman", auth: str = "negotiate", cert_validation: bool = True, connection_timeout: int = 30, encryption: str = "auto", proxy: typing.Optional[str] = None, no_proxy: bool = False, locale: str = "en-US", data_locale: typing.Optional[str] = None, read_timeout: int = 30, reconnection_retries: int = 0, reconnection_backoff: float = 2.0, user_agent: str = "Microsoft WinRM Client", **kwargs: typing.Any, ) -> None: """ Class that handles WSMan transport over HTTP. This exposes a method per action that takes in a resource and the header metadata required by that resource. This is required by the pypsrp.shell.WinRS and pypsrp.powershell.RunspacePool in order to connect to the remote host. It uses HTTP(S) to send data to the remote host. https://msdn.microsoft.com/en-us/library/cc251598.aspx :param server: The hostname or IP address of the host to connect to :param max_envelope_size: The maximum size of the envelope that can be sent to the server. Use update_max_envelope_size() to query the server for the true value :param max_envelope_size: The maximum size of a WSMan envelope that can be sent to the server :param operation_timeout: Indicates that the client expects a response or a fault within the specified time. :param port: The port to connect to, default is 5986 if ssl=True, else 5985 :param username: The username to connect with :param password: The password for the above username :param ssl: Whether to connect over http or https :param path: The WinRM path to connect to :param auth: The auth protocol to use; basic, certificate, negotiate, credssp. Can also specify ntlm or kerberos to limit the negotiate protocol :param cert_validation: Whether to validate the server's SSL cert :param connection_timeout: The timeout for connecting to the HTTP endpoint :param read_timeout: The timeout for receiving from the HTTP endpoint :param encryption: Controls the encryption setting, default is auto but can be set to always or never :param proxy: The proxy URL used to connect to the remote host :param no_proxy: Whether to ignore any environment proxy vars and connect directly to the host endpoint :param locale: The wsmv:Locale value to set on each WSMan request. This specifies the language in which the client wants response text to be translated. The value should be in the format described by RFC 3066, with the default being 'en-US' :param data_locale: The wsmv:DataLocale value to set on each WSMan request. This specifies the format in which numerical data is presented in the response text. The value should be in the format described by RFC 3066, with the default being the value of locale. :param int reconnection_retries: Number of retries on connection problems :param float reconnection_backoff: Number of seconds to backoff in between reconnection attempts (first sleeps X, then sleeps 2*X, 4*X, 8*X, ...) :param kwargs: Dynamic kwargs based on the auth protocol set # auth='certificate' certificate_key_pem: The path to the cert key pem file certificate_pem: The path to the cert pem file # auth='credssp' credssp_auth_mechanism: The sub auth mechanism to use in CredSSP, default is 'auto' but can be 'ntlm' or 'kerberos' credssp_disable_tlsv1_2: Use TLSv1.0 instead of 1.2 credssp_minimum_version: The minimum CredSSP server version to allow # auth in ['negotiate', 'ntlm', 'kerberos'] negotiate_send_cbt: Whether to send the CBT token on HTTPS connections, default is True # the below are only relevant when kerberos (or nego used kerb) negotiate_delegate: Whether to delegate the Kerb token to extra servers (credential delegation), default is False negotiate_hostname_override: Override the hostname used when building the server SPN negotiate_service: Override the service used when building the server SPN, default='WSMAN' # custom user-agent header user_agent: The user agent to use for the HTTP requests, this defaults to 'Microsoft WinRM Client' """ log.debug( "Initialising WSMan class with maximum envelope size of %d " "and operation timeout of %s" % (max_envelope_size, operation_timeout) ) self.session_id = str(uuid.uuid4()) self.locale = locale self.data_locale = self.locale if data_locale is None else data_locale self.transport = _TransportHTTPEWP( server, port, username, password, ssl, path, auth, cert_validation, connection_timeout, encryption, proxy, no_proxy, read_timeout, reconnection_retries, reconnection_backoff, user_agent, **kwargs, ) self.max_envelope_size = max_envelope_size self.operation_timeout = operation_timeout # register well known namespace prefixes so ElementTree doesn't # randomly generate them, saving packet space for key, value in NAMESPACES.items(): ET.register_namespace(key, value) # This is the approx max size of a Base64 string that can be sent in a # SOAP message payload (PSRP fragment or send input data) to the # server. This value is dependent on the server's MaxEnvelopSizekb # value set on the WinRM service and the default is different depending # on the Windows version. Server 2008 (R2) detaults to 150KiB while # newer hosts are 500 KiB and this can be configured manually. Because # we don't know the OS version before we connect, we set the default to # 150KiB to ensure we are compatible with older hosts. This can be # manually adjusted with the max_envelope_size param which is the # MaxEnvelopeSizekb value * 1024. Otherwise the # update_max_envelope_size() function can be called and it will gather # this information for you. self.max_payload_size = self._calc_envelope_size(max_envelope_size) class _TransportHTTPEWP(_TransportHTTP): """Override _TransportHTTP""" def __init__( self, server: str, port: typing.Optional[int] = None, username: typing.Optional[str] = None, password: typing.Optional[str] = None, ssl: bool = True, path: str = "wsman", auth: str = "negotiate", cert_validation: bool = True, connection_timeout: int = 30, encryption: str = "auto", proxy: typing.Optional[str] = None, no_proxy: bool = False, read_timeout: int = 30, reconnection_retries: int = 0, reconnection_backoff: float = 2.0, user_agent: str = "Microsoft WinRM Client", **kwargs: typing.Any, ) -> None: self.server = server self.port = port if port is not None else (5986 if ssl else 5985) self.username = username self.password = password self.ssl = ssl self.path = path if auth not in SUPPORTED_AUTHS: raise ValueError( "The specified auth '%s' is not supported, " "please select one of '%s'" % (auth, ", ".join(SUPPORTED_AUTHS)) ) self.auth = auth self.cert_validation = cert_validation self.connection_timeout = connection_timeout self.read_timeout = read_timeout self.reconnection_retries = reconnection_retries self.reconnection_backoff = reconnection_backoff self.user_agent = user_agent # determine the message encryption logic if encryption not in ["auto", "always", "never"]: raise ValueError( "The encryption value '%s' must be auto, always, or never" % encryption ) enc_providers = ["credssp", "kerberos", "negotiate", "ntlm"] if ssl: # msg's are automatically encrypted with TLS, we only want message # encryption if always was specified self.wrap_required = encryption == "always" if self.wrap_required and self.auth not in enc_providers: raise ValueError( "Cannot use message encryption with auth '%s', either set " "encryption='auto' or use one of the following auth " "providers: %s" % (self.auth, ", ".join(enc_providers)) ) else: # msg's should always be encrypted when not using SSL, unless the # user specifies to never encrypt self.wrap_required = not encryption == "never" if self.wrap_required and self.auth not in enc_providers: raise ValueError( "Cannot use message encryption with auth '%s', either set " "encryption='never', use ssl=True or use one of the " "following auth providers: %s" % (self.auth, ", ".join(enc_providers)) ) self.encryption: typing.Optional[WinRMEncryption] = None self.proxy = proxy self.no_proxy = no_proxy self.certificate_key_pem: typing.Optional[str] = None self.certificate_pem: typing.Optional[str] = None for kwarg_list in AUTH_KWARGS.values(): for kwarg in kwarg_list: setattr(self, kwarg, kwargs.get(kwarg, None)) self.endpoint = self._create_endpoint( self.ssl, self.server, self.port, self.path ) log.debug( "Initialising HTTP transport for endpoint: %s, user: %s, " "auth: %s" % (self.endpoint, self.username, self.auth) ) self.session: typing.Optional[requests.Session] = None # used when building tests, keep commented out # self._test_messages = [] def send(self, message: bytes) -> bytes: hostname = get_hostname(self.endpoint) if self.session is None: self.session = self._build_session() # need to send an initial blank message to setup the security # context required for encryption if self.wrap_required: request = requests.Request("POST", self.endpoint, data=None) prep_request = self.session.prepare_request(request) self._send_request(prep_request) protocol = WinRMEncryption.SPNEGO if isinstance(self.session.auth, HttpCredSSPAuth): protocol = WinRMEncryption.CREDSSP elif self.session.auth.contexts[hostname].response_auth_header == "kerberos": # type: ignore[union-attr] # This should not happen # When Kerberos (not Negotiate) was used, we need to send a special protocol value and not SPNEGO. protocol = WinRMEncryption.KERBEROS self.encryption = WinRMEncryption(self.session.auth.contexts[hostname], protocol) # type: ignore[union-attr] # This should not happen if log.isEnabledFor(logging.DEBUG): log.debug("Sending message: %s" % message.decode("utf-8")) # for testing, keep commented out # self._test_messages.append({"request": message.decode('utf-8'), # "response": None}) headers = self.session.headers if self.wrap_required: content_type, payload = self.encryption.wrap_message(message) # type: ignore[union-attr] # This should not happen protocol = ( self.encryption.protocol if self.encryption else WinRMEncryption.SPNEGO ) type_header = '%s;protocol="%s";boundary="Encrypted Boundary"' % ( content_type, protocol, ) headers.update( { "Content-Type": type_header, "Content-Length": str(len(payload)), } ) else: payload = message headers["Content-Type"] = "application/soap+xml;charset=UTF-8" request = requests.Request("POST", self.endpoint, data=payload, headers=headers) prep_request = self.session.prepare_request(request) try: return self._send_request(prep_request) except WinRMTransportError as err: if err.code == 400: log.debug("Session invalid, resetting session") self.session = None # reset the session so we can retry return self.send(message) else: raise def _build_session(self) -> requests.Session: log.debug("Building requests session with auth %s" % self.auth) self._suppress_library_warnings() session = requests.Session() session.headers["User-Agent"] = self.user_agent # requests defaults to 'Accept-Encoding: gzip, default' which normally doesn't matter on vanila WinRM but for # Exchange endpoints hosted on IIS they actually compress it with 1 of the 2 algorithms. By explicitly setting # identity we are telling the server not to transform (compress) the data using the HTTP methods which we don't # support. https://tools.ietf.org/html/rfc7231#section-5.3.4 session.headers["Accept-Encoding"] = "identity" # get the env requests settings session.trust_env = True settings = session.merge_environment_settings( url=self.endpoint, proxies={}, stream=None, verify=None, cert=None ) # set the proxy config session.proxies = settings["proxies"] proxy_key = "https" if self.ssl else "http" if self.proxy is not None: session.proxies = { proxy_key: self.proxy, } elif self.no_proxy: session.proxies = { proxy_key: False, # type: ignore[dict-item] # A boolean is expected here } # Retry on connection errors, with a backoff factor retry_kwargs = { "total": self.reconnection_retries, "connect": self.reconnection_retries, "status": self.reconnection_retries, "read": 0, "backoff_factor": self.reconnection_backoff, "status_forcelist": (425, 429, 503), } try: retries = Retry(**retry_kwargs) except TypeError: # Status was added in urllib3 >= 1.21 (Requests >= 2.14.0), remove # the status retry counter and try again. The user should upgrade # to a newer version log.warning( "Using an older requests version that without support for status retries, ignoring.", exc_info=True, ) del retry_kwargs["status"] retries = Retry(**retry_kwargs) session.mount("http://", requests.adapters.HTTPAdapter(max_retries=retries)) session.mount("https://", requests.adapters.HTTPAdapter(max_retries=retries)) # set cert validation config session.verify = self.cert_validation # if cert_validation is a bool (no path specified), not False and there # are env settings for verification, set those env settings if ( isinstance(self.cert_validation, bool) and self.cert_validation and settings["verify"] is not None ): session.verify = settings["verify"] build_auth = getattr(self, "_build_auth_%s" % self.auth) build_auth(session) return session ================================================ FILE: setup.py ================================================ import io from os import path from setuptools import find_packages, setup pwd = path.abspath(path.dirname(__file__)) with io.open(path.join(pwd, "README.md"), encoding="utf-8") as readme: desc = readme.read() setup( name="evil-winrm-py", version=__import__("evil_winrm_py").__version__, description="Execute commands interactively on remote Windows machines using the WinRM protocol", long_description=desc, long_description_content_type="text/markdown", author="adityatelange", license="MIT", url="https://github.com/adityatelange/evil-winrm-py", download_url="https://github.com/adityatelange/evil-winrm-py/archive/v%s.zip" % __import__("evil_winrm_py").__version__, packages=find_packages(), classifiers=[ "Topic :: Security", "Operating System :: Unix", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", ], install_requires=[ "pypsrp==0.8.1", "prompt_toolkit==3.0.52", "tqdm==4.67.3", ], extras_require={ "kerberos": [ "pypsrp[kerberos]==0.8.1", ] }, python_requires=">=3.9", entry_points={ "console_scripts": [ "evil-winrm-py = evil_winrm_py.evil_winrm_py:main", "ewp = evil_winrm_py.evil_winrm_py:main", ] }, package_data={ "evil_winrm_py": ["_ps/*.ps1"], }, )