Don't Revoke That Token Yet: Inside the keyv/cacheable npm Worm
When you learn that a compromised package executed on one of your build hosts, muscle memory takes over: revoke the npm token, rotate the GitHub PAT, cycle the cloud keys. That reflex has been correct in almost every supply-chain incident I have worked. In the keyv/cacheable compromise that has been unfolding since yesterday, it is the one thing you should not do first — because revoking the stolen token is exactly what arms the payload.
Let me back up.
What happened
On August 4, 2026, an attacker took over the maintainer account behind the widely used keyv and cacheable npm namespaces — caching libraries that sit near the bottom of a very large number of dependency trees — and published trojanized releases. Socket's Threat Research team, which did the primary analysis, places the first malicious release, [email protected], at 09:35 UTC. The poisoned versions ship a preinstall hook:
"scripts": { "preinstall": "node setup.mjs" }
setup.mjs downloads a standalone Bun runtime, runs an obfuscated second stage (Math_Symbol.js, ~728 KB), and harvests whatever it can reach: AWS instance metadata, cloud keys, Vault tokens, Kubernetes service-account tokens, GitHub Actions secrets, npm tokens, plus a generic regex sweep for private keys and bearer tokens on disk. Then — and this is why the campaign grew from roughly ten packages to several hundred within hours — it uses the stolen npm token to inject the same hook into other packages the compromised identity can publish, recomputes the integrity hashes, and republishes. It is a worm. The public IOC lists now cover more than 440 packages across two thousand-plus versions, and they are still moving.
Two properties make this one worth a closer look than the average typosquat.
It does not need npm install
Most teams scope this kind of incident to "who ran npm install in the exposure window." That misses half the population. The source repository also received IDE and agent autostart hooks — a SessionStart entry in .claude/settings.json and a folderOpen task in .vscode/tasks.json — that run the loader when the cloned folder is simply opened. No install, nothing built.
Sit with who that includes. It includes the security engineer who cloned the repository to investigate the incident after reading about it. It includes the AI coding agent that opened the directory to "take a look." I do not think we have seen AI-agent configuration files used as a first-class supply-chain execution vector at this scale before, and it is worth internalizing: a checked-out repository is now an execution surface, and .claude/, .cursor/, and .vscode/ are part of it.
It punishes remediation
Here is the part that should change how you respond. Alongside the credential theft, the payload installs a host-level dead-man's switch. It writes the stolen GitHub token and an attacker-supplied handler command to ~/.config/gh-token-monitor/, then persists itself as a macOS LaunchAgent or a Linux systemd user service with loginctl enable-linger so it survives logout. The systemd unit describes itself, helpfully, as a "GitHub Token Validity Monitor," so at a glance it reads like a developer convenience.
A watcher script polls the GitHub API with the stolen token every 60 seconds. While the token works, nothing happens. The moment the token stops working — an HTTP 4xx, which is precisely what your revocation produces — it evals the remote-supplied handler string, then deletes its own state and exits. It is single-shot and self-clearing, and it also self-destructs after a 24-hour TTL.
What is in the handler? Public analysis cannot say, because it is attacker-controlled text pulled at runtime and can be changed remotely. It could be data destruction, re-implant, or nothing at all. That is the whole problem: the risk is not that the trap does something specific and known — it is that you cannot assess it, and it fires at the exact moment your team believes it is containing the incident and starts to relax.
One consequence is counterintuitive but load-bearing: isolating the host from the network is safe. With no connectivity there is no HTTP response, so there is no 4xx, so the switch does not fire — and exfiltration stops at the same time. Isolate first. Do not power off; volatile memory is evidence.
Why the usual checks miss it
- "The signature was valid."
[email protected]shipped with a passing SLSA attestation. Provenance attests to build integrity, not source integrity — the legitimate workflow faithfully built already-trojanized code. - "The diff was clean." The library itself was not modified. The malice lives in
package.jsonand two added files. Adist/comparison shows nothing. - "We don't use keyv." You almost certainly do, transitively. The common path is
eslint → file-entry-cache → flat-cache → keyv. Very few victims installed any of these directly. - "Nobody ran
npm install." See the second section.
What to actually do
The order matters more than the individual steps:
- Isolate the host from the network. Safe, for the reason above. Do not shut it down.
- Preserve evidence before you delete anything — the watcher self-clears in ~24 hours. Copy
~/.config/gh-token-monitor/{handler,token,started_at}, the payloads, the plist/unit, and record hashes. Do not execute the handler; treat it as inert text.started_atbounds your exposure window. - Eradicate: kill the watcher, unload the LaunchAgent / disable the systemd unit, drop
loginctllinger, remove the files and the.claude/.vscodehooks, and clear the package caches. - Rotate — now, and only now. npm token first, to stop propagation; then GitHub, cloud, Vault, Kubernetes, CI secrets, and anything that was sitting in a file, because there was a regex sweep. Revoke, do not merely rotate.
- Audit what was done in your name: repositories freshly described "Shai-Hulud: Here We Go Again," unexpected npm publishes under your accounts, and credential use in your cloud logs during the
started_atwindow.
CI runners and any host with confirmed execution should be rebuilt, not cleaned. Arbitrary code ran; the list of known artifacts is not a completeness guarantee.
A small tool to help with the triage
Enumerating this by hand across a fleet is tedious, and the moving IOC list makes a hardcoded grep obsolete within hours. I wrote a scanner to help with the triage: it checks lockfiles and node_modules for the compromised name/version set (with the transitive chain, so "we don't use keyv" gets answered on the spot), flags the host persistence and the dead-man's switch, and prints the response order above so nobody rotates before cleaning.
It is built to be easy to trust during exactly this kind of incident: one auditable file you can read in fifteen minutes, zero dependencies, zero egress (it never phones home; --update is the only network call and it is explicit), and read-only. It runs offline. It is MIT-licensed and open source, and — disclosure — it comes out of my work at Securest8; the IOC data is not mine but the public research of Socket, Wiz, and Kodem, credited in the repository.
If you only take the tool, take the response order with it. The scanner finds the problem; the order in which you touch credentials is what keeps a bad day from getting worse.
Bottom line
The novel part of this campaign is not the credential theft — it is the two design choices around it: an execution path that does not require installing anything, and a switch that turns your remediation reflex into the trigger. Scope the second vector, isolate before you revoke, and clean the host before you touch a single token.
References
- Socket, "Popular npm Packages in the keyv and Cacheable Namespaces Compromised in Active Supply Chain Attack," August 4, 2026. https://socket.dev/blog/popular-npm-packages-in-the-keyv-and-cacheable-namespaces-compromised-in-active-supply-chain
- Wiz Research, public IOC feed (keyv/cacheable). https://github.com/wiz-sec-public/wiz-research-iocs/blob/main/reports/keyv-packages.csv
- Wiz, "keyv and cacheable npm supply chain attack." https://www.wiz.io/blog/keyv-and-cacheable-npm-supply-chain-attack
- Kodem Security, keyv supply-chain attack IOCs and first-hour runbook. https://www.kodemsecurity.com/resources/keyv-supply-chain-attack-shai-hulud-npm-worm-affected-versions-iocs-and-first-hour-response-runbook
--
Botnet Hunting for Vulnerabilities in Diagnostic Tools
This morning, I noticed specific sources "hunting" for vulnerabilities in URLs that I haven't noticed before. All of these URLs appear to be associated with diagnostic tools:
| URL | Count | Vulnerability |
|---|---|---|
| / | 1 | (simple recon for index page) |
| /apply.cgi | 20 | CVE-2024-12856 Four-Faith router command injection |
| /cgi-bin/adv_ping.cgi | 20 | ? |
| /cgi-bin/diagnostic.cgi | 20 | CVE-2013-7179 Seowon Intech WiMAX SWU-9100 mobile route |
| /cgi-bin/DiagnosticsMsg.cgi | 20 | ? |
| /cgi-bin/ping.cgi | 20 | |
| /cgi-bin/system_mgr.cgi | 20 | |
| /cgi-bin/traceroute.cgi | 20 | |
| /diag_ping.cgi | 20 | CVE-2020-8949 (maybe.. slightly different URL) Gocloud devices |
| /goform/diagTool | 20 | CVE-2024-48419 (maybe..) Edimax Routers |
| /goform/ping | 20 | |
| /ping_test.cgi | 20 | |
| /sys_diag.html | 20 |
The naming of these URLs points to diagnostic tools. I was unable to find any specific vulnerabilities associated with many of the URLs, but the table above reflects those I found. But diagnostic tools often suffer from file inclusion and code execution vulnerabilities.
These tools will often call operating system commands directly, without properly separating user-provided arguments. Here is a sample vulnerability in a ping utility:
response = os.system("ping -c 1 -w2 " + hostname )
The above example is in Python. But most (all?) languages have something equivalent to "os.system" (often called "exec", "shell_exec", "process" ...) Often, proper input validation and output encoding are used to prevent this vulnerability, but, in my opinion, there is a better approach that should always be used in addition to input validation, and I do not see it used much.
As with many other vulnerabilities, the root cause of command injection is the concatenation of user data and commands. Mixing control plane and data plane has been an issue since blue boxing and continues today with prompt injection. The real fix is to avoid this comingling of data and commands and instead properly separate them. Prepared statements in SQL are probably the best-known approach following this principle.
For OS command execution, we do have a very similar solution. The "system" command in your language will typically call the standard C function "exec" [1]. This family of function implements some meant to pass command line arguments: execv ("exec vector"). In addition to the command, it accepts an array of command-line arguments that are then passed to the command, properly separating the command from the arguments.
Python implements execv as part of the subprocess module:
response = subprocess.run("ping", "-c", 1, "-w", 2, hostname )
Using "subprocess.run" eliminates the possibility of command injection in this example.
For example, if you are using "google.com; ls" as a hostname, you get:
ping: cannot resolve google.com; ls: Unknown host
The entire string "google.com; ls" was used as a hostname, and the ";" no longer acted as a separator. Give it a try with other command injection strings, and you will see similar results.
There are a few cases where "execv" is not sufficient. Some operating system commands may execute additional commands passed on the command line. For example, tcpdump offers the "-z" option to execute a "postrotate command". But these cases are rare, and if you are running into them, you are back to proper input validation to use these specific command line options. In most cases, users cannot specify the command-line option itself but only the parameter; using the "execv" API will help.
A while ago, I also made a brief video with more details on preventing OS command injection: https://www.youtube.com/watch?v=7QDO3pZbum8. It also covers some of the issues around Windows, which implements different APIs.
[1] https://man7.org/linux/man-pages/man3/exec.3.html
--
Johannes B. Ullrich, Ph.D. , Dean of Research, SANS.edu
Twitter|
1 Comments
Atomic MacOS (AMOS) stealer infection
Introduction
This diary provides indicators from an Atomic MacOS (AMOS) stealer infection that I generated in my lab on July 31st, 2026. This was distributed through a web page from getmacouscloud[.]com with instructions to paste text into a macOS Terminal window, supposedly for "macOS toolkit," but instead the text is a command to retrieve and install AMOS stealer malware.
Of note, I ran the text in the Terminal window twice, because I wanted to make sure I retrieved copies of files in the host's /tmp directory before entering the user account password. This is why the initial infection traffic is repeated, and also likely why there are two different directories with the AMOS stealer malware persistent on my infected lab host.
Images from the Infection

