Skip to content

OPU-32: BRIDGEHEAD (XOR decode-to-exec, async/PowerShell cradle) - #100

Merged
MoSLoF merged 1 commit into
mainfrom
claude/depsnort-project-structure-5re5ui
Aug 25, 2026
Merged

OPU-32: BRIDGEHEAD (XOR decode-to-exec, async/PowerShell cradle)#100
MoSLoF merged 1 commit into
mainfrom
claude/depsnort-project-structure-5re5ui

Conversation

@MoSLoF

@MoSLoF MoSLoF commented Aug 25, 2026

Copy link
Copy Markdown
Owner

The gap

CloudSEK's BRIDGEHEAD writeup (Aug 20 2026) describes an npm typosquatting campaign whose 40-package dropper (scripts/postinstall.js) does two things depSNORT's install-surface analyzer didn't catch:

  1. Decodes its C2 URL and PowerShell bridge with a hand-rolled XOR-over-fromCharCode loop — not base64/hex/gzip, not the existing batch-assembly shapes (one fromCharCode call per cipher-loop iteration).
  2. Fetches-then-execs in two JS-native shapes cradleRe's single-line shell-pipe idiom (curl ... | sh) can't see: a callback-based https.get(url, () => spawn(dest)) on native Windows, and — inside WSL — a PowerShell command assembled at runtime from decoded fragments, so the actual cradle never exists as literal source text for any regex to match, encoded or not.

A byte-faithful reproduction (live IOCs replaced with RFC 5737 / .invalid placeholders) scored exit 0 on default flags and topped out at exit 2 (gate-eligible, never flagged) even under -fail-on-eligible — no flag combination pre-patch blocked it.

The fix

Three new zero-execution/regex markers in internal/installsurface/analyze.go (D-04 — detecting the obfuscation IS the finding):

  • xorCharCodeRe — a ^ inside a fromCharCode(...) argument list. Gated tightly so an XOR used for something unrelated elsewhere in the file doesn't match. Sets CapObfuscation.
  • asyncCradleRe — a network fetch with an exec-family token as an argument within the same statement (bounded at the next ;). The JS-native analog of curl ... | sh. Deliberately same-statement-scoped to avoid re-triggering the D-25/D-28 esbuild FP (fetch in one statement, exec of an already-downloaded binary in an unrelated later one). Sets CapCradle → VC-002f, block-tier.
  • interpreterSpawnRe + co-occurrence — a spawned script interpreter (powershell/pwsh/cmd/wscript/cscript/mshta) is CapExec alone; it only escalates to CapCradle when CapObfuscation is also present in the same hook. This closes the WSL branch: it can't decode a runtime-assembled command (D-04 forbids trying), so it scores the co-occurrence of "decodes something" + "hands something to a script interpreter" as the finding itself.

Review finding — a gap in the handoff's own test suite

The shipped asyncCradleRe negative test used a synthetic downloadPrebuiltBinary(url) wrapper, which never reaches any of the four matched network verbs (https.get/request, fetch, axios.get/post) — so it never actually exercised the same-statement boundary it claimed to guard. Closed by adding a test against the real esbuild_regression_test.go fixture (fetch in one function, execFileSync in a separate later one) instead of a stand-in — it passes, confirming the marker is sound against the actual reference FP case, not just a synthetic one.

Also swept the full testdata/ corpus for any existing fixture combining an interpreter spawn with a decode-ish token (the FP risk the handoff itself flags for the co-occurrence check): none found.

Validation

  • 5 unit tests (opu32_test.go), all positive/negative pairs, plus the added real-esbuild-fixture regression.
  • Live end-to-end: a byte-faithful BRIDGEHEAD reproduction (inert placeholders) scores exit 0 → exit 1 (VC-002f block/critical, VC-002e gate-eligible/high) — matches the handoff's claimed before/after table exactly.
  • Full suite green (34 packages), -race clean, go vet silent, gofmt no diffs.

Residual, disclosed rather than silently accepted

  • interpreterSpawnRe co-occurrence is a heuristic (decode + separate interpreter-spawn scored together, not a resolved cradle) — worth a pass against a larger live-fire corpus if FP reports surface.
  • asyncCradleRe's bounded window is a heuristic bound, not an AST-balanced parser (RE2 has no backreferences/lookaround).
  • The C2 octet-array assembly (['193','70',...].join('.')) was investigated and confirmed not a gap — CapNetwork detection was never string-literal-only.
  • The WSL-detection triad is not escalated as a standalone signal (still reads as ordinary CapEnv); a dedicated WSL-fingerprint + payload-fetch/exec co-occurrence check is scoped out as a future increment.

Files

  • internal/installsurface/analyze.go — the three markers.
  • internal/installsurface/opu32_test.go — tests, plus the added real-esbuild-fixture regression.
  • docs/DECISIONS.md — D-124.

🤖 Generated with Claude Code


Generated by Claude Code

CloudSEK's BRIDGEHEAD writeup describes an npm typosquat campaign whose
dropper defeats depSNORT's existing markers two ways: a hand-rolled
XOR-over-fromCharCode decode loop (not base64/hex/gzip, not the existing
batch-assembly shapes), and a fetch-then-exec cradle in two JS-native
shapes cradleRe's single-line shell-pipe idiom can't see — a callback-based
https.get(url, () => spawn(dest)) on native Windows, and inside WSL a
PowerShell command assembled at runtime from decoded fragments, so the
actual cradle never exists as literal source text at all. A byte-faithful
reproduction scored exit 0 on default flags and topped out at exit 2 even
under -fail-on-eligible.

Three new zero-execution/regex markers (D-04): xorCharCodeRe (a ^ inside a
fromCharCode argument list, gated so an unrelated XOR elsewhere doesn't
match); asyncCradleRe (a network fetch with an exec-family token in the
SAME statement, bounded at the next ';', deliberately scoped to avoid the
D-25/D-28 esbuild FP); and interpreterSpawnRe + co-occurrence (a spawned
script interpreter only escalates to a cradle when CapObfuscation is also
present in the hook — scores the indirection since the runtime-assembled
command can't be resolved statically).

Found and closed a gap in the handoff's own test suite during review: its
asyncCradleRe negative used a synthetic downloadPrebuiltBinary() wrapper
that never reaches a matched network verb, so it never actually exercised
the same-statement boundary. Added a test against the real
esbuild_regression_test.go fixture instead (fetch in one function, exec in
a separate later one) — passes. Swept testdata/ for any existing fixture
combining an interpreter spawn with a decode token: none found.

Live end-to-end: exit 0 -> exit 1 (VC-002f block/critical, VC-002e
gate-eligible/high) on a byte-faithful reproduction, matching the handoff's
claimed table. Full suite green (34 packages), -race clean. Records D-124.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PLd1shywzWPsLgkpLxEyPj
@MoSLoF
MoSLoF merged commit 7a01106 into main Aug 25, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants