Two pieces of malware, four months apart, on opposite ends of the developer world. One is an npm worm that got into more than 400 packages. The other is a macOS infostealer that hides inside Xcode projects. They share almost no code and probably no operator.

They attack the same thing: the moment a developer turns source into software. Not the repository, not the artifact, not the registry. The build.

And both of them walk straight past the controls the industry built after the last round of supply chain attacks - because those controls verify that a process ran correctly, and in both cases the process did run correctly.

[INFO]
Primary sources: Unit 42, ChainDrop: Inside a Self-Propagating npm Worm (6 August 2026), and Unit 42, The Xcode Assassin Returns: A Deep Dive Into the Latest XCSSET Version (31 July 2026, Adva Gabay and Noa Dekel). Every indicator, hash and figure below is theirs.
◈ interactive artifact
What Happens When You Build
The same question at every stage: which security control should have fired, and why did it not?

//ChainDrop: The Worm That Brought Its Own Runtime

ChainDrop infected over 400 npm packages with a combined weekly download count in the hundreds of millions. The entry point is the oldest trick in the registry: a preinstall hook, running node setup.mjs the moment the package is installed.

What happens next is less usual. setup.mjs downloads Bun 1.3.13 - from the real Oven repository on GitHub, a genuine signed release of a legitimate JavaScript runtime - and uses it to execute a 727 KB obfuscated payload.

Bringing your own interpreter is a quietly effective move. The organisation has spent years deciding what Node may do. Bun arrives over HTTPS from a trusted GitHub URL, is not on any blocklist, is a tool thousands of developers install deliberately, and executes the payload in a process whose name means nothing to the endpoint agent.

It knows where it is

The worm checks whether it is running in CI. On a developer workstation it detaches into the background. Inside a CI job it runs inline, in the job, because a detached process in an ephemeral runner dies with the container and a job that finishes early takes its secrets with it.

That distinction is the whole design. This is not a workstation stealer that happens to run in CI. It is a CI credential harvester that tolerates workstations.

On GitHub Actions runners it goes further than reading files. It locates the Runner.Worker process, reads /proc/<pid>/maps and /proc/<pid>/mem, and searches live memory for OIDC tokens and runner secrets - without waiting for anything to be written to disk. Secret scanning that watches the filesystem sees nothing, because nothing touches the filesystem.

The haul: cloud IAM credentials and temporary tokens, npm and GitHub tokens, SSH keys, developer tooling configs, Kubernetes service-account tokens, CI runner secrets.

The C2 has no domain to block

Rather than hardcoding a server, ChainDrop queries an Ethereum smart contract - 0xE1f2395ee43e45A1556EC6438a88c31B83493103 - and reads its C2 addresses out of contract state.

You cannot seize an Ethereum contract. You cannot take it down with an abuse report. The read is an ordinary RPC call to a public node, and the operator rotates destinations with a transaction. Unit 42 documents exactly that: on 4 August 2026, transaction 0xc55920f1bd0531b6738153068a666c080ddded47e6256f1fd980d51c0b507c91 moved the worm from npm-cache[.]com to awqhnjewqjkl[.]icu.

That rotation is also an interesting unforced error. The original domains - npm-cache[.]com, pypi-get[.]com, js-mirror[.]com - were chosen to blend into developer traffic. awqhnjewqjkl[.]icu is keyboard mash on a cheap TLD. Whoever rotated was optimising for speed of replacement over camouflage, which usually means the old infrastructure had just become a problem.

If the domains fail entirely there is a third layer: the worm searches GitHub commits for the marker thebeautifulmarchoftime, expecting a replacement configuration to be sitting in a commit message somewhere on a public repository.

Stolen tokens published in public

Exfiltrated data is JSON-serialised, gzipped, then encrypted with AES-256-GCM and RSA-OAEP-SHA256 before going out to /router on the current C2.

GitHub tokens get separate treatment. They are Base64-encoded twice and published as commit messages, prefixed with the string IfYouBlockThisAPIKeyItWillCrashTheLiveProductionServersOfAllThirdPartyClients. Other instances of the worm then search for that marker and harvest the tokens themselves.

There is no server in that path. The exfiltration channel is GitHub, the storage is GitHub, the distribution to other worm instances is GitHub, and the prefix is a social engineering payload aimed at whichever human eventually finds it.

The worm also reads C2 responses as JSON and evaluates a returned code field, which gives the operator targeted remote execution on any infected host without shipping a new version of the malware.

The part that should worry you most

Unit 42 found logic - not yet observed in the wild - targeting /opensearch-js repositories during the release-drafter.yml workflow. Instead of a preinstall hook, it adds an optional dependency typosquatting the @opensearch-project scope:

"optionalDependencies": { "@opensearch/setup": "github:opensearch-project/opensearch-js#<commit>" }

Then it uses the repository's own OIDC token to request a Fulcio certificate and publish valid SLSA v1 provenance through Sigstore.

