Full Code of adityatelange/evil-winrm-py for AI

main e956d38568ae cached
21 files
128.4 KB
29.9k tokens
34 symbols
1 requests
Download .txt
Repository: adityatelange/evil-winrm-py
Branch: main
Commit: e956d38568ae
Files: 21
Total size: 128.4 KB

Directory structure:
gitextract_ce49hs3q/

├── .github/
│   └── workflows/
│       └── wiki.yml
├── .gitignore
├── LICENSE
├── README.md
├── docs/
│   ├── README.md
│   ├── development.md
│   ├── install.md
│   ├── knowledgebase.md
│   ├── release.md
│   ├── sample/
│   │   └── krb5.conf
│   └── usage.md
├── evil_winrm_py/
│   ├── __init__.py
│   ├── _ps/
│   │   ├── __init__.py
│   │   ├── exec.ps1
│   │   ├── fetch.ps1
│   │   ├── loaddll.ps1
│   │   └── send.ps1
│   ├── evil_winrm_py.py
│   └── pypsrp_ewp/
│       ├── __init__.py
│       └── wsman.py
└── setup.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/wiki.yml
================================================
name: Publish wiki
on:
  push:
    branches: [main]
    paths:
      - docs/**
      - .github/workflows/wiki.yml
concurrency:
  group: publish-wiki
  cancel-in-progress: true
permissions:
  contents: write
jobs:
  publish-wiki:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: Andrew-Chen-Wang/github-wiki-action@v5.0.1
        with:
          path: docs
          ignore: |
            *.conf


================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# UV
#   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#uv.lock

# poetry
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
#   in version control.
#   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# Ruff stuff:
.ruff_cache/

# PyPI configuration file
.pypirc

# Test Files
test_*.py


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2025 Aditya Telange

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<div align=center>
  <img height="150" alt="ewp-logo" src="https://raw.githubusercontent.com/adityatelange/evil-winrm-py/refs/heads/main/assets/logo.svg" />
  <h1>evil-winrm-py</h1>

[![PyPI version](https://img.shields.io/pypi/v/evil-winrm-py)](https://pypi.org/project/evil-winrm-py/)
![Python](https://img.shields.io/badge/python-3.9+-blue.svg)
![License](https://img.shields.io/github/license/adityatelange/evil-winrm-py)
![PyPI - Downloads](https://img.shields.io/pypi/dm/evil-winrm-py?label=pypi%20downloads)
[![Github Wiki](https://img.shields.io/badge/github-wiki%2Fdocs-blue)](https://github.com/adityatelange/evil-winrm-py/wiki)

</div>

`evil-winrm-py` is a python-based tool for executing commands on remote Windows machines using the WinRM (Windows Remote Management) protocol. It provides an interactive shell with enhanced features like file upload/download, command history, and colorized output. It supports various authentication methods including NTLM, Pass-the-Hash, Certificate, and Kerberos.

![](https://raw.githubusercontent.com/adityatelange/evil-winrm-py/refs/tags/v1.6.0/assets/terminal.png)

> [!NOTE]
> This tool is designed strictly for educational, ethical use, and authorized penetration testing. Always ensure you have explicit authorization before accessing any system. Unauthorized access or misuse of this tool is both illegal and unethical.

## Motivation

The original evil-winrm is written in Ruby, which can be a hurdle for some users. Rewriting it in Python makes it more accessible and easier to use, while also allowing us to leverage Python’s rich ecosystem for added features and flexibility.

I also wanted to learn more about winrm and its internals, so this project will also serve as a learning experience for me.

## Features

- Execute commands on remote Windows machines via an interactive shell.
- Download files from the remote host to the local machine.
- Upload files from the local machine to the remote host.
- Progress bar for file transfers with speed and time estimation.
- Stable and reliable file transfer including support for large files with MD5 checksum verification.
- Auto-complete local and remote file paths (even those with spaces) with `Tab` completion.
- Auto-complete PowerShell cmdlets/helpers with `Tab` completion. 🆕
- Load PowerShell functions from local scripts into the interactive shell. 🆕
- Run local PowerShell scripts on the remote host. 🆕
- Load local DLLs (in-memory) as PowerShell modules on the remote host. 🆕
- Upload and execute local EXEs (in-memory) on the remote host. 🆕
- List the running services (except system services) on the remote host. 🆕
- Enable logging and debugging for better traceability.
- Navigate command history using `up`/`down` arrow keys.
- Display colorized output for improved readability.
- Lightweight and Python-based for ease of use.
- Keyboard Interrupt (Ctrl+C / Ctrl+D) support to terminate long-running commands gracefully.

Includes support for:

- NTLM authentication.
- Pass-the-Hash authentication.
- Certificate authentication.
- Kerberos authentication with custom SPN prefix and hostname options.
- SSL to secure communication with the remote host.
- custom WSMan URIs.
- custom user agent for the WinRM client.

Detailed documentation can be found in the [docs](https://github.com/adityatelange/evil-winrm-py/blob/main/docs) directory.

## Installation (Windows/Linux)

#### Installation of Kerberos prerequisites on Linux

```bash
sudo apt install gcc python3-dev libkrb5-dev krb5-pkinit
# Optional: krb5-user
```

### Install `evil-winrm-py`

> You may use [pipx](https://pipx.pypa.io/stable/) or [uv](https://docs.astral.sh/uv/) instead of pip to install evil-winrm-py. `pipx`/`uv` is a tool to install and run Python applications in isolated environments, which helps prevent dependency conflicts by keeping the tool's dependencies separate from your system's Python packages.

```bash
pip install evil-winrm-py
pip install evil-winrm-py[kerberos] # for kerberos support on Linux

# Note: building gssapi and krb5 packages may take some time, so be patient.
```

or if you want to install with latest commit from the main branch you can do so by cloning the repository and installing it with `pip`/`pipx`/`uv`:

```bash
git clone https://github.com/adityatelange/evil-winrm-py
cd evil-winrm-py
pip install .
```

### Update

```bash
pip install --upgrade evil-winrm-py
```

### Uninstall

```bash
pip uninstall evil-winrm-py
```

Check [Installation Guide](https://github.com/adityatelange/evil-winrm-py/blob/main/docs/install.md) for more details.

## Availability on Unix distributions

[![Packaging status](https://repology.org/badge/vertical-allrepos/evil-winrm-py.svg)](https://repology.org/project/evil-winrm-py/versions)

For above mentioned distributions, you can install `evil-winrm-py` directly from their package managers. Thanks to the package maintainers for packaging and maintaining `evil-winrm-py` in their respective distributions.

## Usage

Details on how to use `evil-winrm-py` can be found in the [Usage Guide](https://github.com/adityatelange/evil-winrm-py/blob/main/docs/usage.md).

```bash
usage: evil-winrm-py [-h] -i IP [-u USER] [-p PASSWORD] [-H HASH]
                     [--priv-key-pem PRIV_KEY_PEM] [--cert-pem CERT_PEM] [--uri URI]
                     [--ua UA] [--port PORT] [--spn-prefix SPN_PREFIX]
                     [--spn-hostname SPN_HOSTNAME] [-k] [--no-pass] [--ssl] [--log]
                     [--debug] [--no-colors] [--version]

options:
  -h, --help            show this help message and exit
  -i, --ip IP           remote host IP or hostname
  -u, --user USER       username
  -p, --password PASSWORD
                        password
  -H, --hash HASH       nthash
  --priv-key-pem PRIV_KEY_PEM
                        local path to private key PEM file
  --cert-pem CERT_PEM   local path to certificate PEM file
  --uri URI             wsman URI (default: /wsman)
  --ua UA               user agent for the WinRM client (default: "Microsoft WinRM Client")
  --port PORT           remote host port (default 5985)
  --spn-prefix SPN_PREFIX
                        specify spn prefix
  --spn-hostname SPN_HOSTNAME
                        specify spn hostname
  -k, --kerberos        use kerberos authentication
  --no-pass             do not prompt for password
  --ssl                 use ssl
  --log                 log session to file
  --debug               enable debug logging
  --no-colors           disable colors
  --version             show version

For more information about this project, visit https://github.com/adityatelange/evil-winrm-py
For user guide, visit https://github.com/adityatelange/evil-winrm-py/blob/main/docs/usage.md
```

Example:

```bash
evil-winrm-py -i 192.168.1.100 -u Administrator -p P@ssw0rd --ssl
```

## Menu Commands (inside evil-winrm-py shell)

```bash
Menu:
[+] services                                                - Show the running services (except system services)
[+] upload <local_path> <remote_path>                       - Upload a file
[+] download <remote_path> <local_path>                     - Download a file
[+] loadps <local_path>.ps1                                 - Load PowerShell functions from a local script
[+] runps <local_path>.ps1                                  - Run a local PowerShell script on the remote host
[+] loaddll <local_path>.dll                                - Load a local DLL (in-memory) as a module on the remote host
[+] runexe <local_path>.exe [args]                          - Upload and execute (in-memory) a local EXE on the remote host
[+] menu                                                    - Show this menu
[+] clear, cls                                              - Clear the screen
[+] exit                                                    - Exit the shell
Note: Use absolute paths for upload/download for reliability.
```

## Credits

- Original evil-winrm project - https://github.com/Hackplayers/evil-winrm
- PowerShell Remoting Protocol for Python - https://github.com/jborean93/pypsrp
- Prompt Toolkit - https://github.com/prompt-toolkit/python-prompt-toolkit
- tqdm - https://github.com/tqdm/tqdm
- Thanks to [Github Coplilot](https://github.com/features/copilot) and [Google Gemini](https://gemini.google.com/app) for code suggestions and improvements.

## Stargazers over time

[![Stargazers over time](https://starchart.cc/adityatelange/evil-winrm-py.svg?background=%23ffffff00&axis=%23858585&line=%236b63ff)](https://starchart.cc/adityatelange/evil-winrm-py)


================================================
FILE: docs/README.md
================================================
## Quick Links

- **[Usage Guide](./usage.md)**
- **[Installation Guide](./install.md)**
- [Knowledge Base](./knowledgebase.md)
- [Development Guide](./development.md)
- [Release Guide](./release.md)


================================================
FILE: docs/development.md
================================================
# Development Environment Setup

## Setup

Download the repository.

```bash
git clone https://github.com/adityatelange/evil-winrm-py
cd evil-winrm-py
```

Create a virtual environment (optional but recommended):

```bash
python3 -m venv venv
source venv/bin/activate
```

Install the required packages:

```bash
pip install pypsrp[kerberos]==0.8.1 prompt_toolkit==3.0.51 tqdm==4.67.1
```

## Create a test file

```python
# File: test.py
from evil_winrm_py.evil_winrm_py import main

if __name__ == "__main__":
    main()
```

## Run the test file

```bash
python test.py -h
```


================================================
FILE: docs/install.md
================================================
# Installation Guide

`evil-winrm-py` is available on:

- PyPI - https://pypi.org/project/evil-winrm-py/
- Github - https://github.com/adityatelange/evil-winrm-py
- Kali Linux - https://pkg.kali.org/pkg/evil-winrm-py
- Parrot OS - https://gitlab.com/parrotsec/packages/evil-winrm-py

## For Kali Linux and Parrot OS Users

If you are using Kali Linux or Parrot OS, you can install `evil-winrm-py` directly from the package manager:

```bash
sudo apt update
sudo apt install evil-winrm-py
```

---

## Installation of Kerberos Dependencies on Linux

```bash
sudo apt install gcc python3-dev libkrb5-dev krb5-pkinit
# Optional: krb5-user
```

> [!NOTE]
> `[kerberos]` is an optional dependency that includes the necessary packages for Kerberos authentication support. If you do not require Kerberos authentication, you can install `evil-winrm-py` without this extra.

## Using `pip`

You can install the package directly from PyPI using pip:

```bash
pip install evil-winrm-py[kerberos]
```

Installing latest development version directly from GitHub:

```bash
pip install 'evil-winrm-py[kerberos] @ git+https://github.com/adityatelange/evil-winrm-py'
```

## Using `pipx`

For a more isolated installation, you can use pipx:

```bash
pipx install evil-winrm-py[kerberos]
```

Installing latest development version directly from GitHub:

```bash
pipx install 'evil-winrm-py[kerberos] @ git+https://github.com/adityatelange/evil-winrm-py'
```

## Using `uv`

If you prefer using `uv`, you can install the package with the following command:

```bash
uv tool install evil-winrm-py[kerberos]
```

Installing latest development version directly from GitHub:

```bash
uv tool install git+https://github.com/adityatelange/evil-winrm-py[kerberos]
```


================================================
FILE: docs/knowledgebase.md
================================================
# Knowledge Base

## Negotiate authentication

A negotiated, single sign on type of authentication that is the Windows implementation of [Simple and Protected GSSAPI Negotiation Mechanism (SPNEGO)](https://learn.microsoft.com/en-us/windows/win32/winrm/windows-remote-management-glossary). SPNEGO negotiation determines whether authentication is handled by Kerberos or NTLM. Kerberos is the preferred mechanism. Negotiate authentication on Windows-based systems is also called Windows Integrated Authentication.

Reference:

- https://learn.microsoft.com/en-us/windows/win32/winrm/windows-remote-management-glossary#:~:text=A%20negotiated%2C%20single,Windows%20Integrated%20Authentication
- https://learn.microsoft.com/en-us/windows/win32/winrm/authentication-for-remote-connections#negotiate-authentication

## WinRM - Types of Authentication

1. Basic Authentication
2. Digest Authentication
3. Kerberos Authentication
4. Negotiate Authentication
5. NTLM Authentication
6. Certificate Authentication
7. CredSSP Authentication

Reference: https://learn.microsoft.com/en-us/windows/win32/winrm/authentication-for-remote-connections

Enable Auth

```powershell
Set-Item -Path WSMan:\localhost\Service\Auth\Certificate -Value $true
```

## Configure WinRM HTTPS with self-signed certificate

```powershell
# https://gist.github.com/gregjhogan/dbe0bfa277d450c049e0bbdac6142eed
$cert = New-SelfSignedCertificate -CertstoreLocation Cert:\LocalMachine\My -DnsName $env:COMPUTERNAME
Enable-PSRemoting -SkipNetworkProfileCheck -Force
New-Item -Path WSMan:\LocalHost\Listener -Transport HTTPS -Address * -CertificateThumbPrint $cert.Thumbprint –Force

New-NetFirewallRule -DisplayName "Windows Remote Management (HTTPS-In)" -Name "Windows Remote Management (HTTPS-In)" -Profile Any -LocalPort 5986 -Protocol TCP
```

Reference: https://learn.microsoft.com/en-us/windows/win32/winrm/installation-and-configuration-for-windows-remote-management

- **Get the current WinRM configuration**

```powershell
winrm get winrm/config
```

- **Enumerate WinRM listeners**

```powershell
winrm enumerate winrm/config/listener
```

## Configure WinRM Certificate Authentication

Certificate authentication is a method of authenticating to a remote computer using a certificate. The certificate must be installed on the remote computer and the client must have access to the private key of the certificate.

**Enable Certificate Authentication**

```powershell
Set-Item -Path WSMan:\localhost\Service\Auth\Certificate -Value $true
```

**Generate a certificate using PowerShell**

```powershell
# Set the username to the name of the user the certificate will be mapped to
$username = 'local-user'

$clientParams = @{
    CertStoreLocation = 'Cert:\CurrentUser\My'
    NotAfter          = (Get-Date).AddYears(1)
    Provider          = 'Microsoft Software Key Storage Provider'
    Subject           = "CN=$username"
    TextExtension     = @("2.5.29.37={text}1.3.6.1.5.5.7.3.2","2.5.29.17={text}upn=$username@localhost")
    Type              = 'Custom'
}
$cert = New-SelfSignedCertificate @clientParams
$certKeyName = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey(
    $cert).Key.UniqueName

# Exports the public cert.pem and key cert.pfx
Set-Content -Path "cert.pem" -Value @(
    "-----BEGIN CERTIFICATE-----"
    [Convert]::ToBase64String($cert.RawData) -replace ".{64}", "$&`n"
    "-----END CERTIFICATE-----"
)
$certPfxBytes = $cert.Export('Pfx', '')
[System.IO.File]::WriteAllBytes("$pwd\cert.pfx", $certPfxBytes)

# Removes the private key and cert from the store after exporting
$keyPath = [System.IO.Path]::Combine($env:AppData, 'Microsoft', 'Crypto', 'Keys', $certKeyName)
Remove-Item -LiteralPath "Cert:\CurrentUser\My\$($cert.Thumbprint)" -Force
Remove-Item -LiteralPath $keyPath -Force
```

We now have `cert.pem` and `cert.pfx` files.

**Import Certificate to the Certificate Store**

```powershell
$store = Get-Item -LiteralPath Cert:\LocalMachine\Root
$store.Open('ReadWrite')
$store.Add($cert)
$store.Dispose()
```

**Mapping Certificate to a Local Account**

```powershell
# Will prompt for the password of the user.
$credential = Get-Credential local-user

$certChain = [System.Security.Cryptography.X509Certificates.X509Chain]::new()
[void]$certChain.Build($cert)
$caThumbprint = $certChain.ChainElements.Certificate[-1].Thumbprint

$certMapping = @{
    Path       = 'WSMan:\localhost\ClientCertificate'
    Subject    = $cert.GetNameInfo('UpnName', $false)
    Issuer     = $caThumbprint
    Credential = $credential
    Force      = $true
}
New-Item @certMapping
```

**Convert to PEM format**

```bash
openssl pkcs12 \
    -in cert.pfx \
    -nocerts \
    -nodes \
    -passin pass: |
    sed -ne '/-BEGIN PRIVATE KEY-/,/-END PRIVATE KEY-/p' > priv-key.pem
```

User `local-user` can now auth using private key `priv_key.pem` and public key `cert.pem`.

Reference: https://docs.ansible.com/ansible/latest/os_guide/windows_winrm_certificate.html


================================================
FILE: docs/release.md
================================================
# Releasing a new version on PyPI

Read More: https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/

## Setup

```bash
python3 -m pip install --upgrade build
python3 -m pip install --upgrade twine
```

## Bump version

```bash
# File: evil_winrm_py/__init__.py
__version__ = "X.Y.Z" # update this to the new version
```

## Sceenshot

Creating screenshots for the README using the [freeze](https://github.com/charmbracelet/freeze) tool.

```bash
freeze --execute "evil-winrm-py -h" -o assets/terminal.png --padding 5 --border.radius 4 # --wrap 120
```

Update the screenshot tag in the README file.

```diff
# File: evil_winrm_py/README.md
-![](https://raw.githubusercontent.com/adityatelange/evil-winrm-py/refs/tags/v1.4.0/assets/terminal.png)
+![](https://raw.githubusercontent.com/adityatelange/evil-winrm-py/refs/tags/v1.4.1/assets/terminal.png)
```

## Build

```bash
python3 -m build
```

## Upload

```bash
python3 -m twine upload dist/evil_winrm_py-$VERSION*
# example: python3 -m twine upload dist/evil_winrm_py-0.0.2*
```


================================================
FILE: docs/sample/krb5.conf
================================================
# Sample Kerberos configuration file
# Location: /etc/krb5.conf or /<your working directory>/krb5.conf

[libdefaults]
        default_realm = SEVENKINGDOMS.LOCAL
        dns_lookup_realm = false
        dns_lookup_kdc = false
[realms]
        SEVENKINGDOMS.LOCAL = {
                kdc = kingslanding.sevenkingdoms.local
                admin_server = kingslanding.sevenkingdoms.local
                default_domain = sevenkingdoms.local
        }
[domain_realm]
        .sevenkingdoms.local = SEVENKINGDOMS.LOCAL
        sevenkingdoms.local = SEVENKINGDOMS.LOCAL


================================================
FILE: docs/usage.md
================================================
# Usage Guide

## Authentication Methods

### NTLM Authentication

```bash
evil-winrm-py -i <IP> -u <USERNAME> -p <PASSWORD>
```

### Kerberos Authentication

Kerberos authentication supports both password-based and ticket-based authentication.

#### Generate hosts file entry

Use `netexec` to generate a hosts file entry for the target domain.

```bash
netexec smb sevenkingdoms.local --generate-hosts-file hosts.txt
```

Copy the content of `hosts.txt` to your `/etc/hosts` file.

> [!IMPORTANT]
> If you are adding an entry manually, ensure you follow the correct format for subdomains and fully qualified domain names (FQDNs). Kerberos uses SPNEGO, which relies on a specific algorithm to resolve hostnames. For more details, see [SPNEGO algorithm to resolve host names](https://www.ibm.com/docs/en/samfm/8.0.1?topic=spnego-algorithm-resolve-host-names).
>
> The format is as follows:
>
> ```
> <IP> fully_qualified_hostname short_name
> <IP> kingslanding.sevenkingdoms.local sevenkingdoms.local kingslanding
> ```

#### Generate krb5.conf file

Use `netexec` to generate a `krb5.conf` file for the target domain.

```bash
netexec smb sevenkingdoms.local --generate-krb5-file krb5.conf
```

Sample `krb5.conf` file can be found [here](https://github.com/adityatelange/evil-winrm-py/blob/main/docs/sample/krb5.conf).

#### Password-based Kerberos Authentication

This will request a Kerberos ticket and store it in memory for the session.

```bash
evil-winrm-py -i <IP> -u <USERNAME> -p <PASSWORD> --kerberos
```

#### Ticket-based Kerberos Authentication

If you already have a Kerberos ticket (e.g., from `kinit`), you can use it directly without providing a password.

Specify the `KRB5CCNAME` and `KRB5_CONFIG` environment variables to point to your Kerberos ticket cache and configuration file, respectively.

```bash
export KRB5CCNAME=/path/to/your/krb5cc_file
export KRB5_CONFIG=/path/to/your/krb5.conf
# By default, the ticket cache is stored in `/tmp/krb5cc_<UID>` on Unix-like systems.
# By default, the Kerberos configuration file is located at `/etc/krb5.conf` on Unix-like systems.
```

Then, you can run the command without providing a username or password:

```bash
evil-winrm-py -i <IP> --kerberos
```

> [!IMPORTANT]
> Make sure when you use a cache ticket, the `SPN` i.e `Service principal` is set correctly. The `SPN` is usually in the format of `http/<hostname>` or `cifs/<hostname>`. The hostname should _always_ be in lowercase.

The tool also supports direct authentication (without setting `KRB5CCNAME`) when passing username and password, which will request a ticket for the user and use it for authentication.

```bash
evil-winrm-py -i <IP> -u <USERNAME> -p <PASSWORD> --kerberos
```

Optionally, you can specify the Kerberos realm and SPN prefix/hostname
If you have a Kerberos ticket, you can use it with the following options:

```bash
evil-winrm-py -i <IP> -u <USERNAME> --kerberos --no-pass --spn-prefix <SPN_PREFIX> --spn-hostname <SPN_HOSTNAME>
```

### Pass-the-Hash Authentication

If you have the NTLM hash of the user's password, you can use it for authentication without needing the plaintext password.

```bash
evil-winrm-py -i <IP> -u <USERNAME> -H <NTLM_HASH>
```

### Certificate Authentication

If you want to use certificate-based authentication, you can specify the private key and certificate files in PEM format.

```bash
evil-winrm-py -i <IP> -u <USERNAME> --priv-key-pem <PRIVATE_KEY_PEM_PATH> --cert-pem <CERT_PEM_PATH>
```

## Connection Options

### Using SSL

This will use port 5986 for SSL connections by default. If you want to use a different port, you can specify it with [custom port option](#using-custom-port).

```bash
evil-winrm-py -i <IP> -u <USERNAME> -p <PASSWORD> --ssl
```

### Using Custom URI

If the target server has a custom WinRM URI, you can specify it using the `--uri` option. This is useful if the WinRM service is hosted on a different path than the default.

```bash
evil-winrm-py -i <IP> -u <USERNAME> -p <PASSWORD> --uri <CUSTOM_URI>
```

### Using Custom Port

If the target server is using a non-standard port for WinRM, you can specify the port using the `--port` option. The default port for WinRM over HTTP is 5985, and for HTTPS it is 5986.

```bash
evil-winrm-py -i <IP> -u <USERNAME> -p <PASSWORD> --port <PORT>
```

## Logging and Debugging

Logging will create a log file in the current directory named `evil-winrm-py.log`.

```bash
evil-winrm-py -i <IP> -u <USERNAME> -p <PASSWORD> --log
```

### Debugging

If Debug mode is enabled, it will also log debug information, including debug messages and stack traces from libraries used by the tool.

```bash
evil-winrm-py -i <IP> -u <USERNAME> -p <PASSWORD> --debug
```

Debugging for kerberos authentication can be enabled by setting the `KRB5_TRACE` environment variable to a file path where you want to log the Kerberos debug information.

```bash
export KRB5_TRACE=/path/to/kerberos_debug.log
```

or you can set it to `stdout` to print the debug information to the console.

```bash
export KRB5_TRACE=/dev/stdout evil-winrm-py -i <IP> -u <USERNAME> -p <PASSWORD> --kerberos
```

## Interactive Shell

Once you have successfully authenticated, you will be dropped into an interactive shell where you can execute commands on the remote Windows machine.

```bash
          _ _            _
  _____ _(_| |_____ __ _(_)_ _  _ _ _ __ ___ _ __ _  _
 / -_\ V | | |___\ V  V | | ' \| '_| '  |___| '_ | || |
 \___|\_/|_|_|    \_/\_/|_|_||_|_| |_|_|_|  | .__/\_, |
                                            |_|   |__/  v1.3.0

[*] Connecting to '192.168.1.100' as 'Administrator'
evil-winrm-py PS C:\Users\Administrator\Documents> █
```

You can execute commands just like you would in a normal Windows command prompt. To exit the interactive shell, type `exit` or press `Ctrl+D`.
If you want to cancel a command that is currently running, you can use `Ctrl+C`.

### Menu Commands

Inside the interactive shell, you can use the following commands:

```bash
Menu:
[+] services                                                - Show the running services (except system services)
[+] upload <local_path> <remote_path>                       - Upload a file
[+] download <remote_path> <local_path>                     - Download a file
[+] loadps <local_path>.ps1                                 - Load PowerShell functions from a local script
[+] runps <local_path>.ps1                                  - Run a local PowerShell script on the remote host
[+] loaddll <local_path>.dll                                - Load a local DLL (in-memory) as a module on the remote host
[+] runexe <local_path>.exe [args]                          - Upload and execute (in-memory) a local EXE on the remote host
[+] menu                                                    - Show this menu
[+] clear, cls                                              - Clear the screen
[+] exit                                                    - Exit the shell
Note: Use absolute paths for upload/download for reliability.
```

### Show Running Services

You can list the running services (except system services) on the remote host using the `services` command. This will display a list of services that are currently running, which can be useful for post-exploitation tasks.

```bash
evil-winrm-py PS C:\Users\Administrator\Documents> services
```

### File Transfer

You can upload and download files using the following commands:

```bash
evil-winrm-py PS C:\Users\Administrator\Documents> upload <local_path> <remote_path>
```

```bash
evil-winrm-py PS C:\Users\Administrator\Documents> download <remote_path> <local_path>
```

### Loading PowerShell Scripts (Dot Sourcing)

You can load PowerShell functions from a local script file into the interactive shell using the `loadps` command. This allows you to use custom PowerShell functions defined in your script. This method is known as "dot sourcing".

This can be helpful when using tools like `PowerView` or `PowerUp` that provide a set of PowerShell functions for post-exploitation tasks.

```bash
evil-winrm-py PS C:\Users\Administrator\Documents> loadps <local_path>.ps1
```

These functions will be added to Command Suggestions so you can use them directly using the `Tab` key for auto-completion.

The help command can be used to get more information about the available commands in the interactive shell.

```bash
evil-winrm-py PS C:\Users\Administrator\Documents> Get-Help <LoadedFunctionName> # or help <LoadedFunctionName>
```

### Running Local PowerShell Scripts

You can run a local PowerShell script on the remote host using the `runps` command. This will read the contents of the specified PowerShell script file and execute it on the remote machine.

```bash
evil-winrm-py PS C:\Users\Administrator\Documents> runps <local_path>.ps1
```

### Loading Local DLLs as PowerShell Modules

You can load a local DLL file as a module on the remote host using the `loaddll` command. This will upload the specified DLL file in-memory and load it as a module. Note that this uses .NET's Reflection to load the DLL, so it may not work with all DLL files.

This can be helpful when using tools like [ADModule](https://github.com/samratashok/ADModule).

These Commands/Commandlets will be added to Command Suggestions so you can use them directly using the `Tab` key for auto-completion.

```bash
evil-winrm-py PS C:\Users\Administrator\Documents> loaddll <local_path>.dll
```

### Executing Local EXEs on the Remote Host

You can upload and execute a local EXE file on the remote host using the `runexe` command. This will upload the specified EXE file in-memory and execute it with optional arguments. Note that this uses .NET's Reflection to load and execute the EXE, so it may not work with all EXE files.

This can be helpful when using tools present in [SharpCollection](https://github.com/Flangvik/SharpCollection).

```bash
evil-winrm-py PS C:\Users\Administrator\Documents> runexe <local_path>.exe [args]
```

## Additional Options

### Using No Colors

If you want to disable colored output in the terminal, you can use the `--no-colors` option. This is useful for logging or when your terminal does not support colors.

```bash
evil-winrm-py -i <IP> -u <USERNAME> -p <PASSWORD> --no-colors
```

### Using No Password Prompt

```bash
evil-winrm-py -i <IP> -u <USERNAME> --no-pass
```


================================================
FILE: evil_winrm_py/__init__.py
================================================
__version__ = "1.6.0"


================================================
FILE: evil_winrm_py/_ps/__init__.py
================================================


================================================
FILE: evil_winrm_py/_ps/exec.ps1
================================================
# This script is part of evil-winrm-py project https://github.com/adityatelange/evil-winrm-py
# It runs a dotnet executable in memory from a Base64 string and captures its output.

# --- Define Parameters ---
param (
    [Parameter(Mandatory=$true, Position=0)]
    [string]$Base64Exe, # Base64 encoded executable content

    [Parameter(Mandatory=$false, Position=1)]
    [string[]]$Args     # Arguments to pass to the executable
)

# --- Decode Base64 and Load Assembly ---
$exeBytes = [System.Convert]::FromBase64String($Base64Exe)
$assembly = [System.Reflection.Assembly]::Load($exeBytes)

# --- Execute the Entry Point and Capture Output ---
$entryPoint = $assembly.EntryPoint

if ($entryPoint -eq $null) {
    Write-Error "Error: The provided executable does not have an entry point."
    exit 1
}

# Redirect STDOUT and STDERR
$stdout = New-Object System.IO.StringWriter
$stderr = New-Object System.IO.StringWriter
$oldOut = [Console]::Out
$oldErr = [Console]::Error
[Console]::SetOut($stdout)
[Console]::SetError($stderr)

# Invoke the entry point method and pass arguments if any
$result = $entryPoint.Invoke($null, @(,($Args)))

# Capture outputs
$stdOutContent = $stdout.ToString()
$stdErrContent = $stderr.ToString()

# Log outputs as Plain text
if ($stdOutContent -ne "") {
    Write-Output $stdOutContent.Trim()
}
if ($stdErrContent -ne "") {
    Write-Error $stdErrContent.Trim()
}
if ($result) {
    Write-Output $result.Trim()
}

# Restore original STDOUT and STDERR
[Console]::SetOut($oldOut)
[Console]::SetError($oldErr)


================================================
FILE: evil_winrm_py/_ps/fetch.ps1
================================================
# This script is part of evil-winrm-py project https://github.com/adityatelange/evil-winrm-py
# It reads a file in chunks, converts each chunk to Base64, and outputs metadata and chunks as JSON.

# --- Define Parameters ---
param (
    [Parameter(Mandatory=$true, Position=0)]
    [string]$FilePath
)

# --- Configuration ---
$bufferSize = 65536 # Read in 64 KB chunks

# --- Variables for disposal ---
$fileStream = $null # Initialize as null to handle disposal
$fileInfo = $null   # To store file information

# --- Pre-check and initial metadata ---
if (-not (Test-Path -Path $FilePath -PathType Leaf)) {
    [PSCustomObject]@{
        Type        = "Error"
        Message     = "Error: The specified file path does not exist or is not a file: '$FilePath'"
    } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout
    exit 1 # Exit the script with an error code
}

try {
    $fileInfo = Get-Item -Path $FilePath
    $fileSize = $fileInfo.Length # Total file size in bytes
    $totalChunks = [System.Math]::Ceiling($fileSize / $bufferSize) # Calculate total chunks, rounding up
    $fileHash = (Get-FileHash -Path $FilePath -Algorithm MD5).Hash

    # Output initial file metadata as JSON
    [PSCustomObject]@{
        Type        = "Metadata"
        FilePath    = $FilePath
        FileSize    = $fileSize
        ChunkSize   = $bufferSize
        TotalChunks = $totalChunks
        FileHash    = $fileHash
        FileName    = $fileInfo.Name
    } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout

}
catch {
    [PSCustomObject]@{
        Type        = "Error"
        Message     = "Error getting file information or outputting metadata: $($_.Exception.Message)"
    } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout
    exit 1
}

# --- File Reading and Processing for Base64 Chunks ---
try {
    $fileStream = New-Object System.IO.FileStream($FilePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read)
    $buffer = New-Object byte[] $bufferSize

    $chunkCounter = 0
    $totalBytesRead = 0

    while (($bytesRead = $fileStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
        $chunkCounter++ # Increment chunk counter

        # 1. Convert bytes to Base64
        $chunkBytes = New-Object byte[] $bytesRead
        [System.Array]::Copy($buffer, 0, $chunkBytes, 0, $bytesRead)
        $base64Chunk = [System.Convert]::ToBase64String($chunkBytes)

        # 2. Output the Base64 chunk as a JSON object
        [PSCustomObject]@{
            Type        = "Chunk"
            ChunkNumber = $chunkCounter
            Base64Data  = $base64Chunk
        } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout

        $totalBytesRead += $bytesRead
    }

}
catch {
    [PSCustomObject]@{
        Type        = "Error"
        Message     = "Error during Base64 chunk processing: $($_.Exception.Message)"
    } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout
}
finally {
    if ($fileStream) {
        $fileStream.Dispose()
    }
}


================================================
FILE: evil_winrm_py/_ps/loaddll.ps1
================================================
# This script is part of evil-winrm-py project https://github.com/adityatelange/evil-winrm-py
# It loads a dll in memory as a PowerShell module from a Base64 string and lists its exported functions.

# --- Define Parameters ---
param (
    [Parameter(Mandatory=$true, Position=0)]
    [string]$Base64Dll # Base64 encoded dll content
)

# --- Decode Base64 and Load Assembly ---
$dllBytes = [System.Convert]::FromBase64String($Base64Dll)
$assembly = [System.Reflection.Assembly]::Load($dllBytes)

# --- Output Assembly Metadata ---
$assemblyName = $assembly.GetName().Name
[PSCustomObject]@{
    Type = "Metadata"
    Name = $assemblyName
} | ConvertTo-Json -Compress | Write-Output

# --- Import Modules from the Assembly ---
try {
    Import-Module -Assembly $assembly -ErrorAction Stop
} catch {
    [PSCustomObject]@{
        Type    = "Error"
        Message = "Failed to import module: $($_.Exception.Message)"
    } | ConvertTo-Json -Compress | Write-Output
    exit 1
}

# --- List Exported Functions ---
$loadedModule = Get-Module -Name "dynamic_code_module_$assemblyName*"

if ($loadedModule -ne $null) {
    $exportedFunctions = $loadedModule.ExportedCommands.Keys
    [PSCustomObject]@{
        Type  = "Metadata"
        Funcs = $exportedFunctions
    } | ConvertTo-Json -Compress | Write-Output
} else {
    [PSCustomObject]@{
        Type    = "Error"
        Message = "Could not find the loaded module."
    } | ConvertTo-Json -Compress | Write-Output
    exit 1
}


================================================
FILE: evil_winrm_py/_ps/send.ps1
================================================
# This script is part of evil-winrm-py project https://github.com/adityatelange/evil-winrm-py
# It reads a Base64 encoded chunk of bytes, writes it to a file, and optionally appends to an existing file.
# It also calculates the MD5 hash of the file after writing if required.

# --- Define Parameters ---
param (
    [Parameter(Mandatory=$true, Position=0)]
    [string]$Base64Chunk,   # The Base64 encoded chunk of bytes
    [Parameter(Mandatory=$true, Position=1)]
    [int]$ChunkType = 0,    # 0 for new file, 1 for appending to existing file
    [Parameter(Mandatory=$false, Position=2)]
    [string]$TempFilePath,  # The temporary file path to write/append the bytes to
    [Parameter(Mandatory=$false, Position=3)]
    [string]$FilePath,      # The file path to write/append the bytes to
    [Parameter(Mandatory=$false, Position=4)]
    [string]$FileHash       # The MD5 hash of the file
)

# --- Variables for disposal ---
$fileStream = $null # Initialize as null for safety in finally block


# --- Pre-checks ---
# IF chunkPosition is 0 or 3 its a new file
if ($ChunkType -eq 0 -or $ChunkType -eq 3) {
    # If this is the first chunk, create a unique temporary file path
    $TempFilePath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName())
    # Output initial file metadata as JSON
    [PSCustomObject]@{
        Type            = "Metadata"
        TempFilePath    = $TempFilePath
    } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout
}

# --- Main Logic ---

try {
    # Decode the Base64 chunk into bytes
    $chunkBytes = [System.Convert]::FromBase64String($Base64Chunk)

    # Open the file in Append mode.
    # If the file doesn't exist, it will be created.
    # If it exists, new bytes will be added to the end.
    $fileStream = New-Object System.IO.FileStream(
        $TempFilePath,
        [System.IO.FileMode]::Append, # Use Append mode
        [System.IO.FileAccess]::Write
    )

    # Write the decoded bytes to the file
    # $ChunkSize here is critical and should be the actual length of $chunkBytes for this specific chunk
    $fileStream.Write($chunkBytes, 0, $chunkBytes.Length) # Use $chunkBytes.Length for safety
    $fileStream.Close()
}
catch {
    $FullExceptionMessage = "$($_.Exception.GetType().FullName): $($_.Exception.Message)"
    [PSCustomObject]@{
        Type        = "Error"
        Message     = "Error processing chunk or writing to file: $FullExceptionMessage"
    } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout
}
finally {
    # Ensure the file stream is closed to release the file lock and flush buffers
    if ($fileStream) {
        $fileStream.Dispose()
    }
}

# --- Calculate checksum ---
# Caculate the MD5 hash of the file after writing if ChunkType is 1 or 3
if ($ChunkType -eq 1 -or $ChunkType -eq 3) {
    try {
        if ($TempFilePath) {
            # If a file hash is provided, verify it
            $calculatedHash = (Get-FileHash -Path $TempFilePath -Algorithm MD5).Hash
            if ($calculatedHash -eq $FileHash) {
                # If the hash matches, move the temporary file to the final destination
                [System.IO.File]::Delete($FilePath)
                [System.IO.File]::Move($TempFilePath, $FilePath)

                $fileInfo = Get-Item -Path $FilePath
                $fileSize = $fileInfo.Length # Total file size in bytes
                $fileHash = (Get-FileHash -Path $FilePath -Algorithm MD5).Hash

                # Output initial file metadata as JSON
                [PSCustomObject]@{
                    Type        = "Metadata"
                    FilePath    = $FilePath
                    FileSize    = $fileSize
                    FileHash    = $fileHash
                    FileName    = $fileInfo.Name
                } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout
            } else {
                [PSCustomObject]@{
                    Type        = "Error"
                    Message     = "File hash mismatch. Expected: $FileHash, Calculated: $calculatedHash"
                } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout
            }
        } else {
            [PSCustomObject]@{
                Type        = "Error"
                Message     =  "File hash not provided for verification."
            } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout
        }
    }
    catch {
        $FullExceptionMessage = "$($_.Exception.GetType().FullName): $($_.Exception.Message)"
        [PSCustomObject]@{
            Type        = "Error"
            Message     = "Error processing chunk or writing to file: $FullExceptionMessage"
        } | ConvertTo-Json -Compress | Write-Output # Pipe JSON to stdout
    }
}


================================================
FILE: evil_winrm_py/evil_winrm_py.py
================================================
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
evil-winrm-py
https://github.com/adityatelange/evil-winrm-py
"""

import argparse
import base64
import hashlib
import json
import logging
import os
import re
import shutil
import signal
import sys
import tempfile
import textwrap
import time
import traceback
from importlib import resources
from pathlib import Path

from prompt_toolkit import PromptSession, prompt
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.document import Document
from prompt_toolkit.filters import has_completions
from prompt_toolkit.formatted_text import ANSI
from prompt_toolkit.history import FileHistory
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.shortcuts import clear
from pypsrp.complex_objects import PSInvocationState
from pypsrp.exceptions import AuthenticationError, WinRMTransportError, WSManFaultError
from pypsrp.powershell import PowerShell, RunspacePool
from requests.exceptions import ConnectionError
from spnego.exceptions import NoCredentialError, OperationNotAvailableError, SpnegoError
from tqdm import tqdm

# check if kerberos is installed
try:
    from gssapi.creds import Credentials as GSSAPICredentials
    from gssapi.exceptions import ExpiredCredentialsError, MissingCredentialsError
    from gssapi.raw import Creds as RawCreds
    from krb5._exceptions import Krb5Error

    is_kerb_available = True
except ImportError:
    is_kerb_available = False

    # If kerberos is not available, define a dummy exception
    class Krb5Error(Exception):
        pass


from evil_winrm_py import __version__
from evil_winrm_py.pypsrp_ewp.wsman import WSManEWP

# --- Constants ---
LOG_PATH = Path.cwd().joinpath("evil_winrm_py.log")
HISTORY_FILE = Path.home().joinpath(".evil_winrm_py_history")
HISTORY_LENGTH = 1000
MENU_COMMANDS = {
    "services": {
        "syntax": "services",
        "info": "Show the running services (except system services)",
    },
    "upload": {
        "syntax": "upload <local_path> <remote_path>",
        "info": "Upload a file",
    },
    "download": {
        "syntax": "download <remote_path> <local_path>",
        "info": "Download a file",
    },
    "loadps": {
        "syntax": "loadps <local_path>.ps1",
        "info": "Load PowerShell functions from a local script",
    },
    "runps": {
        "syntax": "runps <local_path>.ps1",
        "info": "Run a local PowerShell script on the remote host",
    },
    "loaddll": {
        "syntax": "loaddll <local_path>.dll",
        "info": "Load a local DLL (in-memory) as a module on the remote host",
    },
    "runexe": {
        "syntax": "runexe <local_path>.exe [args]",
        "info": "Upload and execute (in-memory) a local EXE on the remote host",
    },
    "menu": {
        "syntax": "menu",
        "info": "Show this menu",
    },
    "clear": {
        "syntax": "clear, cls",
        "info": "Clear the screen",
    },
    "exit": {
        "syntax": "exit",
        "info": "Exit the shell",
    },
}
COMMAND_SUGGESTIONS = []

# --- Colors ---
# ANSI escape codes for colored output
RESET = "\033[0m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
BOLD = "\033[1m"


# --- Logging Setup ---
log = logging.getLogger(__name__)


# --- Helper Functions ---
class DelayedKeyboardInterrupt:
    """
    A context manager to delay the handling of a SIGINT (Ctrl+C) signal until
    the enclosed block of code has completed execution.

    This is useful for ensuring that critical sections of code are not
    interrupted by a keyboard interrupt, while still allowing the signal
    to be handled after the block finishes.
    """

    def __enter__(self):
        self.signal_received = False
        self.old_handler = signal.getsignal(signal.SIGINT)

        def handler(sig, frame):
            print(RED + "\n[-] Caught Ctrl+C. Stopping current command..." + RESET)
            self.signal_received = (sig, frame)

        signal.signal(signal.SIGINT, handler)

    def __exit__(self, type, value, traceback):
        signal.signal(signal.SIGINT, self.old_handler)
        if self.signal_received:
            # raise the signal after the task is done
            self.old_handler(*self.signal_received)


def run_ps_cmd(r_pool: RunspacePool, command: str) -> tuple[str, list, bool]:
    """Runs a PowerShell command and returns the output, streams, and error status."""
    log.info("Executing command: {}".format(command))
    ps = PowerShell(r_pool)
    ps.add_cmdlet("Invoke-Expression").add_parameter("Command", command)
    ps.add_cmdlet("Out-String").add_parameter("Stream")
    ps.invoke()
    return "\n".join(ps.output), ps.streams, ps.had_errors


def get_prompt(r_pool: RunspacePool) -> str:
    """Returns the prompt string for the interactive shell."""
    output, streams, had_errors = run_ps_cmd(
        r_pool, "$pwd.Path"
    )  # Get current working directory
    if not had_errors:
        return f"{RED}evil-winrm-py{RESET} {YELLOW}{BOLD}PS{RESET} {output}> "
    return "PS ?> "  # Fallback prompt


def show_menu() -> None:
    """Displays the help menu for interactive commands."""
    print(BOLD + "\nMenu:" + RESET)
    for command in MENU_COMMANDS.values():
        print(f"{CYAN}[+] {command['syntax']:<55} - {command['info']}{RESET}")
    print("Note: Use absolute paths for upload/download for reliability.\n")


def get_directory_and_partial_name(text: str, sep: str) -> tuple[str, str]:
    """
    Parses the input text to find the directory prefix and the partial name.
    """
    if sep not in ["\\", "/"]:
        raise ValueError("Separator must be either '\\' or '/'")
    # Find the last unquoted slash or backslash
    last_sep_index = text.rfind(sep)
    if last_sep_index == -1:
        # No separator found, the whole text is the partial name in the current directory
        directory_prefix = ""
        partial_name = text
    else:
        split_at = last_sep_index + 1
        directory_prefix = text[:split_at]
        partial_name = text[split_at:]
    return directory_prefix, partial_name


def _ps_single_quote(value: str) -> str:
    """Wraps a value in single quotes for PowerShell, escaping embedded quotes."""
    escaped = value.replace("'", "''")
    return f"'{escaped}'"


def get_remote_path_suggestions(
    r_pool: RunspacePool,
    directory_prefix: str,
    partial_name: str,
    dirs_only: bool = False,
) -> list[str]:
    """
    Returns a list of remote path suggestions based on the current directory
    and the partial name entered by the user.
    """

    exp = "FullName"
    attrs = ""
    if not re.match(r"^[a-zA-Z]:", directory_prefix):
        # If the path doesn't start with a drive letter, prepend the current directory
        pwd, streams, had_errors = run_ps_cmd(
            r_pool, "$pwd.Path"
        )  # Get current working directory
        directory_prefix = f"{pwd}\\{directory_prefix}"
        exp = "Name"

    if dirs_only:
        attrs = "-Attributes Directory"

    command = f'Get-ChildItem -LiteralPath "{directory_prefix}" -Filter "{partial_name}*" {attrs} -Fo | select -Exp {exp}'
    ps = PowerShell(r_pool)
    ps.add_cmdlet("Invoke-Expression").add_parameter("Command", command)
    ps.add_cmdlet("Out-String").add_parameter("Stream")
    ps.invoke()
    return ps.output


def get_remote_command_suggestions(
    r_pool: RunspacePool, command_prefix: str
) -> list[str]:
    """
    Returns a list of remote PowerShell command names (cmdlets/aliases) that start
    with the provided prefix.
    """

    prefix_literal = _ps_single_quote(command_prefix or "")
    ps_script = textwrap.dedent(
        f"""
        $prefix = {prefix_literal};
        if ([string]::IsNullOrEmpty($prefix)) {{
            $pattern = '*';
        }} else {{
            $pattern = "$prefix*";
        }}
        $cmds = Get-Command -Name $pattern -ErrorAction SilentlyContinue |
            Select-Object -ExpandProperty Name;
        if (-not $cmds) {{
            $cmds = Get-Alias -Name $pattern -ErrorAction SilentlyContinue |
                Select-Object -ExpandProperty Name;
        }}
        $cmds | Sort-Object -Unique
        """
    ).strip()

    output, _, had_errors = run_ps_cmd(r_pool, ps_script)
    if had_errors:
        return []
    suggestions = [line.strip() for line in output.splitlines() if line.strip()]
    return suggestions


def get_local_path_suggestions(
    directory_prefix: str, partial_name: str, extension: str = None
) -> list[str]:
    """
    Returns a list of local path suggestions based on path entered by the user.
    Optionally filters files by extension (e.g., ".ps1").
    """
    suggestions = []

    # Expand the tilde to the user's home directory
    home = str(Path.home())

    try:
        entries = Path(directory_prefix).expanduser().iterdir()
        for entry in entries:
            if entry.match(f"{partial_name}*"):
                if entry.is_dir():
                    entry = (
                        f"{entry}{os.sep}"  # Append a trailing slash for directories
                    )
                    if directory_prefix.startswith("~"):
                        # If the directory prefix starts with ~, replace home with ~
                        entry = str(entry).replace(home, "~", 1)
                    suggestions.append(str(entry))
                else:
                    if (extension is None) or (
                        entry.suffix.lower() == extension.lower()
                    ):
                        if directory_prefix.startswith("~"):
                            # If the directory prefix starts with ~, replace home with ~
                            entry = str(entry).replace(home, "~", 1)
                        suggestions.append(str(entry))
    except (FileNotFoundError, NotADirectoryError, PermissionError):
        pass
    finally:
        if extension:
            # Sort suggestions alphabetically, prioritizing those that match the extension
            return sorted(suggestions, key=lambda x: not x.endswith(extension))
        return suggestions


class CommandPathCompleter(Completer):
    """
    Completer for command paths in the interactive shell.
    This completer suggests command names based on the user's input.
    """

    def __init__(self, r_pool: RunspacePool):
        self.r_pool = r_pool

    def get_completions(self, document: Document, complete_event):
        dirs_only = False  # Whether to suggest only directories
        text_before_cursor = document.text_before_cursor.lstrip()
        tokens = text_before_cursor.split(maxsplit=1)

        if not tokens:  # Empty input, suggest all commands
            for cmd_sugg in list(MENU_COMMANDS.keys()) + COMMAND_SUGGESTIONS:
                yield Completion(cmd_sugg, start_position=0, display=cmd_sugg)
            return

        command_typed_part = tokens[0]

        # Handle .\name or ./name as first-token paths (run from current remote directory)
        if command_typed_part.startswith(".\\") or command_typed_part.startswith("./"):
            path_being_completed = command_typed_part
            # strip surrounding quotes if any
            if path_being_completed.startswith('"') and path_being_completed.endswith(
                '"'
            ):
                path_being_completed = path_being_completed.strip('"')
            directory_prefix, partial_name = get_directory_and_partial_name(
                path_being_completed, sep="\\"
            )
            suggestions = get_remote_path_suggestions(
                self.r_pool, directory_prefix, partial_name
            )
            for sugg_path in suggestions:
                text_to_insert_in_prompt = f".\\" + sugg_path
                if " " in sugg_path:
                    text_to_insert_in_prompt = f'& ".\\{sugg_path}"'
                yield Completion(
                    text_to_insert_in_prompt,
                    start_position=-len(command_typed_part),
                    display=sugg_path,
                )
            return

        # Case 1: Completing the command name itself
        # There's only one token and no trailing space.
        if len(tokens) == 1 and not text_before_cursor.endswith(" "):
            # User is typing the command, -> "downl"
            seen_commands = set()
            for cmd_sugg in list(MENU_COMMANDS.keys()) + COMMAND_SUGGESTIONS:
                if cmd_sugg.startswith(command_typed_part):
                    seen_commands.add(cmd_sugg.lower())
                    yield Completion(
                        cmd_sugg + " ",  # Full suggested command
                        start_position=-len(
                            command_typed_part
                        ),  # Replace the typed part
                        display=cmd_sugg,
                    )
            remote_cmds = get_remote_command_suggestions(
                self.r_pool, command_typed_part
            )
            lower_prefix = command_typed_part.lower()
            for remote_cmd in remote_cmds:
                cmd_lower = remote_cmd.lower()
                if lower_prefix and not cmd_lower.startswith(lower_prefix):
                    continue
                if cmd_lower in seen_commands:
                    continue
                seen_commands.add(cmd_lower)
                yield Completion(
                    remote_cmd + " ",
                    start_position=-len(command_typed_part),
                    display=remote_cmd,
                )
            return

        # Case 2: Completing a path argument
        path_typed_segment = ""  # What the user has typed for the current path argument
        if len(tokens) == 2:
            path_typed_segment = tokens[1]

        actual_command_name = command_typed_part.strip().lower()

        args = quoted_command_split(path_typed_segment.strip())

        suggestions = []
        current_arg_text_being_completed = ""
        directory_prefix = partial_name = ""

        if actual_command_name == "upload":
            # syntax: upload <local_path> <remote_path>
            num_args_present = len(args)

            if num_args_present == 0:
                # User typed "upload "
                # Completing the 1st argument (local_path), currently empty
                current_arg_text_being_completed = ""
                directory_prefix, partial_name = get_directory_and_partial_name(
                    current_arg_text_being_completed, sep=os.sep
                )
                suggestions = get_local_path_suggestions(directory_prefix, partial_name)
            elif num_args_present == 1:
                # We have one argument part, e.g., "upload arg1" or "upload local_path "
                if path_typed_segment.endswith(" "):
                    # 1st argument (local_path) is complete
                    # Completing the 2nd argument (remote_path), currently empty
                    current_arg_text_being_completed = ""
                    directory_prefix, partial_name = get_directory_and_partial_name(
                        current_arg_text_being_completed, sep="\\"
                    )
                    suggestions = get_remote_path_suggestions(
                        self.r_pool, directory_prefix, partial_name
                    )
                else:
                    # Still completing the 1st argument (local_path), e.g., "upload arg1"
                    current_arg_text_being_completed = path_being_completed = args[0]
                    if path_being_completed.startswith('"'):
                        path_being_completed = current_arg_text_being_completed.strip(
                            '"'
                        )
                    directory_prefix, partial_name = get_directory_and_partial_name(
                        path_being_completed, sep=os.sep
                    )
                    suggestions = get_local_path_suggestions(
                        directory_prefix, partial_name
                    )
            elif num_args_present == 2:
                #  We have two argument parts
                # e.g., "upload local_path arg2" or "upload local_path remote_path "
                if path_typed_segment.endswith(" "):
                    # 2nd argument (remote_path) is complete. No more suggestions for "upload".
                    pass
                else:
                    # Completing the 2nd argument (remote_path), e.g., "upload local_path arg2"
                    current_arg_text_being_completed = path_being_completed = args[1]
                    if path_being_completed.startswith('"'):
                        path_being_completed = current_arg_text_being_completed.strip(
                            '"'
                        )
                    directory_prefix, partial_name = get_directory_and_partial_name(
                        path_being_completed, sep="\\"
                    )
                    suggestions = get_remote_path_suggestions(
                        self.r_pool, directory_prefix, partial_name
                    )
            else:
                # More than 2 arguments, e.g., "upload local_path remote_path extra_arg"
                pass
        elif actual_command_name == "download":
            # syntax: download <remote_path> <local_path>
            num_args_present = len(args)

            if num_args_present == 0:
                # User typed "download "
                # Completing 1st arg (remote_path), empty
                current_arg_text_being_completed = ""
                directory_prefix, partial_name = get_directory_and_partial_name(
                    current_arg_text_being_completed, sep="\\"
                )
                suggestions = get_remote_path_suggestions(
                    self.r_pool, directory_prefix, partial_name
                )
            elif num_args_present == 1:
                # We have "download arg1" or "download local_path "
                if path_typed_segment.endswith(" "):
                    # First arg (remote_path) is complete. Completing 2nd arg (local_path), empty.
                    current_arg_text_being_completed = ""
                    directory_prefix, partial_name = get_directory_and_partial_name(
                        current_arg_text_being_completed, sep=os.sep
                    )
                    suggestions = get_local_path_suggestions(
                        directory_prefix, partial_name
                    )
                else:
                    # Still completing 1st arg (remote_path)
                    current_arg_text_being_completed = path_being_completed = args[0]
                    if path_being_completed.startswith('"'):
                        path_being_completed = current_arg_text_being_completed.strip(
                            '"'
                        )
                    directory_prefix, partial_name = get_directory_and_partial_name(
                        path_being_completed, sep="\\"
                    )
                    suggestions = get_remote_path_suggestions(
                        self.r_pool, directory_prefix, partial_name
                    )
            elif num_args_present == 2:
                # We have two argument parts
                # e.g., "download remote_path arg2" or "download remote_path local_path "
                if path_typed_segment.endswith(" "):
                    # 2nd argument (local_path) is complete. No more suggestions for "download".
                    pass
                else:
                    # Completing 2nd arg (local_path)
                    current_arg_text_being_completed = path_being_completed = args[1]
                    if path_being_completed.startswith('"'):
                        path_being_completed = current_arg_text_being_completed.strip(
                            '"'
                        )
                    directory_prefix, partial_name = get_directory_and_partial_name(
                        path_being_completed, sep=os.sep
                    )
                    suggestions = get_local_path_suggestions(
                        directory_prefix, partial_name
                    )
            else:
                # More than 2 arguments, e.g., "download remote_path local_path extra_arg"
                pass
        elif actual_command_name in ["loadps", "runps"]:
            # syntax: loadps <local_path>
            num_args_present = len(args)

            if num_args_present == 0:
                # User typed "loadps "
                # Completing the 1st argument (local_path), currently empty
                current_arg_text_being_completed = ""
                directory_prefix, partial_name = get_directory_and_partial_name(
                    current_arg_text_being_completed, sep=os.sep
                )
                suggestions = get_local_path_suggestions(
                    directory_prefix, partial_name, extension=".ps1"
                )
            elif num_args_present == 1:
                # We have "loadps arg1" or "loadps local_path "
                if path_typed_segment.endswith(" "):
                    # 1st argument (local_path) is complete, currently empty.
                    current_arg_text_being_completed = ""
                    directory_prefix, partial_name = get_directory_and_partial_name(
                        current_arg_text_being_completed, sep=os.sep
                    )
                    suggestions = get_local_path_suggestions(
                        directory_prefix, partial_name, extension=".ps1"
                    )
                else:
                    # Still completing the 1st argument (local_path)
                    current_arg_text_being_completed = path_being_completed = args[0]
                    if path_being_completed.startswith('"'):
                        path_being_completed = current_arg_text_being_completed.strip(
                            '"'
                        )
                    directory_prefix, partial_name = get_directory_and_partial_name(
                        path_being_completed, sep=os.sep
                    )
                    suggestions = get_local_path_suggestions(
                        directory_prefix, partial_name, extension=".ps1"
                    )
            else:
                # More than 1 argument, e.g., "loadps local_path extra_arg"
                pass
        elif actual_command_name in ["loaddll"]:
            # syntax: loaddll <local_path>
            num_args_present = len(args)

            if num_args_present == 0:
                # User typed "loaddll "
                # Completing the 1st argument (local_path), currently empty
                current_arg_text_being_completed = ""
                directory_prefix, partial_name = get_directory_and_partial_name(
                    current_arg_text_being_completed, sep=os.sep
                )
                suggestions = get_local_path_suggestions(
                    directory_prefix, partial_name, extension=".dll"
                )
            elif num_args_present == 1:
                # We have "loaddll arg1" or "loaddll local_path "
                if path_typed_segment.endswith(" "):
                    # 1st argument (local_path) is complete, currently empty.
                    current_arg_text_being_completed = ""
                    directory_prefix, partial_name = get_directory_and_partial_name(
                        current_arg_text_being_completed, sep=os.sep
                    )
                    suggestions = get_local_path_suggestions(
                        directory_prefix, partial_name, extension=".dll"
                    )
                else:
                    # Still completing the 1st argument (local_path)
                    current_arg_text_being_completed = path_being_completed = args[0]
                    if path_being_completed.startswith('"'):
                        path_being_completed = current_arg_text_being_completed.strip(
                            '"'
                        )
                    directory_prefix, partial_name = get_directory_and_partial_name(
                        path_being_completed, sep=os.sep
                    )
                    suggestions = get_local_path_suggestions(
                        directory_prefix, partial_name, extension=".dll"
                    )
            else:
                # More than 1 argument, e.g., "loaddll local_path extra_arg"
                pass
        elif actual_command_name in ["runexe"]:
            # syntax: runexe <local_path>
            num_args_present = len(args)

            if num_args_present == 0:
                # User typed "runexe "
                # Completing the 1st argument (local_path), currently empty
                current_arg_text_being_completed = ""
                directory_prefix, partial_name = get_directory_and_partial_name(
                    current_arg_text_being_completed, sep=os.sep
                )
                suggestions = get_local_path_suggestions(
                    directory_prefix, partial_name, extension=".exe"
                )
            elif num_args_present == 1:
                # We have "runexe arg1" or "runexe local_path "
                if path_typed_segment.endswith(" "):
                    # 1st argument (local_path) is complete, currently empty.
                    current_arg_text_being_completed = ""
                    directory_prefix, partial_name = get_directory_and_partial_name(
                        current_arg_text_being_completed, sep=os.sep
                    )
                    suggestions = get_local_path_suggestions(
                        directory_prefix, partial_name, extension=".exe"
                    )
                else:
                    # Still completing the 1st argument (local_path)
                    current_arg_text_being_completed = path_being_completed = args[0]
                    if path_being_completed.startswith('"'):
                        path_being_completed = current_arg_text_being_completed.strip(
                            '"'
                        )
                    directory_prefix, partial_name = get_directory_and_partial_name(
                        path_being_completed, sep=os.sep
                    )
                    suggestions = get_local_path_suggestions(
                        directory_prefix, partial_name, extension=".exe"
                    )
            else:
                # More than 1 argument, e.g., "runexe local_path extra_arg"
                pass
        else:
            if actual_command_name == "cd":
                dirs_only = True

            current_arg_text_being_completed = path_being_completed = path_typed_segment

            if path_being_completed.startswith('"'):
                path_being_completed = current_arg_text_being_completed.strip('"')

            directory_prefix, partial_name = get_directory_and_partial_name(
                path_being_completed, sep="\\"
            )
            suggestions = get_remote_path_suggestions(
                self.r_pool, directory_prefix, partial_name, dirs_only
            )

        for sugg_path in suggestions:

            # If the path doesn't start with a drive letter, prepend the directory_prefix
            if (
                not re.match(r"^[a-zA-Z]:", directory_prefix)
                and directory_prefix
                and directory_prefix.endswith("\\")
            ):
                sugg_path = f"{directory_prefix}{sugg_path}"

            text_to_insert_in_prompt = sugg_path

            if " " in sugg_path:
                # If the path contains spaces, quote it
                text_to_insert_in_prompt = f'"{sugg_path}"'

            yield Completion(
                text_to_insert_in_prompt,
                start_position=-len(
                    current_arg_text_being_completed
                ),  # Use the length of quoted part
                display=sugg_path,
            )


def get_ps_script(script_name: str) -> str:
    """
    Returns the content of a PowerShell script from the package resources.
    """
    try:
        with resources.path("evil_winrm_py._ps", script_name) as script_path:
            return script_path.read_text()
    except FileNotFoundError:
        print(RED + f"[-] Script {script_name} not found." + RESET)
        log.error(f"Script {script_name} not found.")
        return ""


def quoted_command_split(command: str) -> list[str]:
    """
    Splits a command string into parts, respecting quoted strings.
    This is useful for handling paths with spaces or special characters.
    """
    actual_command_parts = []
    continuation = False
    cursor = 0

    command_parts = command.split(" ")
    for part in command_parts:
        if not part:
            continue
        if continuation:
            actual_command_parts[cursor] = actual_command_parts[cursor] + " " + part
            if part.endswith('"'):
                continuation = False
                cursor += 1
        else:
            if part.startswith('"'):
                actual_command_parts += [part]
                continuation = True
            elif part.find('"') != -1:
                # #TODO: decide later how to handle this case
                pass
            else:
                actual_command_parts += [part]
                cursor += 1
    return actual_command_parts


def download_file(r_pool: RunspacePool, remote_path: str, local_path: str) -> None:
    ps = PowerShell(r_pool)
    script = get_ps_script("fetch.ps1")
    ps.add_script(script)
    ps.add_parameter("FilePath", remote_path)
    ps.begin_invoke()

    ts = int(time.time())
    tmp_file_path = Path(tempfile.gettempdir()) / f"evil-winrm-py.file_{ts}.tmp"

    try:
        # Create a temporary file to store the downloaded data
        with open(tmp_file_path, "ab+") as bin:
            cursor = 0
            metadata = {}
            while ps.state == PSInvocationState.RUNNING:
                with DelayedKeyboardInterrupt():
                    ps.poll_invoke()
                output = ps.output
                if cursor == 0:
                    line = json.loads(output[0])
                    if line["Type"] == "Error":
                        print(RED + f"[-] Error: {line['Message']}" + RESET)
                        log.error(f"Error: {line['Message']}")
                        return
                    elif line["Type"] == "Metadata":
                        metadata = line
                    pbar = tqdm(
                        total=metadata["FileSize"],
                        unit="B",
                        unit_scale=True,
                        unit_divisor=1024,
                        desc=f"Downloading {remote_path}",
                        dynamic_ncols=True,
                        mininterval=0.1,
                    )
                for line in output[cursor:]:
                    line = json.loads(line)
                    if line["Type"] == "Chunk":
                        Base64Data = line["Base64Data"]
                        chunk = base64.b64decode(Base64Data)
                        bin.write(chunk)
                        pbar.update(metadata["ChunkSize"])
                    if line["Type"] == "Error":
                        print(RED + f"[-] Error: {line['Message']}" + RESET)
                        log.error(f"Error: {line['Message']}")
                        return
                cursor = len(output)
            pbar.close()
            bin.close()

        if ps.had_errors:
            if ps.streams.error:
                for error in ps.streams.error:
                    print(error)

    except KeyboardInterrupt:
        if "pbar" in locals() and pbar:
            pbar.leave = (
                False  # Make the progress bar disappear on close after interrupt
            )
            pbar.close()
        Path(tmp_file_path).unlink(missing_ok=True)
        if ps.state == PSInvocationState.RUNNING:
            log.info("Stopping command execution.")
            ps.stop()
        return

    # Verify the downloaded file's hash
    hexdigest = hashlib.md5(open(tmp_file_path, "rb").read()).hexdigest()
    if metadata["FileHash"].lower() == hexdigest:
        # If the hash matches, rename the temporary file to the final name
        tmp_file_path = Path(tmp_file_path)
        try:
            shutil.move(tmp_file_path, local_path)
        except Exception as e:
            print(RED + f"[-] Error saving file: {e}" + RESET)
            log.error(f"Error saving file: {e}")
            return
        print(
            GREEN
            + "[+] File downloaded successfully and saved as: "
            + local_path
            + RESET
        )
        log.info("File downloaded successfully and saved as: {}".format(local_path))
    else:
        print(RED + "[-] File hash mismatch. Downloaded file may be corrupted." + RESET)
        log.error("File hash mismatch. Downloaded file may be corrupted.")


def upload_file(r_pool: RunspacePool, local_path: str, remote_path: str) -> None:
    hexdigest = hashlib.md5(open(local_path, "rb").read()).hexdigest().upper()
    with open(local_path, "rb") as bin:
        file_size = Path(local_path).stat().st_size
        chunk_size_bytes = 65536  # 64 KB
        total_chunks = (file_size + chunk_size_bytes - 1) // chunk_size_bytes
        metadata = {"FileHash": ""}  # Declare a psuedo metadata

        pbar = tqdm(
            total=file_size,
            unit="B",
            unit_scale=True,
            unit_divisor=1024,
            desc=f"Uploading {local_path}",
            dynamic_ncols=True,
            mininterval=0.1,
        )
        try:
            temp_file_path = ""
            for i in range(total_chunks):
                start_offset = i * chunk_size_bytes
                bin.seek(start_offset)
                chunk = bin.read(chunk_size_bytes)

                if not chunk:  # End of file
                    break

                elif i == 0:
                    chunk_type = 0  # First chunk, tells PS script to create file
                    if len(chunk) < chunk_size_bytes:
                        chunk_type = 3
                elif i == total_chunks - 1:
                    chunk_type = 1  # Last chunk, tells PS script to calculate hash
                else:
                    chunk_type = 2  # Intermediate chunk

                base64_chunk = base64.b64encode(chunk).decode("utf-8")

                script = get_ps_script("send.ps1")
                with DelayedKeyboardInterrupt():
                    ps = PowerShell(r_pool)
                    ps.add_script(script)
                    ps.add_parameter("Base64Chunk", base64_chunk)
                    ps.add_parameter("ChunkType", chunk_type)

                    if chunk_type == 1:
                        # If it's the last chunk, we provide the file path and hash
                        ps.add_parameter("TempFilePath", temp_file_path)
                        ps.add_parameter("FilePath", remote_path)
                        ps.add_parameter("FileHash", hexdigest)
                    elif chunk_type == 2:
                        ps.add_parameter("TempFilePath", temp_file_path)
                    elif chunk_type == 3:
                        ps.add_parameter("FilePath", remote_path)
                        ps.add_parameter("FileHash", hexdigest)

                    ps.begin_invoke()

                    while ps.state == PSInvocationState.RUNNING:
                        ps.poll_invoke()
                output = ps.output

                for line in output:
                    line = json.loads(line)
                    if line["Type"] == "Metadata":
                        metadata = line
                        if "TempFilePath" in metadata:
                            temp_file_path = metadata["TempFilePath"]

                    if line["Type"] == "Error":
                        print(RED + f"[-] Error: {line['Message']}" + RESET)
                        log.error(f"Error: {line['Message']}")
                        pbar.leave = False  # Make the progress bar disappear on close
                        return
                if ps.had_errors:
                    if ps.streams.error:
                        for error in ps.streams.error:
                            print(error)
                if chunk_type == 3:
                    pbar.update(file_size)
                else:
                    pbar.update(chunk_size_bytes)
            pbar.close()

            # Verify the downloaded file's hash
            if metadata["FileHash"] == hexdigest:
                print(
                    GREEN
                    + "[+] File uploaded successfully as: "
                    + metadata["FilePath"]
                    + RESET
                )
                log.info(
                    "File uploaded successfully as: {}".format(metadata["FilePath"])
                )
            else:
                print(
                    RED
                    + "[-] File hash mismatch. Uploaded file may be corrupted."
                    + RESET
                )
                log.error("File hash mismatch. Uploaded file may be corrupted.")

        except KeyboardInterrupt:
            if "pbar" in locals() and pbar:
                pbar.leave = (
                    False  # Make the progress bar disappear on close after interrupt
                )
                pbar.close()
            if ps.state == PSInvocationState.RUNNING:
                log.info("Stopping command execution.")
                ps.stop()


def _read_text_auto_encoding(path) -> str:
    """
    Reads file with enc utf-8-sig, utf-8, utf-16, and latin-1.
    Tries multiple encodings to read the file and returns the content as a string.
    Raises UnicodeDecodeError/Exception if all encodings fail.
    """
    text_file_encodings = ["utf-8-sig", "utf-8", "utf-16", "latin-1"]
    for enc in text_file_encodings:
        try:
            with open(path, "r", encoding=enc) as f:
                text = f.read()
            log.debug(f"Read '{path}' using encoding {enc}")
            return text
        except UnicodeDecodeError:
            continue
        except Exception as e:
            raise
    raise UnicodeDecodeError("All preferred encodings failed for file: {}".format(path))


def load_ps(r_pool: RunspacePool, local_path: str):
    ps = PowerShell(r_pool)
    try:
        try:
            script = _read_text_auto_encoding(local_path)
        except Exception as e:
            print(RED + f"[-] Error reading ps script file: {e}" + RESET)
            log.error(f"Error reading ps script file: {e}")
            return
        # Remove block comments (<#...#>) to avoid matching commented-out functions
        content = re.sub(r"<#.*?#>", "", script, flags=re.DOTALL)
        # Find all function names in the script
        pattern = r"function\s+([a-zA-Z0-9_-]+)\s*(?={|$)"
        function_names = re.findall(pattern, content, re.MULTILINE | re.IGNORECASE)

        ps.add_script(f". {{ {script} }}")  # Dot sourcing the script
        ps.begin_invoke()

        while ps.state == PSInvocationState.RUNNING:
            with DelayedKeyboardInterrupt():
                ps.poll_invoke()

        if ps.streams.error:
            print(RED + "[-] Failed to load PowerShell script." + RESET)
            log.error(f"Failed to load PowerShell script '{local_path}'.")
            for error in ps.streams.error:
                print(RED + error._to_string + RESET)
                log.error("Error: {}".format(error._to_string))
                log.error("\tCategoryInfo: {}".format(error.message))
                log.error("\tFullyQualifiedErrorId: {}".format(error.fq_error))
        else:
            print(GREEN + "[+] PowerShell script loaded successfully." + RESET)
            log.info(f"PowerShell script '{local_path}' loaded successfully.")
            global COMMAND_SUGGESTIONS
            # Update the command suggestions with the function names
            new_suggestions = []
            for func in function_names:
                if func not in COMMAND_SUGGESTIONS:
                    new_suggestions += [func]
            if new_suggestions:
                COMMAND_SUGGESTIONS += new_suggestions
                print(
                    CYAN
                    + "[*] New commands available (use TAB to autocomplete):"
                    + RESET
                )
                print(", ".join(new_suggestions))
    except KeyboardInterrupt:
        if ps.state == PSInvocationState.RUNNING:
            log.info("Stopping command execution.")
            ps.stop()


def run_ps(r_pool: RunspacePool, local_path: str) -> None:
    """Runs a local PowerShell script on the remote host."""
    ps = PowerShell(r_pool)
    try:
        try:
            script = _read_text_auto_encoding(local_path)
        except Exception as e:
            print(RED + f"[-] Error reading ps script file: {e}" + RESET)
            log.error(f"Error reading ps script file: {e}")
            return

        ps.add_script(script)
        ps.begin_invoke()

        cursor = 0
        while ps.state == PSInvocationState.RUNNING:
            with DelayedKeyboardInterrupt():
                ps.poll_invoke()
            output = ps.output
            for line in output[cursor:]:
                print(line)
            cursor = len(output)

        if ps.streams.error:
            print(RED + "[-] Failed to run PowerShell script." + RESET)
            log.error(f"Failed to run PowerShell script '{local_path}'.")
            for error in ps.streams.error:
                print(RED + error._to_string + RESET)
                log.error("Error: {}".format(error._to_string))
                log.error("\tCategoryInfo: {}".format(error.message))
                log.error("\tFullyQualifiedErrorId: {}".format(error.fq_error))
        else:
            print(GREEN + "[+] PowerShell script ran successfully." + RESET)
            log.info(f"PowerShell script '{local_path}' ran successfully.")
    except KeyboardInterrupt:
        if ps.state == PSInvocationState.RUNNING:
            log.info("Stopping command execution.")
            ps.stop()


def load_dll(r_pool: RunspacePool, local_path: str) -> None:
    """Uploads in-memory and loads a local DLL on the remote host, then invokes a specified function."""
    ps = PowerShell(r_pool)
    try:
        with open(local_path, "rb") as dll_file:
            dll_data = dll_file.read()
            base64_dll = base64.b64encode(dll_data).decode("utf-8")

        script = get_ps_script("loaddll.ps1")
        ps.add_script(script)
        ps.add_parameter("Base64Dll", base64_dll)
        ps.begin_invoke()

        cursor = 0
        name = ""
        while ps.state == PSInvocationState.RUNNING:
            with DelayedKeyboardInterrupt():
                ps.poll_invoke()
            output = ps.output
            for line in output[cursor:]:
                line = json.loads(line)
                if line["Type"] == "Error":
                    print(RED + f"[-] Error: {line['Message']}" + RESET)
                    log.error(f"Error: {line['Message']}")
                    return
                elif line["Type"] == "Metadata":
                    if "Name" in line:
                        name = line["Name"]
                        print(GREEN + f"[+] Loading '{name}' as a module..." + RESET)
                        log.info(f"Loading '{name}' as a module...")
                    elif "Funcs" in line:
                        print(
                            CYAN
                            + "[*] New commands available available (use TAB to autocomplete):"
                            + RESET
                        )
                        print(", ".join(line["Funcs"]))
                        global COMMAND_SUGGESTIONS
                        new_suggestions = []
                        for func in line["Funcs"]:
                            if func not in COMMAND_SUGGESTIONS:
                                new_suggestions += [func]
                        if new_suggestions:
                            COMMAND_SUGGESTIONS += new_suggestions
            cursor = len(output)

        if ps.streams.error:
            print(RED + "[-] Failed to load DLL" + RESET)
            log.error(f"Failed to load DLL '{local_path}'")
            for error in ps.streams.error:
                print(RED + error._to_string + RESET)
                log.error("Error: {}".format(error._to_string))
                log.error("\tCategoryInfo: {}".format(error.message))
                log.error("\tFullyQualifiedErrorId: {}".format(error.fq_error))
        else:
            print(GREEN + f"[+] DLL '{name}' loaded successfully." + RESET)
            log.info(f"DLL '{local_path}' loaded successfully.")
    except KeyboardInterrupt:
        if ps.state == PSInvocationState.RUNNING:
            log.info("Stopping command execution.")
            ps.stop()


def run_exe(r_pool: RunspacePool, local_path: str, args: str = "") -> None:
    """Uploads in-memory and runs a local executable on the remote host."""
    ps = PowerShell(r_pool)
    file_path = Path(local_path)
    file_size = file_path.stat().st_size
    print(
        BLUE + f"[*] Uploading in-memory ({file_size} bytes) and executing..." + RESET
    )
    log.info(f"Uploading in-memory {file_size} bytes and executing...")
    try:
        with open(local_path, "rb") as exe_file:
            exe_data = exe_file.read()
            base64_exe = base64.b64encode(exe_data).decode("utf-8")

        script = get_ps_script("exec.ps1")
        ps.add_script(script)
        ps.add_parameter("Base64Exe", base64_exe)
        ps.add_parameter("Args", args.split(" "))
        ps.begin_invoke()

        cursor = 0
        while ps.state == PSInvocationState.RUNNING:
            with DelayedKeyboardInterrupt():
                ps.poll_invoke()
            output = ps.output
            for line in output[cursor:]:
                print(line)
            cursor = len(output)

        if ps.streams.error:
            print(RED + "[-] Failed to run executable." + RESET)
            log.error(f"Failed to run executable '{local_path}'.")
            for error in ps.streams.error:
                print(RED + error._to_string + RESET)
                log.error("Error: {}".format(error._to_string))
                log.error("\tCategoryInfo: {}".format(error.message))
                log.error("\tFullyQualifiedErrorId: {}".format(error.fq_error))
        else:
            print(GREEN + "[+] Executable ran successfully." + RESET)
            log.info(f"Executable '{local_path}' ran successfully.")
    except KeyboardInterrupt:
        if ps.state == PSInvocationState.RUNNING:
            log.info("Stopping command execution.")
            ps.stop()


def interactive_shell(r_pool: RunspacePool) -> None:
    """Runs the interactive pseudo-shell."""
    log.info("Starting interactive PowerShell session...")

    # Set up history file
    if not HISTORY_FILE.exists():
        Path(HISTORY_FILE).touch()
    prompt_history = FileHistory(HISTORY_FILE)
    prompt_session = PromptSession(history=prompt_history)

    # Set up command completer
    completer = CommandPathCompleter(r_pool)

    # Set up key bindings
    kb = KeyBindings()

    @kb.add("enter", filter=has_completions)
    def _(event):
        """Accept the highlighted completion without executing the command."""
        event.current_buffer.apply_completion(
            event.current_buffer.complete_state.current_completion or Completion("", 0)
        )

    while True:
        try:
            try:
                prompt_text = ANSI(get_prompt(r_pool))
            except (KeyboardInterrupt, EOFError):
                return
            command = prompt_session.prompt(
                prompt_text,
                completer=completer,
                complete_while_typing=False,
                key_bindings=kb,
            )

            if not command:
                continue

            # Normalize command input
            command_lower = str(command).strip().lower()

            # Check for exit command
            if command_lower == "exit":
                log.info("Exiting interactive shell.")
                return
            elif command_lower in ["clear", "cls"]:
                log.info("Clearing the screen.")
                clear()  # Clear the screen
                continue
            elif command_lower == "menu":
                log.info("Displaying menu.")
                show_menu()
                continue
            elif command_lower == "services":
                log.info("Displaying services.")
                get_services_command = (
                    "Get-ItemProperty 'Registry::HKLM\\System\\CurrentControlSet\\Services\\*' -ErrorAction "
                    "SilentlyContinue | Where-Object { $_.ImagePath -and ($_.ImagePath -notmatch 'system') } "
                    "| Select-Object @{n='Service';e={$_.PSChildName}}, @{n='Path';e={$_.ImagePath}}"
                )
                services, streams, had_errors = run_ps_cmd(r_pool, get_services_command)
                if not services:
                    print(RED + "[-] Can not retrieve service information" + RESET)
                    continue
                print(services)
                continue

            elif command_lower.startswith("download"):
                command_parts = quoted_command_split(command)
                if len(command_parts) < 3:
                    print(
                        RED + "[-] Usage: download <remote_path> <local_path>" + RESET
                    )
                    continue
                remote_path = command_parts[1].strip('"')
                local_path = command_parts[2].strip('"').strip("'")

                remote_file, streams, had_errors = run_ps_cmd(
                    r_pool, f"(Resolve-Path -Path '{remote_path}').Path"
                )
                if not remote_file:
                    print(
                        RED
                        + f"[-] Remote file '{remote_path}' does not exist or you do not have permission to access it."
                        + RESET
                    )
                    continue

                file_name = remote_file.split("\\")[-1]

                if Path(local_path).expanduser().is_dir() or local_path.endswith(
                    os.sep
                ):
                    local_path = (
                        Path(local_path).expanduser().resolve().joinpath(file_name)
                    )
                else:
                    local_path = Path(local_path).expanduser().resolve()

                download_file(r_pool, remote_file, str(local_path))
                continue
            elif command_lower.startswith("upload"):
                command_parts = quoted_command_split(command)
                if len(command_parts) < 3:
                    print(RED + "[-] Usage: upload <local_path> <remote_path>" + RESET)
                    continue
                local_path = command_parts[1].strip('"').strip("'")
                remote_path = command_parts[2].strip('"')

                if not Path(local_path).expanduser().exists():
                    print(
                        RED + f"[-] Local file '{local_path}' does not exist." + RESET
                    )
                    continue

                file_name = local_path.split(os.sep)[-1]

                if not re.match(r"^[a-zA-Z]:", remote_path):
                    # If the path doesn't start with a drive letter, prepend the current directory
                    pwd, streams, had_errors = run_ps_cmd(r_pool, "$pwd.Path")
                    if remote_path == ".":
                        remote_path = f"{pwd}\\{file_name}"
                    else:
                        remote_path = f"{pwd}\\{remote_path}"

                if remote_path.endswith("\\"):
                    remote_path = f"{remote_path}{file_name}"

                upload_file(
                    r_pool, str(Path(local_path).expanduser().resolve()), remote_path
                )
                continue
            elif command_lower.startswith("loadps"):
                command_parts = quoted_command_split(command)
                if len(command_parts) < 2:
                    print(RED + "[-] Usage: loadps <local_path>" + RESET)
                    continue
                local_path = command_parts[1].strip('"')
                local_path = Path(local_path).expanduser().resolve()

                if not local_path.exists():
                    print(
                        RED
                        + f"[-] Local PowerShell script '{local_path}' does not exist."
                        + RESET
                    )
                    continue
                elif local_path.suffix.lower() != ".ps1":
                    print(
                        RED
                        + "[-] Please provide a valid PowerShell script file with .ps1 extension."
                        + RESET
                    )
                    continue

                load_ps(r_pool, local_path)
                continue
            elif command_lower.startswith("runps"):
                command_parts = quoted_command_split(command)
                if len(command_parts) < 2:
                    print(RED + "[-] Usage: runps <local_path>" + RESET)
                    continue
                local_path = command_parts[1].strip('"')
                local_path = Path(local_path).expanduser().resolve()

                if not local_path.exists():
                    print(
                        RED
                        + f"[-] Local PowerShell script '{local_path}' does not exist."
                        + RESET
                    )
                    continue
                elif local_path.suffix.lower() != ".ps1":
                    print(
                        RED
                        + "[-] Please provide a valid PowerShell script file with .ps1 extension."
                        + RESET
                    )
                    continue

                run_ps(r_pool, local_path)
                continue
            elif command_lower.startswith("loaddll"):
                command_parts = quoted_command_split(command)
                if len(command_parts) < 2:
                    print(RED + "[-] Usage: loaddll <local_path>" + RESET)
                    continue
                local_path = command_parts[1].strip('"')
                local_path = Path(local_path).expanduser().resolve()

                if not local_path.exists():
                    print(RED + f"[-] Local dll '{local_path}' does not exist." + RESET)
                    continue
                elif local_path.suffix.lower() != ".dll":
                    print(
                        RED
                        + "[-] Please provide a valid dll file with .dll extension."
                        + RESET
                    )
                    continue
                load_dll(r_pool, local_path)
                continue
            elif command_lower.startswith("runexe"):
                command_parts = quoted_command_split(command)
                if len(command_parts) < 2:
                    print(RED + "[-] Usage: runexe <local_path> [args]" + RESET)
                    continue
                local_path = command_parts[1].strip('"')
                local_path = Path(local_path).expanduser().resolve()

                if not local_path.exists():
                    print(
                        RED
                        + f"[-] Local executable '{local_path}' does not exist."
                        + RESET
                    )
                    continue
                elif local_path.suffix.lower() != ".exe":
                    print(
                        RED
                        + "[-] Please provide a valid executable file with .exe extension."
                        + RESET
                    )
                    continue

                args = " ".join(command_parts[2:]) if len(command_parts) > 2 else ""

                run_exe(r_pool, local_path, args)
                continue
            else:
                try:
                    ps = PowerShell(r_pool)
                    ps.add_cmdlet("Invoke-Expression").add_parameter("Command", command)
                    ps.add_cmdlet("Out-String").add_parameter("Stream")
                    ps.begin_invoke()
                    log.info("Executing command: {}".format(command))

                    cursor = 0
                    while ps.state == PSInvocationState.RUNNING:
                        with DelayedKeyboardInterrupt():
                            ps.poll_invoke()
                        output = ps.output
                        for line in output[cursor:]:
                            print(line)
                        cursor = len(output)
                    log.info("Command execution completed.")
                    log.info("Output: {}".format("\n".join(output)))

                    if ps.streams.error:
                        for error in ps.streams.error:
                            print(RED + error._to_string + RESET)
                            log.error("Error: {}".format(error._to_string))
                            log.error("\tCategoryInfo: {}".format(error.message))
                            log.error(
                                "\tFullyQualifiedErrorId: {}".format(error.fq_error)
                            )
                except KeyboardInterrupt:
                    if ps.state == PSInvocationState.RUNNING:
                        log.info("Stopping command execution.")
                        ps.stop()
        except KeyboardInterrupt:
            print("\nCaught Ctrl+C. Type 'exit' or press Ctrl+D to exit.")
            continue  # Allow user to continue or type exit
        except EOFError:
            return  # Exit on Ctrl+D


# --- Main Function ---
def main():
    print(
        """          _ _            _                             
  _____ _(_| |_____ __ _(_)_ _  _ _ _ __ ___ _ __ _  _ 
 / -_\\ V | | |___\\ V  V | | ' \\| '_| '  |___| '_ | || |
 \\___|\\_/|_|_|    \\_/\\_/|_|_||_|_| |_|_|_|  | .__/\\_, |
                                            |_|   |__/  v{}\n""".format(
            __version__
        )
    )
    parser = argparse.ArgumentParser(
        epilog="For more information about this project, visit https://github.com/adityatelange/evil-winrm-py"
        "\nFor user guide, visit https://github.com/adityatelange/evil-winrm-py/blob/main/docs/usage.md",
        formatter_class=argparse.RawTextHelpFormatter,
    )

    parser.add_argument(
        "-i",
        "--ip",
        required=True,
        help="remote host IP or hostname",
    )
    parser.add_argument("-u", "--user", help="username")
    parser.add_argument("-p", "--password", help="password")
    parser.add_argument("-H", "--hash", help="nthash")
    parser.add_argument(
        "--priv-key-pem",
        help="local path to private key PEM file",
    )
    parser.add_argument(
        "--cert-pem",
        help="local path to certificate PEM file",
    )
    parser.add_argument("--uri", default="wsman", help="wsman URI (default: /wsman)")
    parser.add_argument(
        "--ua",
        default="Microsoft WinRM Client",
        help='user agent for the WinRM client (default: "Microsoft WinRM Client")',
    )
    parser.add_argument(
        "--port", type=int, default=5985, help="remote host port (default 5985)"
    )
    if is_kerb_available:
        parser.add_argument(
            "--spn-prefix",
            help="specify spn prefix",
        )
        parser.add_argument(
            "--spn-hostname",
            help="specify spn hostname",
        )
        parser.add_argument(
            "-k", "--kerberos", action="store_true", help="use kerberos authentication"
        )
    parser.add_argument(
        "--no-pass", action="store_true", help="do not prompt for password"
    )
    parser.add_argument("--ssl", action="store_true", help="use ssl")
    parser.add_argument("--log", action="store_true", help="log session to file")
    parser.add_argument("--debug", action="store_true", help="enable debug logging")
    parser.add_argument("--no-colors", action="store_true", help="disable colors")
    parser.add_argument(
        "--version", action="version", version=__version__, help="show version"
    )

    args = parser.parse_args()

    # Set Default values
    auth = "ntlm"  # this can be 'negotiate'
    encryption = "auto"
    username = args.user

    # --- Run checks on provided arguments ---
    if args.no_colors:
        global RESET, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, BOLD
        RESET = RED = GREEN = YELLOW = BLUE = MAGENTA = CYAN = BOLD = ""

    if args.cert_pem or args.priv_key_pem:
        auth = "certificate"
        encryption = "never"
        args.ssl = True
        args.no_pass = True
        if not args.cert_pem or not args.priv_key_pem:
            print(
                RED
                + "[-] Both cert.pem and priv-key.pem must be provided for certificate authentication."
                + RESET
            )
            sys.exit(1)

    if args.hash and args.password:
        print(RED + "[-] You cannot use both password and hash." + RESET)
        sys.exit(1)

    if args.hash:
        ntlm_hash_pattern = r"^[0-9a-fA-F]{32}$"
        if re.match(ntlm_hash_pattern, args.hash):
            args.password = "00000000000000000000000000000000:{}".format(args.hash)
        else:
            print(RED + "[-] Invalid NTLM hash format." + RESET)
            sys.exit(1)

    if args.uri:
        if args.uri.startswith("/"):
            args.uri = args.uri.lstrip("/")

    if args.ssl and (args.port == 5985):
        args.port = 5986

    if args.log or args.debug:
        level = logging.INFO
        # Disable all loggers except the root logger
        if args.debug:
            print(BLUE + "[*] Debug logging enabled." + RESET)
            level = logging.DEBUG
            os.environ["KRB5_TRACE"] = str(LOG_PATH)  # Enable Kerberos trace logging
        else:
            # Disable all loggers except the root logger
            for name in logging.root.manager.loggerDict:
                if not name.startswith("evil_winrm_py"):
                    logging.getLogger(name).disabled = True
        # Set up logging to a file
        try:
            logging.basicConfig(
                level=level,
                format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
                filename=LOG_PATH,
            )
            print(BLUE + "[*] Logging session to {}".format(LOG_PATH) + RESET)
        except PermissionError as pe:
            print(
                RED + "[-] Permission denied to write to log file '{}'."
                " Please check the permissions or run with elevated privileges.".format(
                    LOG_PATH
                )
                + RESET
            )
            log.disabled = True
    else:
        log.disabled = True

    # --- Initialize WinRM Session ---
    log.info("--- Evil-WinRM-Py v{} started ---".format(__version__))
    try:
        if is_kerb_available:
            if args.kerberos:
                auth = "kerberos"
                args.spn_prefix = (
                    args.spn_prefix or "http"
                )  # can also be cifs, ldap, HOST
                if not args.user:
                    try:
                        cred = GSSAPICredentials(RawCreds())
                        username = cred.name
                    except MissingCredentialsError:
                        print(
                            MAGENTA
                            + "[%] No credentials cache found for Kerberos authentication."
                            + RESET
                        )
                        sys.exit(1)
                    except ExpiredCredentialsError as ece:
                        print(
                            RED + "[-] The Kerberos credentials have expired. " + RESET
                        )
                        log.error("Expired credentials error: {}".format(ece))
                        sys.exit(1)
                # User needs to set environment variables `KRB5CCNAME` and `KRB5_CONFIG` as per requirements
                # example: export KRB5CCNAME=/tmp/krb5cc_1000
                # example: export KRB5_CONFIG=/etc/krb5.conf
            elif args.spn_prefix or args.spn_hostname:
                args.spn_prefix = args.spn_hostname = None  # Reset to None
                print(
                    MAGENTA
                    + "[%] SPN prefix/hostname is only used with Kerberos authentication."
                    + RESET
                )
        else:
            args.spn_prefix = args.spn_hostname = None

        if args.no_pass:
            args.password = None
        elif args.user and not args.password:
            args.password = prompt("Password: ", is_password=True)
            if not args.password:
                args.password = None

        if username:
            log.info(
                "[*] Connecting to '{}:{}' as '{}'"
                "".format(args.ip, args.port, username, auth)
            )
            print(
                BLUE + "[*] Connecting to '{}:{}' as '{}'"
                "".format(args.ip, args.port, username) + RESET
            )
        else:
            log.info("[*] Connecting to '{}:{}'".format(args.ip, args.port))
            print(BLUE + "[*] Connecting to '{}:{}'".format(args.ip, args.port) + RESET)

        with WSManEWP(
            server=args.ip,
            port=args.port,
            auth=auth,
            encryption=encryption,
            username=args.user,
            password=args.password,
            ssl=args.ssl,
            cert_validation=False,
            path=args.uri,
            negotiate_service=args.spn_prefix,
            negotiate_hostname_override=args.spn_hostname,
            certificate_key_pem=args.priv_key_pem,
            certificate_pem=args.cert_pem,
            user_agent=args.ua,
        ) as wsman:
            with RunspacePool(wsman) as r_pool:
                interactive_shell(r_pool)
    except (KeyboardInterrupt, EOFError):
        sys.exit(0)
    except WinRMTransportError as wte:
        print(RED + "[-] {}".format(wte) + RESET)
        log.error("WinRM transport error: {}".format(wte))
        sys.exit(1)
    except ConnectionError as ce:
        print(
            RED + "[-] Failed to connect to the remote host: {}:{}"
            "".format(args.ip, args.port) + RESET
        )
        log.error("Connection error: {}".format(ce))
        sys.exit(1)
    except AuthenticationError as ae:
        print(RED + "[-] {}".format(ae) + RESET)
        log.error("Authentication failed: {}".format(ae))
        sys.exit(1)
    except WSManFaultError as wfe:
        print(RED + "[-] {}".format(wfe) + RESET)
        log.error("WSMan fault error: {}".format(wfe))
        sys.exit(1)
    except Krb5Error as ke:
        print(RED + "[-] {}".format(ke) + RESET)
        log.error("Kerberos error: {}".format(ke))
        sys.exit(1)
    except (OperationNotAvailableError, NoCredentialError) as se:
        print(RED + "[-] {}".format(se._context_message) + RESET)
        print(RED + "[-] {}".format(se._BASE_MESSAGE) + RESET)
        log.error("SpnegoError error: {}".format(se))
        sys.exit(1)
    except SpnegoError as se:
        print(RED + "[-] {}".format(se._context_message) + RESET)
        print(RED + "[-] {}".format(se.message) + RESET)
        log.error("SpnegoError error: {}".format(se))
        sys.exit(1)
    except Exception as e:
        traceback.print_exc()
        log.exception("An unexpected error occurred: {}".format(e), exc_info=True)
        sys.exit(1)
    finally:
        log.info("--- Evil-WinRM-Py v{} ended ---".format(__version__))


================================================
FILE: evil_winrm_py/pypsrp_ewp/__init__.py
================================================


================================================
FILE: evil_winrm_py/pypsrp_ewp/wsman.py
================================================
# -*- coding: utf-8 -*-
# This file is part of evil-winrm-py.

# Following code is a modified version of pypsrp's wsman.py
# It has been adapted to work with evil-winrm-py.
# Original source: https://github.com/jborean93/pypsrp/blob/master/src/pypsrp/wsman.py

# Copyright: (c) 2018, Jordan Borean (@jborean93) <jborean93@gmail.com>
# MIT License (see LICENSE or https://opensource.org/licenses/MIT)

import logging
import typing
import uuid
import xml.etree.ElementTree as ET

from pypsrp._utils import get_hostname
from pypsrp.encryption import WinRMEncryption
from pypsrp.exceptions import WinRMTransportError
from pypsrp.wsman import (
    AUTH_KWARGS,
    NAMESPACES,
    SUPPORTED_AUTHS,
    WSMan,
    _TransportHTTP,
    requests,
)
from urllib3.util.retry import Retry

try:
    from requests_credssp import HttpCredSSPAuth
except ImportError as err:  # pragma: no cover
    _requests_credssp_import_error = (
        "Cannot use CredSSP auth as requests-credssp is not installed: %s" % err
    )

    class HttpCredSSPAuth(object):  # type: ignore[no-redef] # https://github.com/python/mypy/issues/1153
        def __init__(self, *args, **kwargs):
            raise ImportError(_requests_credssp_import_error)


log = logging.getLogger(__name__)


class WSManEWP(WSMan):
    """Override WSMan class to customize some stuff"""

    def __init__(
        self,
        server: str,
        max_envelope_size: int = 153600,
        operation_timeout: int = 20,
        port: typing.Optional[int] = None,
        username: typing.Optional[str] = None,
        password: typing.Optional[str] = None,
        ssl: bool = True,
        path: str = "wsman",
        auth: str = "negotiate",
        cert_validation: bool = True,
        connection_timeout: int = 30,
        encryption: str = "auto",
        proxy: typing.Optional[str] = None,
        no_proxy: bool = False,
        locale: str = "en-US",
        data_locale: typing.Optional[str] = None,
        read_timeout: int = 30,
        reconnection_retries: int = 0,
        reconnection_backoff: float = 2.0,
        user_agent: str = "Microsoft WinRM Client",
        **kwargs: typing.Any,
    ) -> None:
        """
        Class that handles WSMan transport over HTTP. This exposes a method per
        action that takes in a resource and the header metadata required by
        that resource.

        This is required by the pypsrp.shell.WinRS and
        pypsrp.powershell.RunspacePool in order to connect to the remote host.
        It uses HTTP(S) to send data to the remote host.

        https://msdn.microsoft.com/en-us/library/cc251598.aspx

        :param server: The hostname or IP address of the host to connect to
        :param max_envelope_size: The maximum size of the envelope that can be
            sent to the server. Use update_max_envelope_size() to query the
            server for the true value
        :param max_envelope_size: The maximum size of a WSMan envelope that
            can be sent to the server
        :param operation_timeout: Indicates that the client expects a response
            or a fault within the specified time.
        :param port: The port to connect to, default is 5986 if ssl=True, else
            5985
        :param username: The username to connect with
        :param password: The password for the above username
        :param ssl: Whether to connect over http or https
        :param path: The WinRM path to connect to
        :param auth: The auth protocol to use; basic, certificate, negotiate,
            credssp. Can also specify ntlm or kerberos to limit the negotiate
            protocol
        :param cert_validation: Whether to validate the server's SSL cert
        :param connection_timeout: The timeout for connecting to the HTTP
            endpoint
        :param read_timeout: The timeout for receiving from the HTTP endpoint
        :param encryption: Controls the encryption setting, default is auto
            but can be set to always or never
        :param proxy: The proxy URL used to connect to the remote host
        :param no_proxy: Whether to ignore any environment proxy vars and
            connect directly to the host endpoint
        :param locale: The wsmv:Locale value to set on each WSMan request. This
            specifies the language in which the client wants response text to
            be translated. The value should be in the format described by
            RFC 3066, with the default being 'en-US'
        :param data_locale: The wsmv:DataLocale value to set on each WSMan
            request. This specifies the format in which numerical data is
            presented in the response text. The value should be in the format
            described by RFC 3066, with the default being the value of locale.
        :param int reconnection_retries: Number of retries on connection
            problems
        :param float reconnection_backoff: Number of seconds to backoff in
            between reconnection attempts (first sleeps X, then sleeps 2*X,
            4*X, 8*X, ...)
        :param kwargs: Dynamic kwargs based on the auth protocol set
            # auth='certificate'
            certificate_key_pem: The path to the cert key pem file
            certificate_pem: The path to the cert pem file

            # auth='credssp'
            credssp_auth_mechanism: The sub auth mechanism to use in CredSSP,
                default is 'auto' but can be 'ntlm' or 'kerberos'
            credssp_disable_tlsv1_2: Use TLSv1.0 instead of 1.2
            credssp_minimum_version: The minimum CredSSP server version to
                allow

            # auth in ['negotiate', 'ntlm', 'kerberos']
            negotiate_send_cbt: Whether to send the CBT token on HTTPS
                connections, default is True

            # the below are only relevant when kerberos (or nego used kerb)
            negotiate_delegate: Whether to delegate the Kerb token to extra
                servers (credential delegation), default is False
            negotiate_hostname_override: Override the hostname used when
                building the server SPN
            negotiate_service: Override the service used when building the
                server SPN, default='WSMAN'

            # custom user-agent header
            user_agent: The user agent to use for the HTTP requests, this
                defaults to 'Microsoft WinRM Client'
        """
        log.debug(
            "Initialising WSMan class with maximum envelope size of %d "
            "and operation timeout of %s" % (max_envelope_size, operation_timeout)
        )
        self.session_id = str(uuid.uuid4())
        self.locale = locale
        self.data_locale = self.locale if data_locale is None else data_locale
        self.transport = _TransportHTTPEWP(
            server,
            port,
            username,
            password,
            ssl,
            path,
            auth,
            cert_validation,
            connection_timeout,
            encryption,
            proxy,
            no_proxy,
            read_timeout,
            reconnection_retries,
            reconnection_backoff,
            user_agent,
            **kwargs,
        )
        self.max_envelope_size = max_envelope_size
        self.operation_timeout = operation_timeout

        # register well known namespace prefixes so ElementTree doesn't
        # randomly generate them, saving packet space
        for key, value in NAMESPACES.items():
            ET.register_namespace(key, value)

        # This is the approx max size of a Base64 string that can be sent in a
        # SOAP message payload (PSRP fragment or send input data) to the
        # server. This value is dependent on the server's MaxEnvelopSizekb
        # value set on the WinRM service and the default is different depending
        # on the Windows version. Server 2008 (R2) detaults to 150KiB while
        # newer hosts are 500 KiB and this can be configured manually. Because
        # we don't know the OS version before we connect, we set the default to
        # 150KiB to ensure we are compatible with older hosts. This can be
        # manually adjusted with the max_envelope_size param which is the
        # MaxEnvelopeSizekb value * 1024. Otherwise the
        # update_max_envelope_size() function can be called and it will gather
        # this information for you.
        self.max_payload_size = self._calc_envelope_size(max_envelope_size)


class _TransportHTTPEWP(_TransportHTTP):
    """Override _TransportHTTP"""

    def __init__(
        self,
        server: str,
        port: typing.Optional[int] = None,
        username: typing.Optional[str] = None,
        password: typing.Optional[str] = None,
        ssl: bool = True,
        path: str = "wsman",
        auth: str = "negotiate",
        cert_validation: bool = True,
        connection_timeout: int = 30,
        encryption: str = "auto",
        proxy: typing.Optional[str] = None,
        no_proxy: bool = False,
        read_timeout: int = 30,
        reconnection_retries: int = 0,
        reconnection_backoff: float = 2.0,
        user_agent: str = "Microsoft WinRM Client",
        **kwargs: typing.Any,
    ) -> None:
        self.server = server
        self.port = port if port is not None else (5986 if ssl else 5985)
        self.username = username
        self.password = password
        self.ssl = ssl
        self.path = path

        if auth not in SUPPORTED_AUTHS:
            raise ValueError(
                "The specified auth '%s' is not supported, "
                "please select one of '%s'" % (auth, ", ".join(SUPPORTED_AUTHS))
            )
        self.auth = auth
        self.cert_validation = cert_validation
        self.connection_timeout = connection_timeout
        self.read_timeout = read_timeout
        self.reconnection_retries = reconnection_retries
        self.reconnection_backoff = reconnection_backoff
        self.user_agent = user_agent

        # determine the message encryption logic
        if encryption not in ["auto", "always", "never"]:
            raise ValueError(
                "The encryption value '%s' must be auto, always, or never" % encryption
            )
        enc_providers = ["credssp", "kerberos", "negotiate", "ntlm"]
        if ssl:
            # msg's are automatically encrypted with TLS, we only want message
            # encryption if always was specified
            self.wrap_required = encryption == "always"
            if self.wrap_required and self.auth not in enc_providers:
                raise ValueError(
                    "Cannot use message encryption with auth '%s', either set "
                    "encryption='auto' or use one of the following auth "
                    "providers: %s" % (self.auth, ", ".join(enc_providers))
                )
        else:
            # msg's should always be encrypted when not using SSL, unless the
            # user specifies to never encrypt
            self.wrap_required = not encryption == "never"
            if self.wrap_required and self.auth not in enc_providers:
                raise ValueError(
                    "Cannot use message encryption with auth '%s', either set "
                    "encryption='never', use ssl=True or use one of the "
                    "following auth providers: %s"
                    % (self.auth, ", ".join(enc_providers))
                )
        self.encryption: typing.Optional[WinRMEncryption] = None

        self.proxy = proxy
        self.no_proxy = no_proxy

        self.certificate_key_pem: typing.Optional[str] = None
        self.certificate_pem: typing.Optional[str] = None
        for kwarg_list in AUTH_KWARGS.values():
            for kwarg in kwarg_list:
                setattr(self, kwarg, kwargs.get(kwarg, None))

        self.endpoint = self._create_endpoint(
            self.ssl, self.server, self.port, self.path
        )
        log.debug(
            "Initialising HTTP transport for endpoint: %s, user: %s, "
            "auth: %s" % (self.endpoint, self.username, self.auth)
        )
        self.session: typing.Optional[requests.Session] = None

        # used when building tests, keep commented out
        # self._test_messages = []

    def send(self, message: bytes) -> bytes:
        hostname = get_hostname(self.endpoint)
        if self.session is None:
            self.session = self._build_session()

            # need to send an initial blank message to setup the security
            # context required for encryption
            if self.wrap_required:
                request = requests.Request("POST", self.endpoint, data=None)
                prep_request = self.session.prepare_request(request)
                self._send_request(prep_request)

                protocol = WinRMEncryption.SPNEGO
                if isinstance(self.session.auth, HttpCredSSPAuth):
                    protocol = WinRMEncryption.CREDSSP
                elif self.session.auth.contexts[hostname].response_auth_header == "kerberos":  # type: ignore[union-attr] # This should not happen
                    # When Kerberos (not Negotiate) was used, we need to send a special protocol value and not SPNEGO.
                    protocol = WinRMEncryption.KERBEROS

                self.encryption = WinRMEncryption(self.session.auth.contexts[hostname], protocol)  # type: ignore[union-attr] # This should not happen

        if log.isEnabledFor(logging.DEBUG):
            log.debug("Sending message: %s" % message.decode("utf-8"))
        # for testing, keep commented out
        # self._test_messages.append({"request": message.decode('utf-8'),
        #                             "response": None})

        headers = self.session.headers
        if self.wrap_required:
            content_type, payload = self.encryption.wrap_message(message)  # type: ignore[union-attr] # This should not happen
            protocol = (
                self.encryption.protocol if self.encryption else WinRMEncryption.SPNEGO
            )
            type_header = '%s;protocol="%s";boundary="Encrypted Boundary"' % (
                content_type,
                protocol,
            )
            headers.update(
                {
                    "Content-Type": type_header,
                    "Content-Length": str(len(payload)),
                }
            )
        else:
            payload = message
            headers["Content-Type"] = "application/soap+xml;charset=UTF-8"

        request = requests.Request("POST", self.endpoint, data=payload, headers=headers)
        prep_request = self.session.prepare_request(request)
        try:
            return self._send_request(prep_request)
        except WinRMTransportError as err:
            if err.code == 400:
                log.debug("Session invalid, resetting session")
                self.session = None  # reset the session so we can retry
                return self.send(message)
            else:
                raise

    def _build_session(self) -> requests.Session:
        log.debug("Building requests session with auth %s" % self.auth)
        self._suppress_library_warnings()

        session = requests.Session()
        session.headers["User-Agent"] = self.user_agent

        # requests defaults to 'Accept-Encoding: gzip, default' which normally doesn't matter on vanila WinRM but for
        # Exchange endpoints hosted on IIS they actually compress it with 1 of the 2 algorithms. By explicitly setting
        # identity we are telling the server not to transform (compress) the data using the HTTP methods which we don't
        # support. https://tools.ietf.org/html/rfc7231#section-5.3.4
        session.headers["Accept-Encoding"] = "identity"

        # get the env requests settings
        session.trust_env = True
        settings = session.merge_environment_settings(
            url=self.endpoint, proxies={}, stream=None, verify=None, cert=None
        )

        # set the proxy config
        session.proxies = settings["proxies"]
        proxy_key = "https" if self.ssl else "http"
        if self.proxy is not None:
            session.proxies = {
                proxy_key: self.proxy,
            }
        elif self.no_proxy:
            session.proxies = {
                proxy_key: False,  # type: ignore[dict-item] # A boolean is expected here
            }

        # Retry on connection errors, with a backoff factor
        retry_kwargs = {
            "total": self.reconnection_retries,
            "connect": self.reconnection_retries,
            "status": self.reconnection_retries,
            "read": 0,
            "backoff_factor": self.reconnection_backoff,
            "status_forcelist": (425, 429, 503),
        }
        try:
            retries = Retry(**retry_kwargs)
        except TypeError:
            # Status was added in urllib3 >= 1.21 (Requests >= 2.14.0), remove
            # the status retry counter and try again. The user should upgrade
            # to a newer version
            log.warning(
                "Using an older requests version that without support for status retries, ignoring.",
                exc_info=True,
            )
            del retry_kwargs["status"]
            retries = Retry(**retry_kwargs)

        session.mount("http://", requests.adapters.HTTPAdapter(max_retries=retries))
        session.mount("https://", requests.adapters.HTTPAdapter(max_retries=retries))

        # set cert validation config
        session.verify = self.cert_validation

        # if cert_validation is a bool (no path specified), not False and there
        # are env settings for verification, set those env settings
        if (
            isinstance(self.cert_validation, bool)
            and self.cert_validation
            and settings["verify"] is not None
        ):
            session.verify = settings["verify"]

        build_auth = getattr(self, "_build_auth_%s" % self.auth)
        build_auth(session)
        return session


================================================
FILE: setup.py
================================================
import io
from os import path

from setuptools import find_packages, setup

pwd = path.abspath(path.dirname(__file__))
with io.open(path.join(pwd, "README.md"), encoding="utf-8") as readme:
    desc = readme.read()

setup(
    name="evil-winrm-py",
    version=__import__("evil_winrm_py").__version__,
    description="Execute commands interactively on remote Windows machines using the WinRM protocol",
    long_description=desc,
    long_description_content_type="text/markdown",
    author="adityatelange",
    license="MIT",
    url="https://github.com/adityatelange/evil-winrm-py",
    download_url="https://github.com/adityatelange/evil-winrm-py/archive/v%s.zip"
    % __import__("evil_winrm_py").__version__,
    packages=find_packages(),
    classifiers=[
        "Topic :: Security",
        "Operating System :: Unix",
        "License :: OSI Approved :: MIT License",
        "Programming Language :: Python :: 3",
    ],
    install_requires=[
        "pypsrp==0.8.1",
        "prompt_toolkit==3.0.52",
        "tqdm==4.67.3",
    ],
    extras_require={
        "kerberos": [
            "pypsrp[kerberos]==0.8.1",
        ]
    },
    python_requires=">=3.9",
    entry_points={
        "console_scripts": [
            "evil-winrm-py = evil_winrm_py.evil_winrm_py:main",
            "ewp = evil_winrm_py.evil_winrm_py:main",
        ]
    },
    package_data={
        "evil_winrm_py": ["_ps/*.ps1"],
    },
)
Download .txt
gitextract_ce49hs3q/

├── .github/
│   └── workflows/
│       └── wiki.yml
├── .gitignore
├── LICENSE
├── README.md
├── docs/
│   ├── README.md
│   ├── development.md
│   ├── install.md
│   ├── knowledgebase.md
│   ├── release.md
│   ├── sample/
│   │   └── krb5.conf
│   └── usage.md
├── evil_winrm_py/
│   ├── __init__.py
│   ├── _ps/
│   │   ├── __init__.py
│   │   ├── exec.ps1
│   │   ├── fetch.ps1
│   │   ├── loaddll.ps1
│   │   └── send.ps1
│   ├── evil_winrm_py.py
│   └── pypsrp_ewp/
│       ├── __init__.py
│       └── wsman.py
└── setup.py
Download .txt
SYMBOL INDEX (34 symbols across 2 files)

FILE: evil_winrm_py/evil_winrm_py.py
  class Krb5Error (line 53) | class Krb5Error(Exception):
  class DelayedKeyboardInterrupt (line 125) | class DelayedKeyboardInterrupt:
    method __enter__ (line 135) | def __enter__(self):
    method __exit__ (line 145) | def __exit__(self, type, value, traceback):
  function run_ps_cmd (line 152) | def run_ps_cmd(r_pool: RunspacePool, command: str) -> tuple[str, list, b...
  function get_prompt (line 162) | def get_prompt(r_pool: RunspacePool) -> str:
  function show_menu (line 172) | def show_menu() -> None:
  function get_directory_and_partial_name (line 180) | def get_directory_and_partial_name(text: str, sep: str) -> tuple[str, str]:
  function _ps_single_quote (line 199) | def _ps_single_quote(value: str) -> str:
  function get_remote_path_suggestions (line 205) | def get_remote_path_suggestions(
  function get_remote_command_suggestions (line 237) | def get_remote_command_suggestions(
  function get_local_path_suggestions (line 271) | def get_local_path_suggestions(
  class CommandPathCompleter (line 312) | class CommandPathCompleter(Completer):
    method __init__ (line 318) | def __init__(self, r_pool: RunspacePool):
    method get_completions (line 321) | def get_completions(self, document: Document, complete_event):
  function get_ps_script (line 687) | def get_ps_script(script_name: str) -> str:
  function quoted_command_split (line 700) | def quoted_command_split(command: str) -> list[str]:
  function download_file (line 731) | def download_file(r_pool: RunspacePool, remote_path: str, local_path: st...
  function upload_file (line 822) | def upload_file(r_pool: RunspacePool, local_path: str, remote_path: str)...
  function _read_text_auto_encoding (line 936) | def _read_text_auto_encoding(path) -> str:
  function load_ps (line 956) | def load_ps(r_pool: RunspacePool, local_path: str):
  function run_ps (line 1009) | def run_ps(r_pool: RunspacePool, local_path: str) -> None:
  function load_dll (line 1049) | def load_dll(r_pool: RunspacePool, local_path: str) -> None:
  function run_exe (line 1112) | def run_exe(r_pool: RunspacePool, local_path: str, args: str = "") -> None:
  function interactive_shell (line 1158) | def interactive_shell(r_pool: RunspacePool) -> None:
  function main (line 1427) | def main():

FILE: evil_winrm_py/pypsrp_ewp/wsman.py
  class HttpCredSSPAuth (line 36) | class HttpCredSSPAuth(object):  # type: ignore[no-redef] # https://githu...
    method __init__ (line 37) | def __init__(self, *args, **kwargs):
  class WSManEWP (line 44) | class WSManEWP(WSMan):
    method __init__ (line 47) | def __init__(
  class _TransportHTTPEWP (line 198) | class _TransportHTTPEWP(_TransportHTTP):
    method __init__ (line 201) | def __init__(
    method send (line 291) | def send(self, message: bytes) -> bytes:
    method _build_session (line 350) | def _build_session(self) -> requests.Session:
Condensed preview — 21 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (138K chars).
[
  {
    "path": ".github/workflows/wiki.yml",
    "chars": 432,
    "preview": "name: Publish wiki\non:\n  push:\n    branches: [main]\n    paths:\n      - docs/**\n      - .github/workflows/wiki.yml\nconcur"
  },
  {
    "path": ".gitignore",
    "chars": 3467,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
  },
  {
    "path": "LICENSE",
    "chars": 1071,
    "preview": "MIT License\n\nCopyright (c) 2025 Aditya Telange\n\nPermission is hereby granted, free of charge, to any person obtaining a "
  },
  {
    "path": "README.md",
    "chars": 8565,
    "preview": "<div align=center>\n  <img height=\"150\" alt=\"ewp-logo\" src=\"https://raw.githubusercontent.com/adityatelange/evil-winrm-py"
  },
  {
    "path": "docs/README.md",
    "chars": 200,
    "preview": "## Quick Links\n\n- **[Usage Guide](./usage.md)**\n- **[Installation Guide](./install.md)**\n- [Knowledge Base](./knowledgeb"
  },
  {
    "path": "docs/development.md",
    "chars": 580,
    "preview": "# Development Environment Setup\n\n## Setup\n\nDownload the repository.\n\n```bash\ngit clone https://github.com/adityatelange/"
  },
  {
    "path": "docs/install.md",
    "chars": 1742,
    "preview": "# Installation Guide\n\n`evil-winrm-py` is available on:\n\n- PyPI - https://pypi.org/project/evil-winrm-py/\n- Github - http"
  },
  {
    "path": "docs/knowledgebase.md",
    "chars": 4992,
    "preview": "# Knowledge Base\n\n## Negotiate authentication\n\nA negotiated, single sign on type of authentication that is the Windows i"
  },
  {
    "path": "docs/release.md",
    "chars": 1064,
    "preview": "# Releasing a new version on PyPI\n\nRead More: https://packaging.python.org/en/latest/guides/distributing-packages-using-"
  },
  {
    "path": "docs/sample/krb5.conf",
    "chars": 565,
    "preview": "# Sample Kerberos configuration file\n# Location: /etc/krb5.conf or /<your working directory>/krb5.conf\n\n[libdefaults]\n  "
  },
  {
    "path": "docs/usage.md",
    "chars": 10360,
    "preview": "# Usage Guide\n\n## Authentication Methods\n\n### NTLM Authentication\n\n```bash\nevil-winrm-py -i <IP> -u <USERNAME> -p <PASSW"
  },
  {
    "path": "evil_winrm_py/__init__.py",
    "chars": 22,
    "preview": "__version__ = \"1.6.0\"\n"
  },
  {
    "path": "evil_winrm_py/_ps/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "evil_winrm_py/_ps/exec.ps1",
    "chars": 1540,
    "preview": "# This script is part of evil-winrm-py project https://github.com/adityatelange/evil-winrm-py\n# It runs a dotnet executa"
  },
  {
    "path": "evil_winrm_py/_ps/fetch.ps1",
    "chars": 3021,
    "preview": "# This script is part of evil-winrm-py project https://github.com/adityatelange/evil-winrm-py\n# It reads a file in chunk"
  },
  {
    "path": "evil_winrm_py/_ps/loaddll.ps1",
    "chars": 1481,
    "preview": "# This script is part of evil-winrm-py project https://github.com/adityatelange/evil-winrm-py\n# It loads a dll in memory"
  },
  {
    "path": "evil_winrm_py/_ps/send.ps1",
    "chars": 4782,
    "preview": "# This script is part of evil-winrm-py project https://github.com/adityatelange/evil-winrm-py\n# It reads a Base64 encode"
  },
  {
    "path": "evil_winrm_py/evil_winrm_py.py",
    "chars": 68155,
    "preview": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\n\"\"\"\nevil-winrm-py\nhttps://github.com/adityatelange/evil-winrm-py\n\"\"\"\n\nim"
  },
  {
    "path": "evil_winrm_py/pypsrp_ewp/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "evil_winrm_py/pypsrp_ewp/wsman.py",
    "chars": 18012,
    "preview": "# -*- coding: utf-8 -*-\n# This file is part of evil-winrm-py.\n\n# Following code is a modified version of pypsrp's wsman."
  },
  {
    "path": "setup.py",
    "chars": 1425,
    "preview": "import io\nfrom os import path\n\nfrom setuptools import find_packages, setup\n\npwd = path.abspath(path.dirname(__file__))\nw"
  }
]

About this extraction

This page contains the full source code of the adityatelange/evil-winrm-py GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 21 files (128.4 KB), approximately 29.9k tokens, and a symbol index with 34 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!