Attackers replaced the binaries on five published releases of OVHcloud's official CLI on October 19, 2026, live for 94 minutes, downloaded around 250 times, and deleted before anyone outside OVHcloud archived a copy. We replayed the delivery chain with artifacts, against a rebuild of the exact source commit under the attacker's build flags, and detonated the attacker's GlassWorm loader, recovered from an orphaned commit, against a controlled Solana dead drop. The replay reproduces the forensic signature, exposes a one-command tell, and yields three findings: install.sh never exposed arm64 Linux, Homebrew was never an exposure path, and one subcommand identified the swapped build.
Overview
On October 19, 2026, between 12:47 and 14:21 UTC, an attacker using the GitHub account HassenBF replaced release binaries on v0.11.0 through v0.15.0 of ovh/ovhcloud-cli with a credential stealer. The account belongs to an OVHcloud contributor; initial access traces to October 2025 and the first observed attacker activity to March 2026. No tag was moved. No CI workflow ran. The source repository was never touched. The attacker uploaded locally built binaries straight onto existing releases, replacing checksums.txt last so the published hashes matched the new files.
The swap was spotted within 94 minutes by a customer whose CI pins the SHA-256 of the Linux arm64 tarball; issue #300 documents both the pinned-hash CI failure and the build-metadata analysis that followed. OVHcloud deleted the swapped assets, shipped a clean, immutable, attested v0.16.0, and published a detailed advisory (GHSA-862f-55f5-vmvw).
Action Required
If you downloaded or ran any ovhcloud binary from GitHub releases between 12:47 and 14:21 UTC on October 19, 2026:
- Treat that host as fully compromised. Assume the exfiltration succeeded. The advisory's target list includes AI-agent credential stores (Claude, Hermes, Continue, Cody, OpenCode), cloud CLIs (AWS, GCP, Azure, doctl), Docker and Kubernetes configs, Terraform, NPM, SSH keys,
.netrc, and.gitconfig. - Rotate every secret the machine could read, from a different machine. Updating or reinstalling the CLI revokes nothing.
- Check for
/tmp/envchecker-*directories (see IOCs below). The stealer stages its collection there and never deletes the folder. - Reinstall from the clean, immutable v0.16.0 and verify:
gh release verify-asset v0.16.0 ovhcloud-cli_Linux_x86_64.tar.gz.
Homebrew users were not exposed, and neither were go install, source builds, or the Docker image.
What Happened
| Release | Status on October 19 |
|---|---|
| v0.11.0 – v0.12.0 | Binaries replaced (per OVHcloud) |
| v0.13.0 – v0.15.0 | Binaries + checksums.txt replaced (per issue #300 capture) |
| v0.10.0 and earlier | Clean |
| v0.16.0 (published 17:16 UTC) | Clean, immutable, signed tag, release attestation |
The root cause was mechanical: GitHub release immutability was not enabled, so any account with write access could swap assets on months-old releases at unchanged URLs. The attacker replaced per-release assets newest-first over about 25 minutes: v0.15.0 at 12:47, its checksums.txt at 12:57, then v0.14.0 and v0.13.0 through 13:12.
Two details in the reporter's forensics mattered. First, the swapped binary was not built by the release pipeline: go version -m showed a pseudo-version (v0.15.1-0.20261013083505-5ead8ef9b43d+dirty) built from commit 5ead8ef (a main-branch commit from October 13, never a release tag) with -ldflags="-s -w" and without GoReleaser's -X .../internal/version.Version ldflag. Second, the uploader (HassenBF) was not a public org member.
Our reproduction confirms the signature: rebuilding the real 5ead8ef tree with a plain go build -ldflags="-s -w" yields the identical pseudo-version. One caveat for public IOC guidance: such a build shows vcs.modified=false where the reporter captured +dirty. The dirty bit depends on the build tree; the pseudo-version and the missing -X version ldflag are the reliable markers.
How the Attack Works
Step 1: Initial access, one year before the attack. Per OVHcloud's postmortem, the developer installed a malicious VS Code extension (color-picker-universal 2.8.91) on a personal MacBook on October 20, 2025. It installed a persistent NodeJS service (registered via plist) that communicated with 162.33.177.177: the IDE-extension playbook GlassWorm has used since 2025, previously documented by Truesec, Checkmarx, and others. The release uploads used stolen keys, not the developer's machine; OVHcloud does not believe the corporate laptop was compromised.
Step 2: Dormancy inside a real contributor account. Between May and September 2026, the account kept landing ordinary pull requests in ovh/ovhcloud-docs. On October 15 it deleted branches named dev/test-changest and test/trigger-probe, names that read like CI-trigger probing, four days before the swap.
Step 3: A ForceMemo-style dry run in March. On March 5–6, 2026, four of the account's personal repos were force-pushed with hidden preinstall.js loaders. The amend-and-force-push technique matches what we documented in ForceMemo, down to the Solana dead-drop wallet (see IOCs).
Step 4: The swap. Build the CLI from the October 13 tip of main with an embedded credential stealer, upload over five existing releases, replace checksums.txt to match. The build was a local go build, stripped, without the release pipeline's version ldflag; what the payload did when run is documented in the advisory (target list, envchecker-* staging, exfil endpoint).
The Blast-Chamber Replay
The real binaries were deleted at 14:21 UTC, so we rebuilt the chain from the repository's own source, which the attacker never touched. We also used artifacts such as the install.sh, source trees, and the actual release layout, served from a fake github.com reporting v0.15.0 as latest inside a fully isolated Docker network. The rebuild uses the attacker's exact build flags, so the binary it produces carries the same embedded build metadata as the swapped release, plus a collection routine matching the advisory's target list, so the run behaves the way the advisory says the real one did. The clean v0.16.0 build ran as a control under the same instrumentation and made zero network connections; every request in the timeline below belongs to the v0.15.0 replay.
All times are chamber-relative (T+ seconds from the curl).
T+0: install.sh fetches latest. The script hits releases/latest, parses tag_name: v0.15.0, and downloads ovhcloud-cli_Linux_x86_64.tar.gz. It performs no checksum verification at any point; it never reads checksums.txt.
T+2: installed. The binary lands in ~/.local/bin/ovhcloud. First run (ovhcloud version) prints the tell nobody reported in detail:
$ ovhcloud version
┌───────────┬──────────────────────────────────────────┬──────────────────────┐
│ version │ last_commit │ last_commit_time │
├───────────┼──────────────────────────────────────────┼──────────────────────┤
│ undefined │ 5ead8ef9b43d354dfb5caadff803b11afc8c5c58 │ 2026-10-13T08:35:05Z │
└───────────┴──────────────────────────────────────────┴──────────────────────┘
A release binary that answers undefined and prints the attacker's build commit and its timestamp in the same table. Any local build from a checkout of 5ead8ef without the release pipeline's -X version ldflag produces this. Issue #300's go version -m capture shows the real binary carried the same vcs.revision=5ead8ef, so victims running ovhcloud version saw the same table. ovhcloud upgrade also refuses with "upgrade is not available in development builds." A single subcommand was sufficient to identify the swapped build.
What the real binary did when victims ran it is documented in the advisory: collection of AI-agent credential stores (VS Code, Cursor, Continue, Cody, OpenCode, Claude, Hermes), cloud and container credentials (AWS, GCP, Azure, Docker, Kubernetes, Terraform, NPM, doctl), SSH keys, .netrc, and .gitconfig; staging in never-deleted /tmp/envchecker-* directories; and exfiltration of an archive to https://45.32.148.174/upload as Solana-RPC-shaped JSON. The chamber exercised that behavior end to end.
T+4: exfiltration complete. The collection routine read 18 secret paths, staged a tar.gz in a never-deleted /tmp/envchecker-XXXX/ directory, and POSTed it to the advisory's exfil endpoint as Solana-RPC-shaped JSON with the archive base64-encoded in params. Canary tokens confirmed the full collection arrived in a single 1,949-byte archive:
/home/dev/.ovh.conf CANARY-OVH-APP-KEY / SECRET / CONSUMER
/home/dev/.aws/credentials CANARY-AWS-AKID / SECRET
/home/dev/.ssh/id_ed25519 CANARY-SSH-KEY (full key)
/home/dev/.config/Code/.../state.vscdb CANARY-VSCODE-STATE
/home/dev/.config/Cursor/.../state.vscdb CANARY-CURSOR-STATE
/home/dev/.claude/.credentials.json CANARY-CLAUDE-OAUTH
+ .netrc, .gitconfig, .npmrc, .docker/config.json, .kube/config,
.terraform.d, gcloud, .azure, .continue, .opencode, .hermes
Every subsequent run re-steals. Two invocations produced two exfil archives, 9 seconds apart.
The checksum swap only affected manual verification. install.sh never reads checksums.txt (the script contains no verification at all), and the swapped checksums matched the swapped binaries, so any verifier trusting the repository's own checksums passed. Only an externally pinned hash catches the swap. That is how the reporter caught it, and it is why Homebrew, whose cask pins per-arch SHA-256, refused the swapped files and was never an exposure path.
Detonating the Real GlassWorm Loader
Two days after disclosure, on October 21, the four loader repos were force-pushed again, restoring their pre-compromise history. The loaders vanished from every branch. They are still retrievable online: we recovered preinstall.js from orphaned commit 227207ce (SHA-256 matches the public IOC feeds) and detonated the unmodified file in the chamber against a controlled dead drop.
The chain, as executed:
- 181 bytes of visible JavaScript. Everything else is invisible variation-selector characters (U+FE00–FE0F encode 0–15; U+E0100–E01EF encode 16–79) that decode to an AES-256-CBC wrapper with a hardcoded key and IV.
- Stage 2 waits 10 seconds, exits on Russian locale/timezone/UTC+2..+12 (the geofence GlassWorm has used since the IDE campaigns), honors a 48-hour
~/init.jsonre-run throttle, then pollsgetSignaturesForAddresson nine public Solana RPC providers for the dead-drop wallet. - The newest transaction memo carries
{"link": <base64 URL>}: on-chain C2 configuration that cannot be seized. - Stage 3 is fetched with an
os: linuxfingerprint header and served base64-encoded, with AES key material in response headers namedivbase64andsecretkey, then executed inside a Nodevmcontext withrequirepassed in; the sandbox does not constrain module loading. (We learned the base64 requirement empirically: our first attempt served the script as plaintext and the loader crashed inatob.)
With the drop cold (see below), the loader polled its RPC list every 10 seconds, trying providers in order until one answered, indefinitely and without crashing: the stranded-infection steady state. When none of the nine RPC endpoints were reachable instead, the loader did not retry quietly: it crashed with an unhandled atob InvalidCharacterError. Blocking Solana RPC egress therefore leaves a visible crash in Node logs rather than a silent stranded poller. With our drop live, the chain completed: memo → stage-3 fetch → vm execution → RCE marker and beacon, all inside 35 seconds.
The dead drop is cold today. The wallet returns an empty signature list on the public RPCs we queried (others are now paywalled), consistent with the CrowdStrike-coordinated takedown that disrupted all four GlassWorm C2 channels on May 26, 2026. Machines still carrying this loader continue to poll a dead drop that the operator can re-arm at any time by publishing a new memo transaction.
What the Replay Found
install.shnever exposed arm64 Linux. The script builds the asset name fromuname -m, which isaarch64on arm64 Linux, but GoReleaser publishesLinux_arm64; that asset name never existed. Script installs on arm64 Linux always 404'd. Those users were exposed only viaovhcloud upgrade(which mapsruntime.GOARCHcorrectly and verifies nothing) or manual download. Published exposure tables, including The Hacker News's, include this path.- Homebrew was safe. OpenSourceMalware listed
brew install --cask ovh/tap/ovhcloud-clias an infection path and Help Net Security repeated it. The v0.15.0-era cask pins per-arch SHA-256, including the original5c2beb56…hash, so a brew install during the window would have downloaded the swapped file, hash-mismatched, and refused. The tap's git history is also how we recovered the complete original v0.15.0 hash matrix (IOCs below). - The one-command tell. The missing-ldflag fingerprint was public, but the resulting user-visible behavior was not documented:
ovhcloud versionprintsundefinedplus the attacker's commit hash, andupgradeself-identifies the binary as a development build.
Timeline
All times are UTC.
| Date | Time (UTC) | Event |
|---|---|---|
| 2025-10-20 | Developer installs color-picker-universal 2.8.91; persistent NodeJS backdoor (162.33.177.177) | |
| 2026-03-05/06 | Four personal repos force-pushed with Solana dead-drop loaders | |
| 2026-10-13 | 08:35 | Commit 5ead8ef merged; later used as the build base |
| 2026-10-15 | 14:34 | Account deletes test/trigger-probe branches in ovhcloud-docs |
| 2026-10-19 | 12:47 | First swapped assets land on v0.15.0 |
| 2026-10-19 | 12:57 | v0.15.0 checksums.txt replaced to match |
| 2026-10-19 | 13:12 | v0.13/v0.14 (+ v0.11/v0.12) swapped; replacement ends |
| 2026-10-19 | 13:50 | Customer opens issue #300 (pinned-SHA CI failure) |
| 2026-10-19 | 14:21 | OVHcloud deletes all swapped assets |
| 2026-10-19 | 17:16 | Clean, immutable, attested v0.16.0 published |
| 2026-10-20 | 12:49 | GHSA-862f-55f5-vmvw published (~250 downloads disclosed) |
| 2026-10-21 | 20:26 | Loader repos force-pushed again, history rolled back (our observation) |
Indicators of Compromise
File hashes (v0.15.0 release assets). Malicious binary MD5s are from the advisory; the Linux arm64 malicious archive SHA-256 is from issue #300; original archive SHA-256s were recovered from Homebrew tap history (the cask covers macOS and Linux only). The Linux arm64 value is the only malicious archive SHA-256 published anywhere: we checked the advisory, issue #300, the incident coverage, two independent writeups, and public IOC feeds, and every other source republishes the binary MD5s instead. The reporter's CI pinned only the Linux arm64 artifact, so the other platforms' swapped archives were most likely never hashed before deletion.
| Platform | Malicious archive SHA-256 | Malicious binary MD5 | Original archive SHA-256 |
|---|---|---|---|
| Darwin_arm64 | not published | 6f2f086239f3d88eb2750e871901927d | 26b78ca0045dd21db040b2991d8a301bafcb898454ea80b5285a04e184132142 |
| Darwin_x86_64 | not published | 347e65eb7f2d78d1c6872690272a6cc3 | 5aadbdc9f9eae915fdb13e189fa8e5b492b302418c8b9ef3b57d720128fbbee8 |
| Linux_arm64 | 3833daffcaaaab27e4e0e0833240ffcb1df8df648f4ec0bc4a75c176e28933cb | 01f57206d37c28cf711677f612052913 | 5c2beb56e6d83a4b426565a1557791ef759316abcc4effb38dd627c90aeb80e0 |
| Linux_x86_64 | not published | 301bff0b51980b9e76c93633e7cfd3bd | 1109d157dcf677bf3a7616e4b27b4fe3b5cc984e1cc82987592016541a6c88ed |
| Windows_x86_64 | not published | 6a3cf153338c4161697cda2fe704c8848 | not published |
Infrastructure, accounts, and artifacts.
| Type | Indicator | Context |
|---|---|---|
| Build commit | 5ead8ef9b43d354dfb5caadff803b11afc8c5c58 | 2026-10-13T08:35:05Z; pseudo-version v0.15.1-0.20261013083505-5ead8ef9b43d |
| Exfil endpoint | https://45.32.148[.]174/upload | Solana-RPC-shaped JSON POST, archive base64 in params |
| Backdoor C2 (2025) | 162.33.177[.]177 | Persistent NodeJS service from color-picker-universal 2.8.91 |
| Solana dead-drop wallet | BjVeAjPrSKFiingBn4vZvghsGj9KCE8AJVtbc9S8o8SC | Transaction memos carry the stage-3 link |
| On-host artifact | /tmp/envchecker-* | Stealer staging directories; never deleted |
| On-host artifact | ~/init.json | macOS 48-hour re-run throttle file |
| Loader (stage 1) | HassenBF/next-dashboard@227207ce preinstall.js | SHA-256 43df42fd79f6f81e530892b54634598770d6e496796a863c596e616bcb3fec70; recovered post-rollback from orphaned commit |
| Loader (stage 2) | decrypted stage-2 script | SHA-256 0f79989912572a24ba13baabb32b3fd084496271c51448d47b66fe1aedbf293c; AES key zetqHyfDfod88zloncfnOaS9gGs90ONX, IV a041fdaa0521fb5c3e26b217aaf24115 |
| GitHub account | HassenBF | Asset uploader on the five swapped releases |
Am I Affected?
If you installed via the script or a direct download (macOS any, Linux x86_64, Windows x86_64):
ovhcloud version # "undefined" + commit 5ead8ef... = malicious
go version -m $(which ovhcloud) | head -5
ls -d /tmp/envchecker-* # stealer staging dirs
If you are on arm64 Linux and used install.sh only: you were not exposed; the script 404'd. If you ever ran ovhcloud upgrade or downloaded the tarball manually, you were.
If you used Homebrew, go install, source builds, or the Docker image: not exposed.
CI pipelines: audit any job that fetched releases/download/v0.1[1-5].0/* between 12:47 and 14:21 UTC on October 19, and treat runner-hosted credentials reachable from those runners as exposed. The reporter caught this with one line: a pinned SHA-256. Pin your hashes.
Recovery
Treat any machine that ran a swapped binary as fully compromised. Assume the exfiltration succeeded.
- From a different machine, rotate everything the host could read: OVH API keys, cloud keys, Kubeconfigs, container registry creds, npm tokens, SSH keys,
.netrc, git credentials, and the AI-agent stores the advisory explicitly targets (Claude, Cursor, VS Code, Continue, Cody, OpenCode). - Delete the binary and any
/tmp/envchecker-*directories; reinstall v0.16.0 and verify the attestation (gh release verify-asset v0.16.0 <file>). - Revoke and reissue PATs and tokens used on the machine since October 19.
- If you cannot establish the machine is clean, rebuild it. In our replay, the interval between the start of the install and completed exfiltration was four seconds.
Acknowledgment
Detection credit belongs to alink-tothepast, whose pinned-hash CI job and go version -m capture in issue #300 identified the swap. We thank OVHcloud for the detailed postmortem and the rapid immutable, attested re-release; the advisory's malware analysis is credited to Ivan Kwiatkowski of VirLabs. OpenSourceMalware's account-timeline work informed our loader recovery, building on prior GlassWorm research from Koi Security, Socket, Truesec, Checkmarx, and Aikido. Our replay complements that record; where our chamber data differs from it (arm64, Homebrew), we have included the supporting evidence.
References
- OVHcloud security advisory GHSA-862f-55f5-vmvw
- Issue #300: Release assets of v0.13.0–v0.15.0 replaced
- OpenSourceMalware: Glassworm Hacks OVH Clouds CLI tool
- The Hacker News coverage · Help Net Security · The Stack · BleepingComputer
- Checkmarx: GlassWorm staged malware behind runtime-rebuilt loaders · Truesec: self-propagating VS Code extension worm
- CrowdStrike: Inside the Takedown of a Developer-Targeting Botnet
- StepSecurity: ForceMemo
- Full chamber telemetry, scripts, and raw logs: research/findings/REPORT.md
Check whether ovhcloud-cli release artifacts are in your environment: Start Free with StepSecurity →



