448 lines
18 KiB
Python
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()
|