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:
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:
| File | v1.1.0 | v1.1.2 / v1.1.3 |
|---|---|---|
treeify.js | 3.7 KB, 116 lines | 96.9 KB, 116 lines |
package.json | Legitimate treeify | Identical copy |
test/tree-test.js | Legitimate | Identical |
| All other files | Legitimate | Identical |
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
[TRAP-HIT]canary string recovered fromtreeify.jsin v1.1.0, matching the documented TrapDoor canary format - The
nest-1-airdrop-merklecampaign tag, consistent with TrapDoor’s Solana and DeFi targeting profile - The seeding pattern: a clean canary version first, malicious versions second, published within one day
- The use of a shared payload name (
trap-core.jsin prior TrapDoor packages) and the identical targeting scope: crypto, DeFi, Solana developer environments
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
| Indicator | Type | Value | Method |
|---|---|---|---|
@osmura/treeify | npm package | 1.1.2, 1.1.3 | Identified in OSV MAL-2026-6542 |
treeify.js v1.1.2 | Malicious file | SHA256: 612b89ae1789817f9d1cca75f7c054010c1a1628afe538b31f1dced58c11a4b4 | Hash confirmed against tarball we extracted; matches OSV advisory |
package.json v1.1.2 | File in tarball | SHA256: bf5119c67c496a4174811894b622d38278bfd7c2ac268fb2f323639f7d1e2211 | Hash confirmed against tarball we extracted; matches OSV advisory |
treeify-1.1.2.tgz | Tarball | SHA1: 3714725cbf68087940ee1830ddddf4d087795f8d | SHA1 verified against registry and OSV package_integrity |
[TRAP-HIT] @osmura/treeify@1.1.0 - nest-1-airdrop-merkle | Campaign canary string | Written to stdout on execution | Read directly from treeify.js v1.1.0, line 115 |
osmura-rsesmt@web-library.net | Publisher email | npm account osmura | Pulled from registry metadata during triage |
| AES-256-GCM with XOR-derived key | Payload decryption | 4 base64 buffers XOR-combined | Recovered from OSV advisory analysis and structural inspection of IIFE |
windowsHide: true, detached: true | Spawn config | 3 spawn sites per IIFE, 6 total | Recovered from structural analysis of treeify.js injected section |
Affected Versions
| Version | Published (UTC) | Tarball SHA1 | Current Status | OSV Entry |
|---|---|---|---|---|
| 1.1.0 | 2026-06-21 02:20:29 | ac768093b8edb71e6a5b3fafd2a46dc6163ef3f8 | Live (canary, no payload) | Not in OSV |
| 1.1.2 | 2026-06-22 09:24:53 | 3714725cbf68087940ee1830ddddf4d087795f8d | Live | IN-MAL-2026-007673 |
| 1.1.3 | 2026-06-22 09:37:58 | de2a520d3af5608a8048d9e940028782245b289f | Live | IN-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
-
Search
package-lock.json,yarn.lock, andpnpm-lock.yamlfor@osmura/treeify. Any version should be removed. Version1.1.0carries no payload but is part of the same attacker-controlled scope and should not remain installed. -
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. -
Review egress logs for outbound HTTPS connections from Node.js processes during the exposure window. The payload fetch is a
GETwith a 60-second timeout. Any unexpected HTTPS GET from Node.js to an unfamiliar host in that window warrants investigation. -
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. -
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.
