MOJO LAYERS·security
FILED 2026‑08‑14  /  PART THREE
Follow‑up · supply chain incident

Four layers deep. Here's where we got stuck.

In part two, we broke the encryption on a live C2 and found a multi‑stage loader chain underneath. We kept going. This is the honest account of what the deepest stages actually look like — strong evidence of remote command execution, a decryption puzzle we're still mid‑solve on, and a wall we hit on purpose rather than climb around.

The short version

We followed both stage‑2 loaders from part two to their next hop: one led to a plain‑text HTTP endpoint on a different port, the other to a JSON endpoint carrying two further encoded blobs. Decoding what we could without executing anything, the shape of the code is hard to misread — a callback signature matching Node's child_process exec pattern, a branch that runs commands through Python, and REPL‑style output formatting, wrapped so it runs in a browser, Node, or Angular app alike. One of the two remaining blobs uses a genuinely more sophisticated obfuscator than everything else in this chain — different enough that we stopped there rather than reach for tooling that would blur the line between reading code and running it.

01

Picking up from part two

Part two ended with the encryption cracked and two decrypted responses that turned out to be loaders, not payloads. Each pointed at a further endpoint on the same server we'd already confirmed live. We fetched both — still fetch‑only, still nothing evaluated — and decoded what came back.

One endpoint, reached over plain HTTP on a different port, returned ciphertext that decrypts with a key drawn from the same string table as everything else. The other returned real JSON with two fields: one holding a further encrypted blob, the other holding what turned out to be plain (if heavily obfuscated) JavaScript, stashed for later use rather than decrypted on receipt.

02

What the fourth stage's shape tells us

The encrypted JSON field decodes to a hundred‑kilobyte, fully readable stage. It opens by constructing a function via the Function() constructor and immediately defines LZ‑String inside it — a real, widely‑used open‑source compression library, doing exactly what it's meant to do. That's a legitimate engineering choice on the attacker's part: compress before embedding, which incidentally also shrinks how much suspicious‑looking text a scanner has to work with.

Further into that same stage, before it hits code we can't yet read, the shape is hard to misread. There's a function whose callback signature is (error, stdout, stderr) — the exact pattern Node's child_process module uses. There's a branch that prefixes a value with "py" before handing it to that function. There's code building a prompt string ending in "> ", the same convention as an interactive shell. And the whole thing closes with a standard UMD environment‑detection wrapper — checking for define, module, and angular — so it runs the same way whether it lands in a browser tab, a Node process, or an Angular app.

Exec‑style callback found
(error, stdout, stderr)
Language branches seen
Shell & Python
Runs in
Browser, Node, or Angular
Anything executed by us
Nothing, ever

We're stating this as what the code's structure strongly indicates, not as a fully decompiled, line‑by‑line capability list. That's a real distinction and we're not going to blur it for a cleaner headline. But a REPL‑shaped callback that runs shell and Python commands, reachable through a live, multi‑stage, blockchain‑resolved loader chain, is not a subtle thing to find sitting where a "list files" endpoint's name would suggest something much smaller.

03

A decryption puzzle, live

The other new endpoint — the one on the different port — decrypts using the same by‑hand, execution‑free method from part two: extract the key from the inert string table, apply the algorithm exactly as written in the source, nothing evaluated. It works. Mostly.

The first roughly one‑third of the response decrypts cleanly into readable, syntactically ordinary obfuscated JavaScript — wrapper functions, a self‑defending integrity check we recognize from every other stage in this chain, a fully intact 928‑entry string table. Then, at one exact, repeatable byte position, it stops being readable. Not gradually — a clean plaintext character sits immediately before a control byte that has no business being there, right where the next quoted property name should start.

