diff --git a/deploy.py b/deploy.py index 76abed9..c5d958a 100755 --- a/deploy.py +++ b/deploy.py @@ -227,6 +227,7 @@ def tools_menu(): print(f"6) Infrastructure Health Check") print(f"7) Ops Dashboard {COLORS['CYAN']}(Real-Time Engagement Monitor){COLORS['RESET']}") print(f"8) Claude Bot {COLORS['CYAN']}(Matrix-Claude Code Bridge){COLORS['RESET']}") + print(f"9) Chaos C2 {COLORS['CYAN']}(Deploy / Manage Chaos teamserver){COLORS['RESET']}") print(f"99) Return to Main Menu") choice = input(f"\nSelect an option: ") @@ -254,6 +255,11 @@ def tools_menu(): claude_bot_module = import_module_from_path('deploy_claude_bot', claude_bot_module_path) if claude_bot_module: claude_bot_module.claude_bot_menu() + elif choice == "9": + chaos_module_path = os.path.join(os.path.dirname(__file__), 'modules', 'c2', 'deploy_chaos.py') + chaos_module = import_module_from_path('deploy_chaos', chaos_module_path) + if chaos_module: + chaos_module.chaos_menu() elif choice == "99": return else: diff --git a/modules/c2/__init__.py b/modules/c2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/c2/deploy_chaos.py b/modules/c2/deploy_chaos.py new file mode 100644 index 0000000..fb9a3a2 --- /dev/null +++ b/modules/c2/deploy_chaos.py @@ -0,0 +1,447 @@ +#!/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()