Files
CoM-c2itall/modules/c2/deploy_chaos.py
T
Operator 98103466d8 Initial public portfolio release
Sanitized version of red team infrastructure automation platform.
Operational content (implant pipelines, lures, credential capture)
replaced with documented stubs. Architecture and infrastructure
automation code intact.
2026-06-23 16:12:14 -04:00

448 lines
18 KiB
Python

#!/usr/bin/env python3
"""
Chaos C2 — c2itall integration module
Deploy and manage the Chaos C2 framework (Havoc fork) locally or on a remote host.
"""
import os
import sys
import subprocess
import glob
# Add parent paths for imports
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
from utils.common import COLORS, clear_screen, print_banner, wait_for_input
# ─── Path Registry ────────────────────────────────────────────────────────────
CHAOS_PATH = os.path.expanduser('~/tools/chaos')
INSTALL_SH = os.path.join(CHAOS_PATH, 'Install.sh')
TEAMSERVER = os.path.join(CHAOS_PATH, 'teamserver')
DATA_DIR = os.path.join(CHAOS_PATH, 'data')
LOGS_DIR = os.path.join(DATA_DIR, 'logs')
WS_PATH_FILE = os.path.join(DATA_DIR, '.ws_path')
PROFILES_DIR = os.path.join(CHAOS_PATH, 'profiles')
# ─── SSH helpers ──────────────────────────────────────────────────────────────
def _build_ssh_cmd(host, ssh_user, ssh_port, ssh_key, command=None, interactive=False):
"""Build an SSH command list."""
cmd = ['ssh']
if ssh_key:
cmd.extend(['-i', ssh_key])
cmd.extend(['-p', str(ssh_port)])
known_hosts = os.path.expanduser('~/.ssh/c2deploy_chaos_known_hosts')
cmd.extend([
'-o', 'StrictHostKeyChecking=accept-new',
'-o', f'UserKnownHostsFile={known_hosts}',
'-o', 'IdentitiesOnly=yes',
])
if interactive:
cmd.append('-t')
cmd.append(f'{ssh_user}@{host}')
if command:
cmd.append(command)
return cmd
def _build_scp_cmd(ssh_port, ssh_key=None):
"""Build base SCP command with consistent SSH options."""
known_hosts = os.path.expanduser('~/.ssh/c2deploy_chaos_known_hosts')
cmd = [
'scp',
'-P', str(ssh_port),
'-o', 'StrictHostKeyChecking=accept-new',
'-o', f'UserKnownHostsFile={known_hosts}',
]
if ssh_key:
cmd.extend(['-i', ssh_key])
return cmd
# ─── Remote target prompt ─────────────────────────────────────────────────────
def _prompt_remote_target():
"""Prompt for host/user/port/key. Returns (host, user, port, key) or None on cancel."""
print(f"\n{COLORS['CYAN']}Remote Target{COLORS['RESET']}")
print(f"{COLORS['WHITE']}============={COLORS['RESET']}")
host = input(f" Remote host (IP or hostname): ").strip()
if not host:
print(f"{COLORS['YELLOW']}No host provided{COLORS['RESET']}")
return None, None, None, None
ssh_user = input(f" SSH user [{COLORS['CYAN']}root{COLORS['RESET']}]: ").strip() or 'root'
ssh_port_raw = input(f" SSH port [{COLORS['CYAN']}22{COLORS['RESET']}]: ").strip() or '22'
try:
ssh_port = int(ssh_port_raw)
except ValueError:
ssh_port = 22
ssh_key = input(f" SSH key path (blank for default): ").strip() or None
return host, ssh_user, ssh_port, ssh_key
# ─── Profile selection ────────────────────────────────────────────────────────
def _select_profile(label="Select profile"):
"""List available profiles and let the user choose one. Returns path or None."""
profiles = []
if os.path.isdir(PROFILES_DIR):
for ext in ('*.toml', '*.yaotl', '*.yaml', '*.yml'):
profiles.extend(glob.glob(os.path.join(PROFILES_DIR, ext)))
profiles.sort()
if not profiles:
print(f" {COLORS['YELLOW']}No profiles found in {PROFILES_DIR}{COLORS['RESET']}")
manual = input(f" Enter profile path manually (blank to cancel): ").strip()
return manual or None
print(f"\n {COLORS['CYAN']}{label}:{COLORS['RESET']}")
for i, p in enumerate(profiles, 1):
print(f" {i}) {os.path.basename(p)}")
raw = input(f"\n Select [1]: ").strip() or '1'
try:
idx = int(raw) - 1
if 0 <= idx < len(profiles):
return profiles[idx]
except ValueError:
pass
print(f" {COLORS['YELLOW']}Invalid selection — using first profile{COLORS['RESET']}")
return profiles[0]
# ─── Local actions ────────────────────────────────────────────────────────────
def _deploy_local():
"""Run Install.sh locally after a pre-flight check."""
if not os.path.exists(INSTALL_SH):
print(f"\n{COLORS['RED']}Install.sh not found at {INSTALL_SH}{COLORS['RESET']}")
print(f"{COLORS['YELLOW']}Clone the Chaos repo to ~/tools/chaos first{COLORS['RESET']}")
wait_for_input()
return
print(f"\n{COLORS['CYAN']}Running pre-flight check (Install.sh --check)...{COLORS['RESET']}")
check_result = subprocess.run(
['bash', INSTALL_SH, '--check'],
cwd=CHAOS_PATH,
capture_output=True,
text=True,
)
if check_result.stdout:
print(check_result.stdout)
if check_result.stderr:
print(check_result.stderr)
if check_result.returncode != 0:
print(f"{COLORS['YELLOW']}Pre-flight check reported issues (rc={check_result.returncode}) — continue anyway? (y/N): {COLORS['RESET']}", end='')
if input().strip().lower() != 'y':
wait_for_input()
return
print(f"\n{COLORS['CYAN']}Installing Chaos C2 locally...{COLORS['RESET']}")
print(f"{COLORS['YELLOW']}Note: script may require sudo — you may be prompted for your password.{COLORS['RESET']}\n")
try:
subprocess.run(['bash', INSTALL_SH], cwd=CHAOS_PATH)
except KeyboardInterrupt:
print(f"\n{COLORS['YELLOW']}Installation interrupted{COLORS['RESET']}")
wait_for_input()
def _deploy_remote():
"""SCP the chaos repo to a remote host and run Install.sh over SSH."""
if not os.path.isdir(CHAOS_PATH):
print(f"\n{COLORS['RED']}Chaos source not found at {CHAOS_PATH}{COLORS['RESET']}")
wait_for_input()
return
host, ssh_user, ssh_port, ssh_key = _prompt_remote_target()
if not host:
wait_for_input()
return
remote_staging = '/tmp/chaos-deploy'
print(f"\n{COLORS['CYAN']}Creating remote staging directory on {ssh_user}@{host}...{COLORS['RESET']}")
mkdir_cmd = _build_ssh_cmd(host, ssh_user, ssh_port, ssh_key,
f'rm -rf {remote_staging} && mkdir -p {remote_staging}')
result = subprocess.run(mkdir_cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"{COLORS['RED']}Failed to create remote staging dir: {result.stderr}{COLORS['RESET']}")
wait_for_input()
return
print(f"{COLORS['CYAN']}Copying Chaos to {ssh_user}@{host}:{remote_staging}...{COLORS['RESET']}")
scp_cmd = _build_scp_cmd(ssh_port, ssh_key)
scp_cmd.extend(['-r', CHAOS_PATH + '/'])
scp_cmd.append(f'{ssh_user}@{host}:{remote_staging}/')
result = subprocess.run(scp_cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"{COLORS['RED']}SCP failed: {result.stderr}{COLORS['RESET']}")
wait_for_input()
return
print(f" {COLORS['GREEN']}Files copied{COLORS['RESET']}")
print(f"\n{COLORS['CYAN']}Running Install.sh on {host}...{COLORS['RESET']}")
install_cmd_str = f'cd {remote_staging} && bash Install.sh'
ssh_install = _build_ssh_cmd(host, ssh_user, ssh_port, ssh_key,
install_cmd_str, interactive=True)
try:
subprocess.run(ssh_install)
except KeyboardInterrupt:
print(f"\n{COLORS['YELLOW']}Remote install interrupted{COLORS['RESET']}")
wait_for_input()
def _start_teamserver():
"""Start the Chaos teamserver with a selected profile."""
if not os.path.exists(TEAMSERVER):
print(f"\n{COLORS['RED']}teamserver binary not found at {TEAMSERVER}{COLORS['RESET']}")
print(f"{COLORS['YELLOW']}Run 'Deploy Chaos (local)' first to build it{COLORS['RESET']}")
wait_for_input()
return
profile = _select_profile("Select listener profile")
if not profile:
print(f"{COLORS['YELLOW']}No profile selected — aborting{COLORS['RESET']}")
wait_for_input()
return
print(f"\n{COLORS['CYAN']}Starting Chaos teamserver with profile: {os.path.basename(profile)}{COLORS['RESET']}")
print(f"{COLORS['GRAY']}Press Ctrl+C to stop{COLORS['RESET']}\n")
try:
subprocess.run([TEAMSERVER, '-p', profile], cwd=CHAOS_PATH)
except KeyboardInterrupt:
print(f"\n{COLORS['YELLOW']}Teamserver stopped by user{COLORS['RESET']}")
wait_for_input()
def _stop_teamserver():
"""Find and kill the teamserver process."""
print(f"\n{COLORS['CYAN']}Looking for running teamserver process...{COLORS['RESET']}")
result = subprocess.run(
['pgrep', '-f', 'teamserver'],
capture_output=True, text=True
)
pids = result.stdout.strip().splitlines()
if not pids:
print(f"{COLORS['YELLOW']}No teamserver process found{COLORS['RESET']}")
wait_for_input()
return
print(f" Found PID(s): {', '.join(pids)}")
confirm = input(f" {COLORS['YELLOW']}Kill these processes? (y/N): {COLORS['RESET']}").strip().lower()
if confirm != 'y':
print(f"{COLORS['GREEN']}Cancelled{COLORS['RESET']}")
wait_for_input()
return
for pid in pids:
kill_result = subprocess.run(['kill', pid], capture_output=True, text=True)
if kill_result.returncode == 0:
print(f" {COLORS['GREEN']}Killed PID {pid}{COLORS['RESET']}")
else:
print(f" {COLORS['RED']}Failed to kill PID {pid}: {kill_result.stderr.strip()}{COLORS['RESET']}")
wait_for_input()
def _show_status():
"""Check if teamserver is running, show WS path and active profile."""
print(f"\n{COLORS['CYAN']}Chaos C2 Status{COLORS['RESET']}")
print(f"{COLORS['WHITE']}==============={COLORS['RESET']}")
# Check for running process
result = subprocess.run(['pgrep', '-a', '-f', 'teamserver'], capture_output=True, text=True)
if result.stdout.strip():
print(f" Teamserver: {COLORS['GREEN']}RUNNING{COLORS['RESET']}")
for line in result.stdout.strip().splitlines():
print(f" {COLORS['GRAY']}{line}{COLORS['RESET']}")
else:
print(f" Teamserver: {COLORS['RED']}NOT RUNNING{COLORS['RESET']}")
# Show WS path
if os.path.exists(WS_PATH_FILE):
with open(WS_PATH_FILE) as f:
ws_path = f.read().strip()
print(f" WS Path: {COLORS['CYAN']}{ws_path}{COLORS['RESET']}")
else:
print(f" WS Path: {COLORS['GRAY']}(not set){COLORS['RESET']}")
# Show install dir
if os.path.isdir(CHAOS_PATH):
print(f" Install: {COLORS['GREEN']}{CHAOS_PATH}{COLORS['RESET']}")
else:
print(f" Install: {COLORS['RED']}NOT FOUND — {CHAOS_PATH}{COLORS['RESET']}")
# Show available profiles
if os.path.isdir(PROFILES_DIR):
profiles = []
for ext in ('*.toml', '*.yaotl', '*.yaml', '*.yml'):
profiles.extend(glob.glob(os.path.join(PROFILES_DIR, ext)))
if profiles:
print(f" Profiles:")
for p in sorted(profiles):
print(f" {COLORS['GRAY']}{os.path.basename(p)}{COLORS['RESET']}")
else:
print(f" Profiles: {COLORS['YELLOW']}none found in {PROFILES_DIR}{COLORS['RESET']}")
wait_for_input()
def _generate_payload():
"""Interactive payload generation: listener profile, arch, format, output path."""
if not os.path.exists(TEAMSERVER):
print(f"\n{COLORS['RED']}teamserver not found — build Chaos first{COLORS['RESET']}")
wait_for_input()
return
print(f"\n{COLORS['CYAN']}Payload Generation{COLORS['RESET']}")
print(f"{COLORS['WHITE']}=================={COLORS['RESET']}")
profile = _select_profile("Select listener profile for payload")
if not profile:
print(f"{COLORS['YELLOW']}No profile selected — aborting{COLORS['RESET']}")
wait_for_input()
return
print(f"\n Architecture:")
print(f" 1) x86_64 (64-bit)")
print(f" 2) x86 (32-bit)")
arch_choice = input(f" Select [1]: ").strip() or '1'
arch = 'x86_64' if arch_choice != '2' else 'x86'
print(f"\n Format:")
print(f" 1) exe (Windows executable)")
print(f" 2) dll (Windows DLL)")
print(f" 3) bin (raw shellcode)")
fmt_choice = input(f" Select [1]: ").strip() or '1'
fmt_map = {'1': 'exe', '2': 'dll', '3': 'bin'}
fmt = fmt_map.get(fmt_choice, 'exe')
default_out = os.path.join(CHAOS_PATH, 'payloads', f'payload_{arch}.{fmt}')
out_path = input(f"\n Output path [{COLORS['CYAN']}{default_out}{COLORS['RESET']}]: ").strip() or default_out
os.makedirs(os.path.dirname(out_path), exist_ok=True)
print(f"\n{COLORS['CYAN']}Generating payload...{COLORS['RESET']}")
cmd = [TEAMSERVER, 'generate', '--profile', profile,
'--arch', arch, '--format', fmt, '--output', out_path]
try:
result = subprocess.run(cmd, cwd=CHAOS_PATH, capture_output=True, text=True)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr)
if result.returncode == 0:
print(f"{COLORS['GREEN']}Payload written to: {out_path}{COLORS['RESET']}")
else:
print(f"{COLORS['RED']}Payload generation failed (rc={result.returncode}){COLORS['RESET']}")
print(f"{COLORS['YELLOW']}Note: 'generate' subcommand may differ in your build — check teamserver --help{COLORS['RESET']}")
except FileNotFoundError:
print(f"{COLORS['RED']}teamserver binary not executable — did the build complete?{COLORS['RESET']}")
wait_for_input()
def _view_loot():
"""Tail structured JSON logs from data/logs/."""
if not os.path.isdir(LOGS_DIR):
print(f"\n{COLORS['YELLOW']}Logs directory not found: {LOGS_DIR}{COLORS['RESET']}")
print(f"{COLORS['GRAY']}Logs will appear here after the teamserver has been started{COLORS['RESET']}")
wait_for_input()
return
log_files = sorted(glob.glob(os.path.join(LOGS_DIR, '*.json')) +
glob.glob(os.path.join(LOGS_DIR, '*.log')))
if not log_files:
print(f"\n{COLORS['YELLOW']}No log files found in {LOGS_DIR}{COLORS['RESET']}")
wait_for_input()
return
print(f"\n{COLORS['CYAN']}Available log files:{COLORS['RESET']}")
for i, lf in enumerate(log_files, 1):
size = os.path.getsize(lf)
print(f" {i}) {os.path.basename(lf)} ({size} bytes)")
raw = input(f"\n Select log to tail [1]: ").strip() or '1'
try:
idx = int(raw) - 1
if 0 <= idx < len(log_files):
chosen = log_files[idx]
else:
chosen = log_files[0]
except ValueError:
chosen = log_files[0]
lines_raw = input(f" Lines to show [{COLORS['CYAN']}50{COLORS['RESET']}]: ").strip() or '50'
try:
lines = int(lines_raw)
except ValueError:
lines = 50
print(f"\n{COLORS['CYAN']}--- {os.path.basename(chosen)} (last {lines} lines) ---{COLORS['RESET']}\n")
try:
result = subprocess.run(['tail', '-n', str(lines), chosen],
capture_output=True, text=True)
print(result.stdout)
except Exception as e:
print(f"{COLORS['RED']}Error reading log: {e}{COLORS['RESET']}")
wait_for_input()
# ─── Main menu ────────────────────────────────────────────────────────────────
def chaos_menu():
"""Main entry point — called from c2itall deploy.py tools_menu()."""
while True:
clear_screen()
print_banner()
print(f"{COLORS['WHITE']}CHAOS C2{COLORS['RESET']}")
print(f"{COLORS['WHITE']}========{COLORS['RESET']}")
print(f" Havoc-based C2 framework")
# Show whether source is available
if os.path.isdir(CHAOS_PATH):
ts_label = (f"{COLORS['GREEN']}built{COLORS['RESET']}"
if os.path.exists(TEAMSERVER)
else f"{COLORS['YELLOW']}not built{COLORS['RESET']}")
print(f" Source: {COLORS['GREEN']}{CHAOS_PATH}{COLORS['RESET']} (teamserver: {ts_label})")
else:
print(f" Source: {COLORS['RED']}NOT FOUND — {CHAOS_PATH}{COLORS['RESET']}")
print()
print(f"1) Deploy Chaos {COLORS['CYAN']}(local — runs Install.sh){COLORS['RESET']}")
print(f"2) Deploy Chaos {COLORS['CYAN']}(remote SSH — SCP + install){COLORS['RESET']}")
print(f"3) Start Teamserver")
print(f"4) Stop Teamserver")
print(f"5) Show Status")
print(f"6) Generate Payload")
print(f"7) View Loot {COLORS['GRAY']}(tail data/logs/){COLORS['RESET']}")
print(f"99) Return to Tools Menu")
choice = input(f"\nSelect an option: ").strip()
if choice == '1':
_deploy_local()
elif choice == '2':
_deploy_remote()
elif choice == '3':
_start_teamserver()
elif choice == '4':
_stop_teamserver()
elif choice == '5':
_show_status()
elif choice == '6':
_generate_payload()
elif choice == '7':
_view_loot()
elif choice == '99':
return
else:
print(f"\n{COLORS['RED']}Invalid option. Please try again.{COLORS['RESET']}")
wait_for_input()