Read that again. The provenance is genuine. The signature chains correctly. The attestation truthfully states that this artifact was built by that workflow in that repository - because it was. Sigstore did its job perfectly.

Build provenance answers "did this come from where it claims?". It was never designed to answer "was the thing that built it honest?". An attacker who executes inside your build inherits your provenance, and the stronger your supply chain attestation, the more convincing their output.

Infrastructure timeline

On 22 May 2026 between 13:40:28 and 13:40:36 UTC, three C2 domains were registered eight seconds apart - scripted, not typed. Fourteen minutes later FixedFloat moved 0.01805723 ETH to wallet 0x55F9780e...f31cD. On 25 May that wallet deployed the resolver contract and populated it, narrowing to npm-cache[.]com at 02:35.

Unit 42 found 453 public repositories across five GitHub accounts carrying the exfiltration marker Shai-Hulud: Here We Go Again, with account names like sardaukar-futar-421 and harkonnen-ghola-669. The earliest dated 11 May; the newest appeared 25 minutes before they found it. Whatever else that tells you, the campaign was still actively spreading at the moment of publication.

//XCSSET v40: Waiting Inside the Project

XCSSET takes a different road to the same place. It injects a downloader script into benign files inside Xcode projects and Git repositories, and then waits. Nothing happens until the developer builds the project locally. The build is the trigger.

That makes it a supply chain attack on software that has not shipped yet. A developer who clones a repository, opens it and builds - the most ordinary action in their day - executes the first stage.

From there, four stages. An initial loader that calls home to /a. A reconnaissance stage that fingerprints the host. A staging applet that loads the final stage into memory. Then the core "boot" orchestrator, which runs specialised modules in memory - and, in Unit 42's words, the moment that loop is active the malware terminates its staging processes and deletes all installation files from disk.

There are 17 modules. The ones that matter most are the two new ones.

Chrome, driven from the inside

The browser hijack does not inject into pages or install an extension. It wraps the Chrome binary in a malicious persistence script, relaunches Chrome with DevTools Protocol arguments listening on a predefined local port, and connects a chrome_remote binary to that port.

CDP is how automated testing drives Chrome. It is a supported interface with full page control. Through it the module executes arbitrary JavaScript in the user's real, logged-in session: it intercepts MetaMask's Ethereum provider to manipulate transactions, and overrides password-manager fields to harvest credentials.

No malicious extension for the user to spot, no injected DLL, and a browser that is genuinely Google's, genuinely signed, and genuinely doing what the flags it was launched with instruct.

Telegram, replaced wholesale

The tdesktop module downloads a malicious Telegram.app archive, replaces the legitimate installation, and ad-hoc code-signs the replacement. Configuration lives in ~/.tr with state tracked in ~/.tr_map.

Ad-hoc signing is worth noting: it is not a stolen or purchased Developer ID. It is the local signature macOS applies to a binary with no identity at all, enough to satisfy the parts of the system that require a signature rather than a trusted one.

Built to defeat exactly the controls you have

Polymorphism against hashes. The loader is recompiled every few hours. Unit 42 observed eight distinct hashes in 24 hours. Any IOC hash you write down has a shelf life measured in a single shift.

Aged domains against new-domain heuristics. Around 40 domains registered in four bursts in early 2026 across .ru and .in, then left to age before use. The "newly registered domain" rule that catches most commodity infrastructure sees nothing unusual.

Layered crypto against network inspection. AES-256-CBC with a per-build key and randomised IV on the wire; a module-level Caesar cipher with a randomised 52-character alphabet and variable shift underneath.

Fileless persistence against file-based hunting. A Base64-encoded staging payload stored in a macOS defaults preferences domain, under plausible-looking keys such as mpirv_eahpi_apm.

Direct attacks on the defences. It disables the SoftwareUpdate configuration channel, terminates CloudTelemetryService, takes an exclusive file lock on the endpoint's YARA rule database (XPdb) so it cannot be updated, and runs tccutil reset AppleEventsto wipe the user's automation consent decisions.

Sandbox discipline. The stats module checks CPU and hardware metadata and reports a verdict to the C2. Hosts that look virtual receive no further modules at all. You can detonate the loader in a VM all day and never see the interesting half.

And then they reused an SSL certificate

All four operator IPs share a single SSL thumbprint, 6e480d648fa1b70612f5d198a66875e28847547d. They also reused SSH keys and a shared self-signed RDP certificate across the estate.

Every piece of malware discipline above, defeated by the one part nobody recompiles every few hours. That thumbprint is a stable pivot across the entire infrastructure and it will outlive every file hash in the report. It is also the single most useful indicator in it.

//What The Two Have In Common

Both execute during a routine developer action. Installing a dependency; building a project. Neither requires a mistake beyond doing the job.

Both target credentials rather than data. Tokens, keys, service accounts, OIDC. The payload is access to the next thing.

