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