report: npm/app-soda-layer 2.1.6 (credential + wallet stealer, SSH backdoor) - #1405
report: npm/app-soda-layer 2.1.6 (credential + wallet stealer, SSH backdoor)#1405rbnfrhnr wants to merge 9 commits into
Conversation
…ckdoor) Signed-off-by: Robin Frehner <frehner.robin@gmail.com>
5a2c7d8 to
d151ad3
Compare
Signed-off-by: Robin Frehner <frehner.robin@gmail.com>
|
Updated to redact the SSH key's comment field. The retrieved key carried a comment impersonating the support address of a real, unrelated company. On reflection I don't think it belongs in a public malware record: naming the impersonated company attaches it to this entry for no detection benefit, since the base64 body is the correct thing to match on regardless — the comment can be changed without changing the key. The key body itself is unchanged and complete. The report notes that the comment is redacted deliberately rather than unknown, and that the unredacted line stays retrievable from The corresponding write-up has been updated the same way. |
|
Hi, thanks for your contribution to the Malicious Packages repo! We're always excited to have more contributors! :) |
| } | ||
| } | ||
| ], | ||
| "details": "`app-soda-layer@2.1.6` executed a credential and cryptocurrency-wallet collector, and installed a remote-access backdoor, on `npm install`. It was published 2026-07-27T18:47:46Z and unpublished by its author 2026-07-27T21:13:07Z, an availability window of 2 hours 25 minutes. It is no longer retrievable from the registry; the source below was reconstructed from a system-call trace of an installation performed in an isolated sandbox at 2026-07-27T20:58:33Z.\n\nThe registry metadata record that survives the unpublish shows the package name created at `18:47:46.475Z` and its only version published at `18:47:46.759Z` — 284 milliseconds apart, with no prior release. This was not a compromise of an established package; it was purpose-built, and the version number `2.1.6` was chosen to suggest a release history that never existed.\n\n## Trigger\n\n`package.json` declared `\"postinstall\": \"node test.js\"`. No import or invocation by the installing project was required. `test.js` called two routines in `index.js`.\n\n## Routine 1 — fixed-target collection\n\n`from_str_1()` recursively scanned `process.cwd()` for the fixed patterns `id.json`, `config.toml`, `Config.toml`, `env` and `.env`, and POSTed the **contents** of each match to `http://95.216.118.146:3000/api/v1`, prefixed with the value of `$USER`. `maxContentLength` and `maxBodyLength` were both set to `Infinity`, removing the HTTP client's default request-size limits. `id.json` is the default keypair filename for the Solana command-line wallet and contains private key material; `config.toml` is that tool's configuration file.\n\n## Routine 2 — C2-directed collection and SSH backdoor\n\n`from_str_2()` first requested three control endpoints on port 3001 of the same host:\n\n- `http://95.216.118.146:3001/api/ssh-key`\n- `http://95.216.118.146:3001/api/scan-patterns`\n- `http://95.216.118.146:3001/api/block-patterns`\n\nThe file patterns it collects are therefore **not present in the package** — they are supplied by the operator at install time, so the same published artifact can be retargeted without republishing.\n\nThe value returned by `/api/ssh-key` was appended to the user's `~/.ssh/authorized_keys` by `addSshKeyToUser()`, which then ran `sudo chown -R <user>:<user> ~/.ssh` (required, because `sshd` ignores an `authorized_keys` file with incorrect ownership under the default `StrictModes` setting), followed by `sudo ufw enable` and `sudo ufw allow 22/tcp` to permit inbound SSH. Whether the key was installed successfully was reported back to the operator in the metadata accompanying the upload. The function contains an idempotency guard so repeated installs do not append duplicate entries.\n\nMatched files were then swept from the user's home directory (or, on Windows, every drive letter enumerated via `wmic logicaldisk get name` with a PowerShell fallback) and uploaded to `http://95.216.118.146:3001/api/v1` as multipart form data in batches of up to 100 files per request.\n\n## Control-server contents\n\nThe control endpoints were requested directly on 2026-07-28T06:45:57Z, roughly nine and a half hours after the package was withdrawn. All three returned HTTP 200, establishing that the infrastructure was operated rather than abandoned. Requests were GET-only from a disposable host; no data was sent, and the upload endpoints were deliberately not contacted.\n\n`/api/ssh-key` returned the following key, which is the backdoor an affected operator can test for directly:\n\n```\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFYMx8MqdYTD/aZjqxmXo+9460+9EvsSjfiy9YAU+xwY <comment redacted>\n```\n\nThe key's comment field has been redacted here because it impersonates the support address of a real, unrelated company. An SSH key comment is arbitrary text chosen by whoever generated the key, is never validated, and carries no relationship to the address it names; it is best read as camouflage. It is omitted deliberately rather than unknown, so that this entry does not attach an uninvolved company to a malware record. **Match on the base64 body, which is the correct indicator regardless** — the comment can be changed without changing the key. The unredacted line remains retrievable from `/api/ssh-key` on the host below by anyone wishing to verify it.\n\n`/api/scan-patterns` returned the target list, matched as a case-insensitive **substring** of the filename:\n\n```\n[\".env\", \".xls\", \".xlsx\", \"privatekey\", \"private key\", \"seed\", \"wallet\", \"key\", \"phrase\", \"private\"]\n```\n\n`seed` and `phrase` target cryptocurrency wallet mnemonic recovery phrases; the spreadsheet extensions target the common practice of recording such phrases in spreadsheets. `/api/block-patterns` returned a 60-entry exclusion list (dependency directories, caches, source-code extensions, images, binaries) that suppresses the bulk of a developer machine's file count while retaining documents, spreadsheets and anything named for a key or a phrase.\n\nThese lists reflect what the host served on 2026-07-28 and are operator-controlled; they are not necessarily what was served during the package's availability window.\n\n## Concealment\n\nThe `readme.md` was copied verbatim from an unrelated, permissively licensed open-source project and bore no relation to the shipped code. The package declared no description, author, repository or homepage. `child_process` and `os` were listed as dependencies despite being Node.js built-ins. Windows enumeration suppressed subprocess output via `stdio: [\"ignore\", \"pipe\", \"ignore\"]`, `-NoProfile` and `windowsHide: true`. Every stage was wrapped in exception handlers that discard errors, so a failed collection would not cause a visibly failed install.\n\n## Remediation\n\nAny environment where this package appears in a lockfile, CI log or `node_modules` tree dated 2026-07-27 should be checked for the SSH key above in `~/.ssh/authorized_keys`, on every account — a match indicates host access rather than only credential exposure. Firewall state should be checked for port 22 having been newly permitted. Solana keypairs, SSH private keys and any secrets in `.env` files reachable from the install directory should be rotated. Because routine 2's targets were supplied by the control server, the file list above should not be treated as exhaustive.", |
There was a problem hiding this comment.
Nit: can you please make the description more concise?
Signed-off-by: Robin Frehner <frehner.robin@gmail.com>
|
Thanks for the review, and for the welcome! The description indeed was far longer than anything else in the repo. I've cut it from ~6,200 to ~1,800 characters, keeping only what a consumer of the entry needs: what it does on install, the operator-supplied targeting (the file patterns aren't in the package, they're fetched from the C2 at install time), and the implanted SSH key as a checkable indicator. Happy to trim further if it's still too long. |
Signed-off-by: Robin Frehner <frehner.robin@gmail.com>
|
One more update, and this one changes the framing. This payload is not new. It matches the So the package and the C2 are new, the payload is a redeployment. I've added a short paragraph to One difference worth recording: the sibling obfuscates identifiers with reversed string literals ( Also, the SSH public key in this report was not published in either prior write-up, so if it's useful to cross-reference against #1366 that comparison would establish a common operator directly rather than by code similarity. |
|
Can you please trim it a bit more and make it more human-readable? Here's a recent example of a report, which is of a good quality: #1395 We haven't yet published a guideline for contributing to the repo, especially in regards to using AI, but the reports should still be written with a human user in mind. |
…rence Signed-off-by: Robin Frehner <frehner.robin@gmail.com>
|
Trimmed the details to two prose paragraphs in the style of #1395 and dropped the inline link, since the writeup is already in references. This report complements the Amazon Inspector record already on the package rather than repeating it: it adds attribution to the levex-refa/lint-builder toolkit from the February 2026 dev-protocol org compromise (with the cross-reference to #1366, polymarket-mcp-v2), the injected SSH public key that the Amazon finding notes is fetched but does not publish, and the file-target list retrieved from the live C2. Happy to trim further if you would like. |
Adds a report for
app-soda-layer@2.1.6(npm).The package was published 2026-07-27T18:47:46Z and unpublished by its author 2026-07-27T21:13:07Z — an availability window of 2h25m. It is no longer retrievable from the registry, so the source in this report was reconstructed from a system-call trace of an installation performed in an isolated sandbox during that window.
Behaviour
npm installviapostinstall.id.json),config.tomland.envfiles and uploads their contents, with the HTTP client's size limits explicitly removed.~/.ssh/authorized_keys, corrects~/.sshownership sosshdwill honour it underStrictModes, then attemptsufw allow 22/tcp. Implant success is reported back to the operator.Control server still active
The three control endpoints on
95.216.118.146:3001were requested directly and returned HTTP 200 on both 2026-07-28T06:45:57Z and 2026-07-28T11:57:24Z — roughly 9.5 and 15 hours after the package was withdrawn. Responses were byte-identical between the two fetches (sha256 recorded in the report). The infrastructure is operated, not abandoned.Those requests were GET-only from a disposable host; no request body was sent, and the upload endpoints (
/api/v1) were deliberately not contacted.This means the report includes the actual SSH public key the malware implants, so anyone affected can test their own
authorized_keysfor it directly, and the actual target list (seed,phrase,wallet,privatekey,.xls/.xlsx,.env).Note on the key's comment field
The retrieved key carries a comment naming a third-party company's support address. An SSH key comment is arbitrary, unvalidated text chosen by whoever generated the key; it is camouflage and is not evidence of involvement by that organisation. The report states this inline, immediately below the key, because the entry will syndicate and the disclaimer needs to travel with it.
Conventions
idomitted (assigned automatically);summaryomitted (overridden).affected[].versionspinned to2.1.6rather than an open range — the registry record shows the package name and its only version were created 284ms apart, so no other version ever existed.ips(host) andurls(full endpoints) perdocs/schema_additions.md.Full technical write-up, including the recovered source and the syscall evidence: https://protet.io/disclosures/app-soda-layer-2.1.6