the exact breakpoint, byte‑preciseclean text stops, corruption starts, same spot every time
// last clean fragment before the break:
...new j(a0d)['SaPQVB'](),a0d['[control byte, not a valid identifier start]

Two explanations fit what we're seeing: a bug in our reconstruction of this stage's specific decoding scheme, or a defect introduced during capture. We tested the second one directly — a clean re‑fetch of the same endpoint, compared byte‑for‑byte against the original. They're identical, all 57,053 bytes. The server returns the exact same response deterministically. That rules out network flakiness and capture corruption entirely — whatever's happening, it isn't a transport problem.

That leaves the algorithm. And the evidence there is genuinely mixed: everything up to the break decrypts into completely valid JavaScript — not approximately valid, not mostly‑readable‑with‑typos, but a fully intact, correctly‑structured 928‑entry string table sitting inside otherwise coherent code. A wrong key doesn't produce that. Repeating‑key XOR with the wrong key garbles everything from the first byte, not the first nineteen thousand. So the key is very likely right, the algorithm is very likely right, and something more specific — plausibly in exactly how the malware's own response‑accumulation logic behaves across the underlying network chunk boundaries, which we can't fully see from the outside even with a byte‑identical capture — is the actual gap.

STATUS: Byte‑identical re‑fetch confirmed — not a network or capture issue. The remaining gap is narrower than when we started (an algorithm subtlety, not a wrong key) but still open. We're calling this one honestly unresolved rather than spending indefinitely more time on a single stage when the structural evidence in section 02 already answers the question this post set out to ask.
04

The wall, and why we stopped there

The remaining unread material — the field we mentioned earlier that was stashed rather than decrypted on receipt — is a quarter‑megabyte of JavaScript using a meaningfully different obfuscator from everything else in this chain. Every other layer we've broken so far uses some variant of the same pattern: a shuffled array of short string fragments, plus a small lookup function to reassemble them. Once you find the array and the lookup function, the rest is arithmetic.

This one doesn't have that shortcut. It uses generator functions and switch/with statements to flatten its own control flow, and instead of one shared lookup table, it generates a freshly shuffled character alphabet for nearly every individual encoded value. There's no single array to extract and no single key to find — the structure is deliberately built to resist exactly the technique that worked on every layer above it.

Breaking it properly, at this point, would mean one of two things: reverse‑engineering its exact custom encoding from scratch through pure manual analysis, which is a substantially larger undertaking than anything else in this investigation so far, or pointing a real deobfuscation tool at it — which means parsing and transforming the code as a program, not just reading it as text. That second option is closer to running it than we're willing to get. We said at the outset that we'd fetch and read, never execute, and "run a proper AST tool against untrusted code" is close enough to that line that we're treating it as being on the wrong side of it.

05

Code review: four stages, three obfuscators

Updating the review from part two now that we've seen deeper into the chain.

The obfuscation strategy across the whole chain

Assessed from four independently decrypted stages, read only

Escalating sophistication by depth, not uniform effort everywhere. The outer stages use a common, well‑understood obfuscation pattern — breakable with patience and a lookup table. The deepest stage we've found uses something meaningfully harder. That's a sensible way to spend obfuscation budget: cheap protection on the parts likely to get caught early, real protection on the part that matters most if someone gets this far.

A real dependency, not reinvented crypto. Reaching for LZ‑String instead of writing a custom compressor is the same instinct we praised in part two for the RPC failover logic — competent, unglamorous engineering, not attacker mythology.

!

The chain is only as strong as its weakest stage, and three of four are weak. Everything up to the final layer falls to the same technique every time: find the array, find the lookup function, do the arithmetic. An analyst with time and patience gets most of the way through this chain regardless of how hard the last stage is.

Staging strategy: A− Consistency across stages: C+
06

Where this actually leaves us

We have strong, structurally‑grounded evidence that this chain terminates in something capable of running arbitrary shell and Python commands on an infected host, wrapped to work in whatever JavaScript environment it lands in. We do not have a fully decompiled, exhaustive list of exactly what it exfiltrates, and we're not going to manufacture false certainty by rounding "the shape of the code strongly indicates X" up to "the code does X." The honest state is: mostly mapped, partially read, one genuine puzzle still open, one wall we chose not to climb.

If you run anything that fetched from the repos named in part one, or anything downstream of them, that's the operating assumption to work from regardless of how this specific puzzle resolves: assume command execution capability, not just credential scraping, and remediate accordingly.