CentrioleBlog
Back to blog

Threat Research

What Does a Tree Utility Need 93KB of Obfuscated Code For?

The npm package @osmura/treeify appended 93KB of RC4-obfuscated JavaScript to the legitimate treeify library, running an AES-256-GCM dropper with dual IIFE fallback C2 on every require call.

Date

Reading time

10 min read

Author

Centriole Research
Share
What Does a Tree Utility Need 93KB of Obfuscated Code For?

treeify.js in the legitimate treeify package is 116 lines and 3.7 KB. In @osmura/treeify@1.1.2, it is still 116 lines. It is 96.9 KB. Every byte of the difference is a dropper that executes on every require('@osmura/treeify') call, fetches an AES-256-GCM encrypted payload from an attacker-controlled host, decrypts it using a key XOR-derived from four embedded base64 buffers, writes it to a temp directory, and spawns it as a detached hidden process. A second independent dropper in the same file does the same thing against a separate C2 pool.

Campaign Context

When we looked at version 1.1.0, published June 21, before the malicious versions appeared, we found a line that does not belong in a tree-printing utility:

treeify.js v1.1.0: canary line at end of file
void (typeof Treeify === 'object' && process.stdout.write(
  '[TRAP-HIT] @osmura/treeify@1.1.0 - nest-1-airdrop-merkle\n'
));

That string is a campaign tag. TRAP-HIT is the canary marker used across the TrapDoor campaign, a cross-ecosystem supply chain operation documented by Socket on May 25, 2026 spanning 34 malicious packages across npm, PyPI, and Crates.io. TrapDoor targets developers in crypto, DeFi, Solana, and AI communities. The nest-1-airdrop-merkle tag encodes campaign versioning alongside the specific target community: airdrop and merkle tree are Solana/DeFi primitives. The canary serves two purposes: confirming execution during testing, and tagging which lure generated the hit on the attacker’s logging infrastructure.

The v1.1.0 canary version was published June 21. The malicious versions followed June 22. The attacker seeded the scope first, confirmed the canary executed, then deployed the actual payload.

Package Anatomy

The @osmura scope has no affiliation with the legitimate treeify package authored by Luke Plaster. All three published versions copy package.json verbatim from the real treeify@1.1.0: author field Luke Plaster <notatestuser@gmail.com>, repository pointing to github.com/notatestuser/treeify.git, identical keywords and description. The publisher account osmura registered with osmura-rsesmt@web-library.net. No prior publishing history exists for that account.

The file inventory is identical across all three versions: nine files, same paths, same legitimate content except for treeify.js:

Filev1.1.0v1.1.2 / v1.1.3
treeify.js3.7 KB, 116 lines96.9 KB, 116 lines
package.jsonLegitimate treeifyIdentical copy
test/tree-test.jsLegitimateIdentical
All other filesLegitimateIdentical

93.2 KB of obfuscated JavaScript is the entire malicious surface. The injection is appended as a single line after the closing })); of the legitimate UMD wrapper, making the file count and line count unchanged.

93 KB on a single line. That is not an accident.

Execution Trigger

There is no preinstall, postinstall, or install script in package.json. The injection runs because treeify.js is the main entry point and the obfuscated code executes at the module’s top level. The moment any Node.js process calls require('@osmura/treeify'), the dropper runs before any of the caller’s code executes.

--ignore-scripts stops nothing here.

Payload Analysis

The injected code has three structural components separated by ;; double semicolons.

Component 1: The string decoder. Two functions, a4() and a5(), implement an RC4-based string array decoder using obfuscator.io’s WXS encoding. a4() holds 566 base64-encoded, RC4-encrypted strings. a5() decodes them on demand. The string array rotator, a (function(a4, 0xdcba6){while(!![]){...}})(a4, 0xdcba6) initializer IIFE, shuffles the array until a checksum matches. The while(!![]){} loop with a numerical checksum target is the canonical obfuscator.io control-flow pattern.

No plaintext hostname, path, or crypto parameter appears anywhere in the 93 KB. Every sensitive string passes through a5() at runtime.

Component 2: IIFE dropper 1. A (function(a,b){...}) IIFE with internal wrappers bc() and bd() calling a5() with offsets cE.a = 0xd4 and cF.a = 0xec. We recovered the structural fingerprint from source analysis: it loads https and child_process via decoded require() calls, builds an HTTPS GET request to a hostname decoded from the string array, decrypts the response using createDecipheriv with algorithm aes-256-gcm, derives the key by XOR-combining four base64 buffers also decoded from the array, writes the plaintext to os.tmpdir()/<name>-<pid>/, and spawns it with child_process.spawn(..., { stdio: ['ignore','ignore','ignore'], windowsHide: true, detached: true }).

Six occurrences of windowsHide and detached across the file confirm three spawn call sites per IIFE. The windowsHide: true flag suppresses any console window on Windows. detached: true lets the spawned process outlive the Node.js parent.

Component 3: IIFE dropper 2. A second independent (function(a,b){...}) IIFE with a different internal function naming scheme (bf, bg, bh wrappers rather than bc, bd). At 45.9 KB versus 34.7 KB for IIFE 1, it is the larger of the two. Different string decoding offsets, different encoded URL pool. The OSV advisory describes this as a “separate encoded URL pool providing fallback C2.” If IIFE 1’s C2 is unreachable, IIFE 2 attempts its own pool independently.