Shown above: Website with instructions to copy and paste text into a Terminal window, supposedly for a "macOS toolkit" but actually for malware.

Shown above: The malicious text pasted into a Terminal Window on a macOS host.

Shown above: Files from my infected host's /tmp directory, showing data stolen and other info for AMOS stealer.

Shown above: Examples of AMOS stealer persistent on my infected macOS host.

Shown above: Traffic from the AMOS stealer infection filtered in Wireshark.
Indicators of Compromise
Traffic leading to the getmacouscloud[.]com page on Friday 2026-07-31:
- hxxps[:]//macostruecloud[.]xyz/?h=2f9548d041648a8030c040ae0e1e530b&z=304
- macspheres[.]com - HTTPS traffic
- hxxps[:]//getmacouscloud[.]com/?FSSbmnNdviEDE5S?io=16vwsb0rgIiPNIgM
URL from the base64 text provided by getmacouscloud[.]com for the initial download:
- hxxps[:]//render65[.]com/curl/f5509695dd98a9732378e5256d6235415d64d92194459bb08525c7ce5991a0c9
URLs from extracted from the payload returned from the initial download:
- hxxps[:]//grove-89[.]com/api/metrics/run?event=pasted
- hxxps[:]//render65[.]com/2kqYRM0DCrnyJgoS4gVLl_FHJRRdTUhGCbjyuYwpZ6c/m1/update
AMOS stealer C2 traffic - HTTP POST requests over TCP port 80:
- hxxp[:]//188.166.78[.]138/api/metrics/run?event=started&stage=boot
- hxxp[:]//188.166.78[.]138/api/metrics/run?event=stage&stage=init_session
- hxxp[:]//188.166.78[.]138/api/metrics/run?event=stage&stage=messengers
- hxxp[:]//188.166.78[.]138/api/metrics/run?event=stage&stage=credentials
- hxxp[:]//188.166.78[.]138/api/metrics/run?event=stage&stage=browsers
- hxxp[:]//188.166.78[.]138/api/metrics/run?event=stage&stage=wallets
- hxxp[:]//188.166.78[.]138/contact
- hxxp[:]//188.166.78[.]138/api/metrics/run?event=stage&stage=resolve_auth
- hxxp[:]//188.166.78[.]138/api/metrics/run?event=stage&stage=local_data
- hxxp[:]//188.166.78[.]138/api/join/
- hxxp[:]//188.166.78[.]138/api/bots/device-info
- hxxp[:]//188.166.78[.]138/api/tasks/ack
- hxxp[:]//188.166.78[.]138/api/feed/register
AMOS stealer C2 traffic - examples of HTTP GET requests over TCP port 80:
- hxxp[:]//188.166.78[.]138/api/tasks/r3dqbX7fptIT-gXz--D_nw?v=2.1
- hxxp[:]//188.166.78[.]138/api/feed/items/49359f77ebb4ffd9a95568d27a8ff3e7
SHA-256 hash: b9ec3261d633c289e51c5fa8842af4350efe68446df39cb995de82e0941d0f3c
- File size: 1,973 bytes
- File type: Paul Falstad's zsh script text executable, ASCII text
- File description: Initial file retrieved by malicious text in Terminal window
SHA-256 hash: 13b868b3ea8b492e7fbab1ca04535c53d0930650185b5a082cd59c1974689cd5
- File size: 1,227 bytes
- File type: Paul Falstad's zsh script text executable, ASCII text, with very long lines (315)
- File description: Script extracted from a gzip-compressed file from base64 text in the above file
SHA-256 hash: 9f25ec533cb23d020e568fb771500d7776b1300f07119ad9d0876f4329ce22ab
- File size: 297,952 bytes
- File location: /tmp/helper
- File type: Mach-O universal binary with 2 architectures: x86_64 & arm64
SHA-256 hash: 0a03cf18de28017c0ea591dffc380a6b41fedd2acc3a39e901e58d9188c01836
- File size: 438,656 bytes
- File location: /Users/[username]/Library/Application Support/.com.apple.accountsd/AccountsHelper
- File type: Mach-O universal binary with 2 architectures: x86_64 & arm64
SHA-256 hash: 01a0d5332b09bb299f7784bf0d0c43c4199269ed6a0712377279eeb999847d20
- File size: 503,152 bytes
- File location: /Users/[username]/Library/Application Support/.com.apple.metadata.mds/mdworker_shared
- File type: Mach-O universal binary with 2 architectures: x86_64 & arm64
---
Bradley Duncan
brad [at] malware-traffic-analysis.net
0 Comments
Phishing Campaigns Targeting AI Solutions Providers
Most phishing campaigns rely on the fact that the victim is afraid to loose "something": money, access to information, ... Many brands have been impersonated by campaigns but I spotted some phishing emails that focus on AI services like ChatGPT.
Yesterday, I found this email that was properly designed but also sent with a very good timing: the end of the month when your classic billing process is restarted!

The threat actor is just trying to grab your payment details:

Seeing the importance of AI used by most companies but also residential users, this is a clever move from threat actors! Many people will be afraid to loose their access to ChatGPT.
Xavier Mertens (@xme)
Senior ISC Handler | SANS Principal Instructor | Freelance Consultant
Xameco | PGP Key
0 Comments

0 Comments