Update c2_final.py
This commit is contained in:
+192
-4
@@ -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(
|
||||||
@@ -421,7 +421,8 @@ class BeaconManager:
|
|||||||
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 = '''
|
||||||
@@ -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():
|
||||||
@@ -995,6 +1173,8 @@ 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)')
|
||||||
@@ -1002,8 +1182,12 @@ def main():
|
|||||||
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)
|
||||||
|
|
||||||
@@ -1022,6 +1206,10 @@ def main():
|
|||||||
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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user