Both resolve C2 out of band. An Ethereum contract with a GitHub-commit fallback; forty aged domains on rotating URI paths. Blocking a domain is not a response to either.

Both use real, signed, legitimate software as the execution vehicle. Bun from the official GitHub release; Chrome via its own debugging protocol; Telegram, replaced but signed.

And both beat the specific control that was supposed to stop them. ChainDrop produces valid Sigstore provenance. XCSSET produces a new hash before your feed has ingested the last one. Neither is a bypass in the sense of a flaw being exploited. Both are the control doing precisely what it was specified to do, on an input its specification never contemplated.

//What To Actually Do

Treat CI runner secrets as already compromised on any incident. ChainDrop reads them out of process memory. Rotation is the only remediation that means anything, and it has to include cloud IAM, npm, GitHub, SSH and Kubernetes tokens reachable from the runner.

Disable install scripts by default. npm ci --ignore-scripts, with an explicit allowlist for the handful of packages that genuinely need a build step. This is unglamorous, it breaks a few things once, and it removes ChainDrop's entire entry point.

Alert on unexpected runtimes appearing in CI. A job that pulls a Bun release it was never configured to use is a strong signal that survives obfuscation, C2 rotation and repacking.

Watch for Chrome launched with remote debugging flags by anything that is not your test harness. CDP on a local port in a user session is not normal, and unlike hashes it does not change every four hours.

Pivot on the infrastructure, not the files. The XCSSET SSL thumbprint, the Ethereum contract address, the GitHub markers. These are the durable parts of both campaigns.

Understand what your provenance actually attests.If your policy is "we only run artifacts with valid SLSA provenance", write down explicitly what that excludes. It does not exclude an attacker running inside the build that produced the attestation.

[IOC]
ChainDrop - all from Unit 42, 6 August 2026
Ethereum C2 resolver contract 0xE1f2395ee43e45A1556EC6438a88c31B83493103
Rotation tx 0xc55920f1bd0531b6738153068a666c080ddded47e6256f1fd980d51c0b507c91 (4 Aug 2026)
C2: npm-cache[.]com, pypi-get[.]com, js-mirror[.]com, awqhnjewqjkl[.]icu
Endpoints: /router, /cdn-cgi/rum
math_init.js / Math_Symbol.js SHA256 9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc
setup.mjs SHA256 54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668
setup.mjs SHA256 fd3ca4007b225fdf8de7af4345a19179d5efa8c4bb9205f88cda806e5684b1eb
Artifacts: .vscode/tasks.json, .vscode/setup.mjs, .claude/settings.json, .claude/setup.mjs, .claude/math_init.js, ~/.local/bin/gh-token-monitor.sh, ~/.config/systemd/user/gh-token-monitor.service, ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
GitHub search markers: thebeautifulmarchoftime, IfYouBlockThisAPIKeyItWillCrashTheLiveProductionServersOfAllThirdPartyClients, Shai-Hulud: Here We Go Again

XCSSET v40 - all from Unit 42, 31 July 2026
SSL thumbprint (all four operator IPs) 6e480d648fa1b70612f5d198a66875e28847547d
C2 IPs: 91.108.106[.]229, 95.142.35[.]34, 95.142.35[.]206, 95.142.37[.]159, 151.243.109[.]188, 178.208.92[.]129, 178.208.92[.]168
Sample domains: accapple[.]ru, adschecks[.]ru, adsmobi[.]ru, adsmorein[.]in, amdcdn[.]ru, amzndev[.]in, applecdn[.]ru, appletime[.]in, bulksec[.]ru, cdnapple[.]in, chromeads[.]ru
URI paths: /a, /d/, /s/, /l, /u, /p, /w?, /e
Host artifacts: ~/.tr, ~/.tr_map, preferences-domain keys such as mpirv_eahpi_apm

//Sourcing and Uncertainty

Everything factual above is from the two Unit 42 reports named at the top. Package counts, hashes, contract and transaction addresses, the repository and account counts, the module list, the thumbprint, the timings - all theirs. File hashes for XCSSET are deliberately not reproduced here, because the loader is recompiled every few hours and a hash from a July report is of no operational use in September.

The interpretation is mine: the argument that both attacks target the build step specifically, the reading of the C2 domain rotation as a rushed replacement, the claim that valid Sigstore provenance is a structural problem rather than an implementation one, and the ranking of the SSL thumbprint as the most durable indicator in the XCSSET set.

Gaps worth stating. The OpenSearch provenance logic was found in the code by Unit 42 but was not observed executing in the wild; treat it as capability, not incident. The two campaigns are not linked by any evidence I have seen - pairing them is an analytical choice about technique, not an attribution claim. The Shai-Hulud: Here We Go Againmarker places ChainDrop in the lineage of earlier npm worms by name; I have not verified whether the same operators are behind both. And Unit 42 describe XCSSET's numbering as v40 - I have not independently confirmed the versioning scheme or what happened to the thirty-odd versions in between.