Back to Blog

OVHcloud CLI Compromised via GitHub Release Swap: Detonating the Real GlassWorm Loader and What We Found

An attacker swapped the binaries on five OVHcloud CLI releases, stayed live for 94 minutes, and deleted the evidence. We rebuilt the exact attacker build, recovered the GlassWorm loader from an orphaned commit, and detonated the full chain in an isolated chamber. The replay shows that install.sh never exposed arm64 Linux, Homebrew was never a path in, and a single ovhcloud version command was enough to spot the malicious build.
Owais Shaikh
View LinkedIn

October 19, 2026

Share on X
Share on X
Share on LinkedIn
Share on Facebook
Follow our RSS feed
Table of Contents

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

ReleaseStatus on October 19
v0.11.0 – v0.12.0Binaries replaced (per OVHcloud)
v0.13.0 – v0.15.0Binaries + checksums.txt replaced (per issue #300 capture)
v0.10.0 and earlierClean
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:

  1. 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.
  2. 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.json re-run throttle, then polls getSignaturesForAddress on nine public Solana RPC providers for the dead-drop wallet.
  3. The newest transaction memo carries {"link": <base64 URL>}: on-chain C2 configuration that cannot be seized.
  4. Stage 3 is fetched with an os: linux fingerprint header and served base64-encoded, with AES key material in response headers named ivbase64 and secretkey, then executed inside a Node vm context with require passed 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 in atob.)

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

  1. install.sh never exposed arm64 Linux. The script builds the asset name from uname -m, which is aarch64 on arm64 Linux, but GoReleaser publishes Linux_arm64; that asset name never existed. Script installs on arm64 Linux always 404'd. Those users were exposed only via ovhcloud upgrade (which maps runtime.GOARCH correctly and verifies nothing) or manual download. Published exposure tables, including The Hacker News's, include this path.
  2. Homebrew was safe. OpenSourceMalware listed brew install --cask ovh/tap/ovhcloud-cli as an infection path and Help Net Security repeated it. The v0.15.0-era cask pins per-arch SHA-256, including the original 5c2beb56… 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).
  3. The one-command tell. The missing-ldflag fingerprint was public, but the resulting user-visible behavior was not documented: ovhcloud version prints undefined plus the attacker's commit hash, and upgrade self-identifies the binary as a development build.

Timeline

All times are UTC.

DateTime (UTC)Event
2025-10-20Developer installs color-picker-universal 2.8.91; persistent NodeJS backdoor (162.33.177.177)
2026-03-05/06Four personal repos force-pushed with Solana dead-drop loaders
2026-10-1308:35Commit 5ead8ef merged; later used as the build base
2026-10-1514:34Account deletes test/trigger-probe branches in ovhcloud-docs
2026-10-1912:47First swapped assets land on v0.15.0
2026-10-1912:57v0.15.0 checksums.txt replaced to match
2026-10-1913:12v0.13/v0.14 (+ v0.11/v0.12) swapped; replacement ends
2026-10-1913:50Customer opens issue #300 (pinned-SHA CI failure)
2026-10-1914:21OVHcloud deletes all swapped assets
2026-10-1917:16Clean, immutable, attested v0.16.0 published
2026-10-2012:49GHSA-862f-55f5-vmvw published (~250 downloads disclosed)
2026-10-2120:26Loader 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.

PlatformMalicious archive SHA-256Malicious binary MD5Original archive SHA-256
Darwin_arm64not published6f2f086239f3d88eb2750e871901927d26b78ca0045dd21db040b2991d8a301bafcb898454ea80b5285a04e184132142
Darwin_x86_64not published347e65eb7f2d78d1c6872690272a6cc35aadbdc9f9eae915fdb13e189fa8e5b492b302418c8b9ef3b57d720128fbbee8
Linux_arm643833daffcaaaab27e4e0e0833240ffcb1df8df648f4ec0bc4a75c176e28933cb01f57206d37c28cf711677f6120529135c2beb56e6d83a4b426565a1557791ef759316abcc4effb38dd627c90aeb80e0
Linux_x86_64not published301bff0b51980b9e76c93633e7cfd3bd1109d157dcf677bf3a7616e4b27b4fe3b5cc984e1cc82987592016541a6c88ed
Windows_x86_64not published6a3cf153338c4161697cda2fe704c8848not published

Infrastructure, accounts, and artifacts.

TypeIndicatorContext
Build commit5ead8ef9b43d354dfb5caadff803b11afc8c5c582026-10-13T08:35:05Z; pseudo-version v0.15.1-0.20261013083505-5ead8ef9b43d
Exfil endpointhttps://45.32.148[.]174/uploadSolana-RPC-shaped JSON POST, archive base64 in params
Backdoor C2 (2025)162.33.177[.]177Persistent NodeJS service from color-picker-universal 2.8.91
Solana dead-drop walletBjVeAjPrSKFiingBn4vZvghsGj9KCE8AJVtbc9S8o8SCTransaction memos carry the stage-3 link
On-host artifact/tmp/envchecker-*Stealer staging directories; never deleted
On-host artifact~/init.jsonmacOS 48-hour re-run throttle file
Loader (stage 1)HassenBF/next-dashboard@227207ce preinstall.jsSHA-256 43df42fd79f6f81e530892b54634598770d6e496796a863c596e616bcb3fec70; recovered post-rollback from orphaned commit
Loader (stage 2)decrypted stage-2 scriptSHA-256 0f79989912572a24ba13baabb32b3fd084496271c51448d47b66fe1aedbf293c; AES key zetqHyfDfod88zloncfnOaS9gGs90ONX, IV a041fdaa0521fb5c3e26b217aaf24115
GitHub accountHassenBFAsset 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.

  1. 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).
  2. Delete the binary and any /tmp/envchecker-* directories; reinstall v0.16.0 and verify the attestation (gh release verify-asset v0.16.0 <file>).
  3. Revoke and reissue PATs and tokens used on the machine since October 19.
  4. 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


Check whether ovhcloud-cli release artifacts are in your environment: Start Free with StepSecurity →

Explore Related Posts

‍