Two separate dropper implementations, each with its own C2 pool and its own AES key material. Taking down one C2 does not stop the other.

C2 and Infrastructure

The C2 hostnames are RC4-encrypted inside the string array and cannot be recovered through static analysis alone. The RC4 key derivation uses a function-to-string check ((''+function(){return 0x0;})['indexOf']('\x0a')) that produces different results in Node.js versus a browser V8 context, preventing straightforward dynamic extraction in a Node.js sandbox.

The OSV advisory confirms: “issues an HTTPS request to a hostname encoded inside the obfuscated string array” and “AES-256-GCM-decrypts the response using a key XOR-derived from four embedded base64 buffers.” The use of AES-256-GCM for payload delivery rather than a plain GET to a paste host is a meaningful step up from the simpler droppers in the TrapDoor campaign’s earlier packages. Encrypting the payload at rest on the C2 means network inspection of the response yields nothing actionable without the key material embedded in the package.

We searched for the osmura publisher account and osmura-rsesmt@web-library.net against VirusTotal, Shodan, and prior TrapDoor package lists documented by Socket. Neither appears in any prior threat intelligence feed. The web-library.net email domain is not on known throwaway provider lists, though its pattern (rsesmt random suffix) is consistent with account generation tooling.

OPSEC Failures

The v1.1.0 canary version is the most valuable OPSEC failure. Publishing a test build to the live public registry left a documented record of campaign staging: the attacker confirmed execution behavior with process.stdout.write before deploying the payload in v1.1.2. The canary also named the campaign and the target community in plaintext, inside a package that remained live and downloadable.

The ;; double-semicolon separator between the three injected components is a structural artifact of how the obfuscator.io output was stitched together. It is unusual enough that any scanner looking for this pattern in JavaScript files would flag it immediately.

Attribution

@osmura/treeify uses the TrapDoor campaign delivery architecture. The shared indicators extracted from our analysis:

The TrapDoor campaign was documented by Socket as spanning npm, PyPI, and Crates.io from May 22, 2026. @osmura/treeify was published June 21 to 22, placing it in the campaign’s active window.

IOC Table

IndicatorTypeValueMethod
@osmura/treeifynpm package1.1.2, 1.1.3Identified in OSV MAL-2026-6542
treeify.js v1.1.2Malicious fileSHA256: 612b89ae1789817f9d1cca75f7c054010c1a1628afe538b31f1dced58c11a4b4Hash confirmed against tarball we extracted; matches OSV advisory
package.json v1.1.2File in tarballSHA256: bf5119c67c496a4174811894b622d38278bfd7c2ac268fb2f323639f7d1e2211Hash confirmed against tarball we extracted; matches OSV advisory
treeify-1.1.2.tgzTarballSHA1: 3714725cbf68087940ee1830ddddf4d087795f8dSHA1 verified against registry and OSV package_integrity
[TRAP-HIT] @osmura/treeify@1.1.0 - nest-1-airdrop-merkleCampaign canary stringWritten to stdout on executionRead directly from treeify.js v1.1.0, line 115
osmura-rsesmt@web-library.netPublisher emailnpm account osmuraPulled from registry metadata during triage
AES-256-GCM with XOR-derived keyPayload decryption4 base64 buffers XOR-combinedRecovered from OSV advisory analysis and structural inspection of IIFE
windowsHide: true, detached: trueSpawn config3 spawn sites per IIFE, 6 totalRecovered from structural analysis of treeify.js injected section

Affected Versions

VersionPublished (UTC)Tarball SHA1Current StatusOSV Entry
1.1.02026-06-21 02:20:29ac768093b8edb71e6a5b3fafd2a46dc6163ef3f8Live (canary, no payload)Not in OSV
1.1.22026-06-22 09:24:533714725cbf68087940ee1830ddddf4d087795f8dLiveIN-MAL-2026-007673
1.1.32026-06-22 09:37:58de2a520d3af5608a8048d9e940028782245b289fLiveIN-MAL-2026-007672

Both malicious versions carry identical injected code. The 13-minute gap between them suggests a republish after a minor packaging adjustment, not a payload change.

Remediation

  1. Search package-lock.json, yarn.lock, and pnpm-lock.yaml for @osmura/treeify. Any version should be removed. Version 1.1.0 carries no payload but is part of the same attacker-controlled scope and should not remain installed.

  2. Check os.tmpdir() for unexpected subdirectories matching <name>-<pid> patterns created around the time of the install. If found, do not execute them. Hash them, preserve for forensics, then delete.

  3. Review egress logs for outbound HTTPS connections from Node.js processes during the exposure window. The payload fetch is a GET with a 60-second timeout. Any unexpected HTTPS GET from Node.js to an unfamiliar host in that window warrants investigation.

  4. Rotate credentials accessible from the affected process: npm tokens, GitHub tokens, AWS credentials, SSH keys, and any environment variables present at the time require('@osmura/treeify') ran.

  5. If CI/CD pipelines installed this package, audit that pipeline’s secrets and rotate anything injected as environment variables in that job.

The TrapDoor campaign remains active. The canary-then-payload staging pattern and the dual IIFE fallback architecture suggest an operator who tests infrastructure before deploying and expects individual C2 endpoints to go down. Blocking one hostname does not stop the dropper.