OVHcloud CLI Release-Asset Backdoor (v0.11.0 to v0.15.0)
Executive Summary
Attacker objective. Steal developer and operator secrets. On 2026-10-19 someone using a legitimate OVHcloud contributor's GitHub account swapped the prebuilt archives on five existing releases of the OVHcloud CLI (v0.11.0 to v0.15.0) with a build that reads credentials and files from disk. [#1] [#2] [#4]
How it worked. The uploads came through the ordinary GitHub Releases API. No tag was pushed, no workflow ran, and the source and build pipeline were untouched. OVHcloud's own root cause is that release immutability was not enabled, so a user with write permission could overwrite assets on published releases. [#1] [#3] [#5] The bad files covered macOS (Intel and ARM), Linux (x86_64 and arm64) and Windows x86_64, and were live for about 94 minutes (14:47 to 16:21 Paris time, 12:47 to 14:21 UTC) before OVHcloud deleted them after a customer's pinned-hash CI check failed. [#1] [#2] [#3]
What the binary does. It is the real CLI recompiled with extra Go code. Per OVHcloud it collects IDE and AI-assistant databases, cloud, container and Terraform credentials, SSH keys, .netrc and git config into a folder named envchecker-NNNNNNNNN in the temp directory, archives it, and uploads it to a hard-coded address, using Solana RPC calls through one of five public endpoints. [#1] The advisory reports about 250 downloads. [#1]
Attribution is open. One analysis ties the hijacked account to the GlassWorm campaign through an earlier loader in the account's personal repositories and calls the link an inference. Another report says nothing from the October binary has been tied to GlassWorm because no copy was preserved, and the binary's Solana wallet in the advisory differs from the wallet in that earlier loader. [#3] [#4] [#1] This pack does not assert the attribution and ships none of that campaign's indicators.
Defender priority: find every host or CI runner that downloaded or ran an ovhcloud binary on 2026-10-19 between 12:47 and 14:21 UTC (Q7, Q8, Q1). For any hit, treat every secret readable on that machine as stolen and rotate it from a different machine. Then require immutable releases and pinned hashes for every tool you pull from GitHub Releases.
Source Review & Web Hunter Notes
| # | Tier | Source | Key finding | Carry forward |
|---|---|---|---|---|
| 1 | T1 | OVHcloud GitHub security advisory GHSA-862f-55f5-vmvw (published 2026-10-20) | Timeline, affected versions v0.11.0 to v0.15.0, five malicious-binary MD5s, exfiltration address, Solana RPC endpoints, targeted file list, root cause. | yes |
| 2 | T1 | GitHub issue #300 on ovh/ovhcloud-cli (reporter alink-tothepast, 2026-10-19) | First-hand capture: replaced asset names and times, original and swapped SHA-256 of the v0.15.0 Linux arm64 archive, go version -m build metadata. | yes |
| 3 | T2 | OpenSourceMalware: OVHcloud CLI supply chain compromise (2026-10-20) | Server-side timeline, what was replaced, how to check a binary, account-compromise evidence. Did not analyze the payload. GlassWorm link is the authors' inference. | yes, attribution only as unconfirmed |
| 4 | T2 | The Hacker News: Hackers Hijack Five OVHcloud CLI Releases (2026-10-20) | Independent check of exposure paths. Says the Homebrew cask pinned original hashes so brew installs would have failed, and warns that a +dirty or vcs flag alone also appears on OVHcloud's clean v0.16.0. | yes |
| 5 | T2 | Help Net Security: Info-stealing malware slipped into OVHcloud CLI releases (2026-10-21) | Confirms versions, times, quotes from the OVHcloud CEO, immutability root cause. | corroboration only |
| x | n/a | BleepingComputer article on the incident | Fetch returned HTTP 403. Not read, not cited, nothing taken from it. | no |
Decisions
- Hash roles corrected. The brief that started this pack described 5c2beb56…aeb80e0 as a partial malicious hash. Source 2 gives it in full and labels it the original (legitimate) v0.15.0 Linux arm64 archive. The swapped (malicious) archive is the 3833daff value. The legitimate hash is kept out of the IOC import.
- Related-cluster indicators excluded. The advisory root-cause section and the OpenSourceMalware report list a wallet, a loader file hash, a key and IV, an address and an extension from the account's earlier compromise. They belong to a different incident and are not shipped. The wallet in the advisory's own IOC list (the binary's wallet) is shipped as a hunt string.
- Time zones. OVHcloud's 14:47 to 16:21 is Paris time (UTC+2), equal to 12:47 to 14:21 UTC. OpenSourceMalware says 94 minutes, the advisory says 1h33.
- Version detail. The reporter's build info is from the Linux arm64 binary and the advisory's from a Linux x86_64 build. Both show the same pseudo-version and commit, so those two strings are the signal. The +dirty flag is not, because the clean v0.16.0 build also reports it [#4].
Hunt Brief & Attack Chain
Seven hypotheses, ordered by fidelity.
- H1 (high): a host executed one of the five replaced binaries. Event: process start with MD5/SHA-256 (Q1, Q7). T1195.002. FP: none.
- H2 (high): a host connected to the advisory's upload address (Q2). T1041. FP: none, but the address may be changed by the operator.
- H3 (high): an ovhcloud process resolved a Solana RPC hostname (Q3, Q4). T1102. FP: blockchain tooling for Q4.
- H4 (medium): ovhcloud spawned shells, archivers or credential-store CLIs (Q5). T1119, T1552.001. FP: helper launches.
- H5 (medium): the CLI was fetched during 12:47 to 14:21 UTC on 2026-10-19 (Q8). T1195.002. FP: normal installs outside the window.
- H6 (low): a command line names an envchecker temp folder (Q6). T1119. Rarely visible, use N1.
- H7 (low): ovhcloud reached an unexpected public address (Q9). T1041. FP: OVHcloud API addresses.
Attack chain
| Step | Behavior | Telemetry | Hunt angle | Queries |
|---|---|---|---|---|
| 1 | Attacker holds a contributor's GitHub credentials (account compromised months earlier per OVHcloud and OpenSourceMalware) | GitHub audit log (outside endpoint telemetry) | Org audit log review | N4 |
| 2 | Malicious binary built outside CI and uploaded over five existing releases via the Releases API | GitHub release records and server-side activity (outside endpoint telemetry) | Compare asset uploader and times; verify attestation | N4, N5 |
| 3 | Victim installs or updates the CLI: install.sh, direct download, CI fetch, self-upgrade | Process creation with command line, image path and parent (CrowdStrike: ProcessRollup2. SentinelOne: Process Creation. Defender: DeviceProcessEvents) | Q8, Q7 | Q8, Q7 |
| 4 | Swapped binary runs; stealer code collects IDE databases, cloud and SSH credentials into a temp folder | Process creation with hashes (CrowdStrike: ProcessRollup2, MD5HashData and SHA256HashData. SentinelOne: Process Creation. Defender: DeviceProcessEvents); file reads are not logged | Q1, Q5, Q6, N1 | Q1, Q5, Q6 |
| 5 | Stealer resolves a Solana RPC endpoint and uploads the archive over HTTPS | DNS lookup with requesting process (CrowdStrike: DnsRequest. SentinelOne: DNS Resolved. Defender: DeviceEvents DnsQueryResponse) and outbound network connection (CrowdStrike: NetworkConnectIP4. SentinelOne: IP Connect. Defender: DeviceNetworkEvents) | Q2, Q3, Q4, Q9 | Q2, Q3, Q4, Q9 |
Not telemetry-visible: steps 1 and 2 happen in GitHub, and the stealer's reads of credential files and its creation of the temp folder are in-process Go file operations that the sensor does not log. They are flagged as gaps in Section 15.
Consolidated IOC Table
Hashes of OVHcloud's legitimate builds are listed for reference only and must never be blocked. Solana RPC hostnames are public services shared with legitimate users: hunt, never block.
| Type | Value | Confidence | Action | Context |
|---|---|---|---|---|
| MD5 | 6f2f086239f3d88eb2750e871901927d | high | detect | Malicious ovhcloud (Darwin_arm64 binary) per OVHcloud advisory [#1]. Single source. |
| MD5 | 347e65eb7f2d78d1c6872690272a6cc3 | high | detect | Malicious ovhcloud (Darwin_x86_64 binary) per OVHcloud advisory [#1]. Single source. |
| MD5 | 01f57206d37c28cf711677f612052913 | high | detect | Malicious ovhcloud (Linux_arm64 binary) per OVHcloud advisory [#1]. Single source. |
| MD5 | 301bff0b51980b9e76c93633e7cfd3bd | high | detect | Malicious ovhcloud (Linux_x86_64 binary) per OVHcloud advisory [#1]. Single source. |
| MD5 | 6a3cf153338c416169cda2fe704c8848 | high | detect | Malicious ovhcloud.exe (Windows x86_64 binary) per OVHcloud advisory [#1]. Single source. |
| SHA-256 | 3833daffcaaaab27e4e0e0833240ffcb1df8df648f4ec0bc4a75c176e28933cb | high | prevent | Swapped v0.15.0 ovhcloud-cli_Linux_arm64.tar.gz [#2] [#3] [#4]. Archive, so prevention has no execution effect; it flags the file. |
| SHA-256 | 5c2beb56e6d83a4b426565a1557791ef759316abcc4effb38dd627c90aeb80e0 | high | do not block | LEGITIMATE original v0.15.0 Linux arm64 archive [#2] [#3] [#4]. Known-good reference only. |
| SHA-256 | e9c31dca232cdaa8058cb59eadf425c182a23afb6ddb057eb92eda13b850dff4 | medium | detect | Sample linked by the advisory on VirusTotal [#1]. The advisory does not say whether it is a binary or an archive. Single source. |
| SHA-256 | 3c10bd782cd0dc5f720a33570af4651814cb82917e54d27ffce3849a3c5cef2a | medium | detect | Sample linked by the advisory on VirusTotal [#1]. The advisory does not say whether it is a binary or an archive. Single source. |
| SHA-256 | 3cbe1ac592beeca7ce0ad0ba945349a9ef33ef0f1324935c3a115f30b1d0c927 | medium | detect | Sample linked by the advisory on VirusTotal [#1]. The advisory does not say whether it is a binary or an archive. Single source. |
| SHA-256 | 25c68c5e9ae23883aebda59f4b6a7df78e1ce8aae5e4b4ba636419497d54bbec | medium | detect | Sample linked by the advisory on VirusTotal [#1]. The advisory does not say whether it is a binary or an archive. Single source. |
| SHA-256 | f97284530343b3903ffd19bbe70ab6f62e36f04eb7e5f885934bd21d10899e8a | medium | detect | Sample linked by the advisory on VirusTotal [#1]. The advisory does not say whether it is a binary or an archive. Single source. |
| IPv4 | 45.32.148.174 | high | detect | Stealer upload endpoint, TCP/443, path /upload [#1]. Advisory says it can change at any time. Single source. |
| Solana wallet | BHaDaVbdCYFNaKZGn7yLXFneWoJHZgLiBYkHxVrjyALF | high | hunt | Wallet used by the October binary for its RPC calls [#1]. Not importable to Falcon; search proxy and DNS-adjacent logs. |
| Domain | solana.drpc.org | low | hunt (do not block) | Public Solana RPC endpoint the stealer picks from [#1]. Legitimate shared service. |
| Domain | solana-rpc.publicnode.com | low | hunt (do not block) | Public Solana RPC endpoint the stealer picks from [#1]. Legitimate shared service. |
| Domain | solana.api.pocket.network | low | hunt (do not block) | Public Solana RPC endpoint the stealer picks from [#1]. Legitimate shared service. |
| Domain | api.mainnet-beta.solana.com | low | hunt (do not block) | Public Solana RPC endpoint the stealer picks from [#1]. Legitimate shared service. |
| Domain | solana.api.onfinality.io | low | hunt (do not block) | Public Solana RPC endpoint the stealer picks from [#1]. Legitimate shared service. |
| Folder | envchecker-[0-9]{9,10} in the temp directory | high | hunt | Stealer working folder, never deleted [#1]. Q6, N1. |
| Build string | v0.15.1-0.20261013083505-5ead8ef9b43d+dirty | high | hunt | Go module pseudo-version in the malicious build [#1] [#2] [#3]. A clean v0.15.0 reports v0.15.0. Do not key on +dirty alone [#4]. |
| Commit | 5ead8ef9b43d354dfb5caadff803b11afc8c5c58 | high | hunt | vcs.revision of the malicious build [#1] [#2]. The real v0.15.0 tag points to 760fd72de93cbea3b507eb36dc0095991e4048e5 [#2]. |
| Filename | ovhcloud / ovhcloud.exe | low | hunt | Name of the replaced binaries. Also the legitimate name, so name alone proves nothing. |
| GitHub account | HassenBF | medium | enrich | Account that uploaded the assets [#2] [#3]. Context for audit-log review. A victim identity, not an attacker handle. |
Affected Surface & Telemetry Matrix
| Surface | Telemetry | Priority | Gap risk |
|---|---|---|---|
| Developer workstations (macOS, Linux, Windows) that installed or upgraded the CLI | Process creation with command line, hashes and parent (CrowdStrike: ProcessRollup2. SentinelOne: Process Creation. Defender: DeviceProcessEvents) | High | Reads of credential files and the temp-folder creation are in-process and not logged, so a host that ran the binary shows only the execution |
| CI runners and build containers that fetch the release tarball | Process creation with command line (as above); outbound connection (CrowdStrike: NetworkConnectIP4. SentinelOne: IP Connect. Defender: DeviceNetworkEvents) | High | Ephemeral runners may have no sensor; use build logs (N2) |
| Network egress to the upload address and Solana RPC endpoints | DNS lookup with requesting process (CrowdStrike: DnsRequest. SentinelOne: DNS Resolved. Defender: DeviceEvents) | Medium | Defender has no endpoint DNS table; use proxy and resolver logs (N3) |
| GitHub organisation release and membership activity | GitHub audit log | Medium | Outside endpoint telemetry |
| Secrets the CLI could read (cloud, SSH, git, IDE, AI-assistant data) | Identity provider and cloud audit logs after rotation | High | Outside endpoint telemetry; watch for use of rotated or old keys |
ATT&CK Mapping
| Tactic | Technique | Observed behavior | Query or control |
|---|---|---|---|
| Initial Access | T1195.002 Compromise Software Supply Chain | Release assets replaced on legitimate releases | Q1, Q7, Q8; Hardening T1 |
| Initial Access | T1078 Valid Accounts | Hijacked contributor GitHub account used to upload | N4; Hardening T2 |
| Execution | T1204.002 User Execution | Victim runs the trojanised CLI | Q1, Q7 |
| Defense Evasion | T1036 Masquerading | Binary carries the legitimate name and release version | Q7, N5 |
| Collection | T1119 Automated Collection, T1005 Data from Local System | Copies IDE, AI-assistant, cloud, SSH and git data into the temp folder | Q5, Q6, N1 |
| Credential Access | T1552.001 Credentials In Files, T1552.004 Private Keys | Reads ~/.aws, ~/.kube, ~/.ssh, ~/.netrc, Terraform and cloud CLI files | Hardening T4 |
| Collection | T1560 Archive Collected Data | Archives the working folder before upload | Q5, Q6 |
| Command and Control | T1102 Web Service | Solana RPC endpoints used, chosen at random | Q3, Q4 |
| Exfiltration | T1041 Exfiltration Over C2 Channel | Archive uploaded to a hard-coded address over HTTPS | Q2, Q9 |
Native Audit-Log Hunts
These checks need no Falcon query.
N1 · Endpoint triage: stealer working folder
Where: Developer workstation or runner
# macOS / Linux (read-only)
ls -ld "${TMPDIR:-/tmp}"/envchecker-* 2>/dev/null
# Windows PowerShell (read-only)
Get-ChildItem -Path $env:TEMP -Directory -Filter "envchecker-*" -ErrorAction SilentlyContinueThe advisory says the folder is created on every run and never deleted. A match means the swapped binary ran on this machine. Absence is not proof: the tool may have run before the folder was cleaned.
N2 · CI and build logs: release downloads in the window
Where: CI system, container build logs, package mirror logs
# Search for the window 2026-10-19 12:47 to 14:21 UTC (14:47 to 16:21 Paris) # Strings: ovhcloud-cli, releases/download, install.sh, ovhcloud-cli_Linux_x86_64.tar.gz # Any job that fetched an archive without a pinned hash in that window is exposed
Pinned-hash jobs failed and are safe. Unpinned jobs, Dockerfiles and the CLI's own upgrade command pulled the swapped file.
N3 · Proxy, firewall and resolver logs
Where: Egress logging
# Destination 45.32.148.174 port 443 (path /upload) # DNS or SNI: solana.drpc.org, solana-rpc.publicnode.com, solana.api.pocket.network, # api.mainnet-beta.solana.com, solana.api.onfinality.io # Hits from build hosts with no blockchain role are suspicious
Covers hosts and runners with no sensor.
N4 · GitHub organisation audit log
Where: GitHub org settings, audit log
# Review release asset delete and upload events on every repo, by user # Flag assets changed on releases older than 24 hours # List members with Write on release-publishing repos; check for personal accounts # Confirm immutable releases is enabled org-wide
This is how the swap could be spotted at the source. Event names vary by plan; check your own audit log schema.
N5 · Check a binary without running it
Where: Any host that kept a copy
go version -m ./ovhcloud # Malicious: mod ... v0.15.1-0.20261013083505-5ead8ef9b43d+dirty and vcs.revision=5ead8ef9b43d354dfb5caadff803b11afc8c5c58 # Do NOT treat vcs.modified=true alone as malicious: the clean v0.16.0 build also reports it md5sum ./ovhcloud # compare with the five MD5 values in Section 10 shasum -a 256 ovhcloud-cli_*.tar.gz
Never run the binary to check it.
Hunt Queries: CrowdStrike CQL, SentinelOne PowerQuery and Defender XDR KQL
Use your tenant's own address. SentinelOne does not document a link that pre-fills a query, so each "Copy & open SentinelOne" button copies the query and opens this console: paste it into PowerQuery.
Pick your portal. Microsoft documents a Share link in advanced hunting but not its URL format, so each "Copy & open Defender" button copies the query and opens Advanced hunting: paste it into the editor.
All nine queries run on Falcon sensor telemetry and work on Windows, macOS and Linux hosts. For Q1, Q7 and Q8 set the time picker to cover 2026-10-19 onward, because the exposure window is a single afternoon and Q7 should also show what is installed now. Q3 relies on process context on DNS events; check it in your tenant.
Looks for: any process start or written executable whose MD5 matches the five malicious binaries listed in the OVHcloud advisory, or whose SHA-256 matches a malicious sample the reporting names (the swapped v0.15.0 Linux arm64 archive and the five advisory sample links). FP: none expected, these are hashes of the replaced files. The known-good original archive hash is deliberately not in this query.
Known limitations and deployment notes
- PowerQuery: A file event the sensor did not hash is missed; the hash fields can be empty for large or in-flight files.
- PowerQuery assumes: The process and file hash fields are populated on this tenant's events.
- PowerQuery assumes: File Creation events stand in for the sensor's PE-written and new-executable-written events and also cover non-PE files.
- KQL – Defender XDR: Files written to disk but never executed are not covered; run the same hash lists against DeviceFileEvents.
- KQL – Defender XDR assumes: MD5 is populated in DeviceProcessEvents; SHA256 is often empty, so MD5 carries the match on most tenants.
// HUNT: OVHcloud CLI replaced release binaries by hash // MITRE: T1195.002, T1204.002 // CONF: high // FP: low // COST: low // REQUIRES: Falcon sensor process and Windows file-write telemetry with MD5HashData/SHA256HashData // FALSE POSITIVES: none expected, hashes belong to the replaced files // LOOKBACK: 7d #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2|PeFileWritten|NewExecutableWritten)$/ | (MD5HashData=/^(6f2f086239f3d88eb2750e871901927d|347e65eb7f2d78d1c6872690272a6cc3|01f57206d37c28cf711677f612052913|301bff0b51980b9e76c93633e7cfd3bd|6a3cf153338c416169cda2fe704c8848)$/i or SHA256HashData=/^(e9c31dca232cdaa8058cb59eadf425c182a23afb6ddb057eb92eda13b850dff4|3c10bd782cd0dc5f720a33570af4651814cb82917e54d27ffce3849a3c5cef2a|3cbe1ac592beeca7ce0ad0ba945349a9ef33ef0f1324935c3a115f30b1d0c927|25c68c5e9ae23883aebda59f4b6a7df78e1ce8aae5e4b4ba636419497d54bbec|f97284530343b3903ffd19bbe70ab6f62e36f04eb7e5f885934bd21d10899e8a|3833daffcaaaab27e4e0e0833240ffcb1df8df648f4ec0bc4a75c176e28933cb)$/i) | table([@timestamp, #event_simpleName, aid, ComputerName, UserName, FileName, ImageFileName, MD5HashData, SHA256HashData, ParentBaseFileName, CommandLine]) | sort(@timestamp, order=desc)
5 required fields · View requirements
Required telemetry
- Process Creation
- File Creation
Required fields
event.type: Event typetgt.process.image.md5: Created process MD5tgt.process.image.sha256: Created process SHA256tgt.file.md5: Written file MD5tgt.file.sha256: Written file SHA256
Recommended fields
endpoint.name: Host
Readiness test
event.type in ('Process Creation', 'File Creation') and (tgt.process.image.sha256 = * or tgt.file.sha256 = *)
| limit 5
| columns event.type, tgt.process.image.md5, tgt.process.image.sha256, tgt.file.md5, tgt.file.sha256// HUNT: OVHcloud CLI replaced release binaries by hash
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1195.002, T1204.002
// CONF: high | FP: low
// FALSE POSITIVES: none expected, hashes belong to the replaced files
// EQUIVALENCE: equivalent-with-schema-assumptions - Same hash pivot over the process-start and file-write populations: the MD5 and SHA256 lists are matched on Process Creation and File Creation events. in:anycase reproduces the case-insensitive intent. The CQL's four sensor event names collapse to two PowerQuery event types.
// REQUIRES: SentinelOne process and file telemetry with hash fields populated
// TUNING: None expected; extend the hash lists if the advisory adds more.
// LOOKBACK: 7d
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: A file event the sensor did not hash is missed; the hash fields can be empty for large or in-flight files.
// VALIDATION LEVEL: static-reviewed
event.type in ('Process Creation', 'File Creation') and (tgt.process.image.md5 in:anycase ('6f2f086239f3d88eb2750e871901927d', '347e65eb7f2d78d1c6872690272a6cc3', '01f57206d37c28cf711677f612052913', '301bff0b51980b9e76c93633e7cfd3bd', '6a3cf153338c416169cda2fe704c8848') or tgt.process.image.sha256 in:anycase ('e9c31dca232cdaa8058cb59eadf425c182a23afb6ddb057eb92eda13b850dff4', '3c10bd782cd0dc5f720a33570af4651814cb82917e54d27ffce3849a3c5cef2a', '3cbe1ac592beeca7ce0ad0ba945349a9ef33ef0f1324935c3a115f30b1d0c927', '25c68c5e9ae23883aebda59f4b6a7df78e1ce8aae5e4b4ba636419497d54bbec', 'f97284530343b3903ffd19bbe70ab6f62e36f04eb7e5f885934bd21d10899e8a', '3833daffcaaaab27e4e0e0833240ffcb1df8df648f4ec0bc4a75c176e28933cb') or tgt.file.md5 in:anycase ('6f2f086239f3d88eb2750e871901927d', '347e65eb7f2d78d1c6872690272a6cc3', '01f57206d37c28cf711677f612052913', '301bff0b51980b9e76c93633e7cfd3bd', '6a3cf153338c416169cda2fe704c8848') or tgt.file.sha256 in:anycase ('e9c31dca232cdaa8058cb59eadf425c182a23afb6ddb057eb92eda13b850dff4', '3c10bd782cd0dc5f720a33570af4651814cb82917e54d27ffce3849a3c5cef2a', '3cbe1ac592beeca7ce0ad0ba945349a9ef33ef0f1324935c3a115f30b1d0c927', '25c68c5e9ae23883aebda59f4b6a7df78e1ce8aae5e4b4ba636419497d54bbec', 'f97284530343b3903ffd19bbe70ab6f62e36f04eb7e5f885934bd21d10899e8a', '3833daffcaaaab27e4e0e0833240ffcb1df8df648f4ec0bc4a75c176e28933cb'))
| columns event.time, endpoint.name, src.process.user, event.type, tgt.process.name, tgt.process.image.path, tgt.process.image.md5, tgt.process.image.sha256, tgt.file.path, tgt.file.md5, tgt.file.sha256, src.process.name, tgt.process.cmdline
| sort -event.time
| limit 200Automated validation: static-passed
3 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeMD5: Created process MD5SHA256: Created process SHA256
Recommended fields
FileName: Created process name
Optional fields
SHA1: Hash. SHA256 is often unpopulated in DeviceProcessEvents; the lists here are MD5 and SHA256 only.
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, FileName, MD5, SHA256 | take 5
// HUNT: OVHcloud CLI replaced release binaries by hash
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1195.002, T1204.002
// CONF: high | FP: low
// FALSE POSITIVES: none expected, hashes belong to the replaced files
// EQUIVALENCE: telemetry-gap - Partial pivot: the same MD5 and SHA256 lists are matched on process-creation rows only. The file-write half of the CQL is not covered because the card allows one table.
// REQUIRES: Defender for Endpoint process-creation telemetry (DeviceProcessEvents)
// TUNING: None expected; extend the hash lists if the advisory adds more.
// LOOKBACK: 7d
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: Files written to disk but never executed are not covered; run the same hash lists against DeviceFileEvents.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0
DeviceProcessEvents
| where Timestamp >= ago(7d)
| where MD5 in~ ("6f2f086239f3d88eb2750e871901927d", "347e65eb7f2d78d1c6872690272a6cc3", "01f57206d37c28cf711677f612052913", "301bff0b51980b9e76c93633e7cfd3bd", "6a3cf153338c416169cda2fe704c8848") or SHA256 in~ ("e9c31dca232cdaa8058cb59eadf425c182a23afb6ddb057eb92eda13b850dff4", "3c10bd782cd0dc5f720a33570af4651814cb82917e54d27ffce3849a3c5cef2a", "3cbe1ac592beeca7ce0ad0ba945349a9ef33ef0f1324935c3a115f30b1d0c927", "25c68c5e9ae23883aebda59f4b6a7df78e1ce8aae5e4b4ba636419497d54bbec", "f97284530343b3903ffd19bbe70ab6f62e36f04eb7e5f885934bd21d10899e8a", "3833daffcaaaab27e4e0e0833240ffcb1df8df648f4ec0bc4a75c176e28933cb")
| project Timestamp, ReportId, DeviceId, DeviceName, AccountName, FileName, FolderPath, MD5, SHA256, InitiatingProcessFileName, ProcessCommandLine
| sort by Timestamp desc
| take 200Looks for: an outbound connection to the address the advisory names as the upload endpoint (TCP/443, path /upload). The advisory warns the URL is time-sensitive and the operator can change it, so a miss here does not clear a host. FP: none expected.
Known limitations and deployment notes
- PowerQuery: Connections made before the sensor was installed, or by a host without network telemetry, are not captured.
- PowerQuery assumes: src.process.name names the connecting process on IP Connect events.
- KQL – Defender XDR: Connections the sensor did not record are not captured.
- KQL – Defender XDR assumes: RemoteIP holds the destination address for outbound connections.
// HUNT: Outbound connection to the OVHcloud CLI stealer upload address // MITRE: T1041 // CONF: high // FP: low // COST: low // REQUIRES: Falcon sensor network telemetry // FALSE POSITIVES: none expected // LOOKBACK: 7d #event_simpleName=NetworkConnectIP4 | RemoteAddressIP4="45.32.148.174" | table([@timestamp, aid, ComputerName, ContextBaseFileName, RemoteAddressIP4, RemotePort, LocalAddressIP4]) | sort(@timestamp, order=desc)
2 required fields · View requirements
Required telemetry
- IP Connect
Required fields
event.type: Event typedst.ip.address: Destination address
Recommended fields
src.process.name: Connecting processendpoint.name: Host
Readiness test
event.type = 'IP Connect' and dst.ip.address = * | limit 5 | columns event.type, dst.ip.address
// HUNT: Outbound connection to the OVHcloud CLI stealer upload address // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1041 // CONF: high | FP: low // FALSE POSITIVES: none expected // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: an outbound IP connection whose destination is the stealer upload address. The connecting process is src.process.name on IP Connect events. // REQUIRES: SentinelOne network (IP Connect) telemetry // TUNING: None expected. // LOOKBACK: 7d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: Connections made before the sensor was installed, or by a host without network telemetry, are not captured. // VALIDATION LEVEL: static-reviewed event.type = 'IP Connect' and dst.ip.address = '45.32.148.174' | columns event.time, endpoint.name, agent.uuid, src.process.name, src.ip.address, dst.ip.address, dst.port.number | sort -event.time | limit 200
Automated validation: static-passed
2 required fields · View requirements
Required telemetry
- DeviceNetworkEvents
Required fields
Timestamp: Event timeRemoteIP: Remote address
Recommended fields
InitiatingProcessFileName: Connecting process
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceNetworkEvents | where Timestamp > ago(1d) | project Timestamp, RemoteIP | take 5
// HUNT: Outbound connection to the OVHcloud CLI stealer upload address // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1041 // CONF: high | FP: low // FALSE POSITIVES: none expected // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: a network event whose remote address is the stealer upload address. The connecting process is InitiatingProcessFileName. // REQUIRES: Defender for Endpoint network telemetry (DeviceNetworkEvents) // TUNING: None expected. // LOOKBACK: 7d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: Connections the sensor did not record are not captured. // VALIDATION LEVEL: schema-aligned // AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0 DeviceNetworkEvents | where Timestamp >= ago(7d) | where RemoteIP == "45.32.148.174" | project Timestamp, ReportId, DeviceId, DeviceName, InitiatingProcessFileName, RemoteIP, RemotePort, LocalIP | sort by Timestamp desc | take 200
Looks for: a DNS lookup of one of the four Solana RPC hostnames from a process named ovhcloud. The advisory says the stealer picks one of its listed endpoints at random on each run. A cloud-management CLI has no reason to look these up. FP: none expected. ⚠ validate in tenant: if ContextBaseFileName is empty on DnsRequest events for your sensor version, rely on Q4. The advisory writes its fifth endpoint with a path (/public); only the hostname part can appear in DNS, and it is included.
Known limitations and deployment notes
- PowerQuery: A resolution made through a proxy or a DNS-over-HTTPS client is not recorded as a DNS Resolved event.
- PowerQuery assumes: event.dns.request is the populated DNS name field on this tenant; some schema versions use dns.request.
- PowerQuery assumes: src.process.name names the resolving process on DNS Resolved events.
- PowerQuery assumes: The hyphenated hostnames are written with a [-] class; the literal check reads their tails rpc.publicnode.com and beta.solana.com as separate names, so they are listed here.
- KQL – Defender XDR: Tenants that do not record DnsQueryResponse return nothing here, which reads as a clean estate.
- KQL – Defender XDR assumes: DnsQueryResponse is populated on this tenant and AdditionalFields carries DnsQueryString or QueryName.
- KQL – Defender XDR assumes: InitiatingProcessFileName is the resolving process.
- KQL – Defender XDR assumes: The hyphenated hostnames are written with a [-] class; the literal check reads their tails rpc.publicnode.com and beta.solana.com as separate names, so they are listed here.
// HUNT: ovhcloud process resolving Solana RPC hostnames // MITRE: T1102, T1041 // CONF: high // FP: low // COST: low // REQUIRES: Falcon sensor DNS telemetry with process context (⚠ validate in tenant: ContextBaseFileName on DnsRequest) // FALSE POSITIVES: none expected // LOOKBACK: 7d #event_simpleName=DnsRequest | ContextBaseFileName=/^ovhcloud(\.exe)?$/i | DomainName=/^(solana\.drpc\.org|solana\-rpc\.publicnode\.com|solana\.api\.pocket\.network|api\.mainnet\-beta\.solana\.com|solana\.api\.onfinality\.io)$/i | table([@timestamp, aid, ComputerName, ContextBaseFileName, DomainName]) | sort(@timestamp, order=desc)
3 required fields · View requirements
Required telemetry
- DNS Resolved
Required fields
event.type: Event typesrc.process.name: Resolving processevent.dns.request: DNS name
Recommended fields
endpoint.name: Host
Fallbacks
event.dns.request→dns.request: Different schema versions populate one or the other.
Readiness test
event.type = 'DNS Resolved' and event.dns.request = * | limit 5 | columns event.type, src.process.name, event.dns.request
// HUNT: ovhcloud process resolving Solana RPC hostnames // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1102, T1041 // CONF: high | FP: low // FALSE POSITIVES: none expected // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: a DNS resolution of one of the five Solana RPC hostnames by a process named ovhcloud. The resolving process is src.process.name on DNS events, and matches is case-insensitive, which reproduces the CQL intent. // REQUIRES: SentinelOne DNS telemetry with process context // TUNING: None expected. // LOOKBACK: 7d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: A resolution made through a proxy or a DNS-over-HTTPS client is not recorded as a DNS Resolved event. // VALIDATION LEVEL: static-reviewed event.type = 'DNS Resolved' and src.process.name matches '^ovhcloud(\\.exe)?$' and event.dns.request matches '^(solana\\.drpc\\.org|solana[-]rpc\\.publicnode\\.com|solana\\.api\\.pocket\\.network|api\\.mainnet[-]beta\\.solana\\.com|solana\\.api\\.onfinality\\.io)$' | columns event.time, endpoint.name, agent.uuid, src.process.name, event.dns.request | sort -event.time | limit 200
Automated validation: static-passed
4 required fields · View requirements
Required telemetry
- DeviceEvents
Required fields
Timestamp: Event timeActionType: Action typeAdditionalFields: DNS query detailInitiatingProcessFileName: Resolving process
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceEvents | where Timestamp > ago(1d) | where ActionType == "DnsQueryResponse" | project Timestamp, ActionType, AdditionalFields, InitiatingProcessFileName | take 5
// HUNT: ovhcloud process resolving Solana RPC hostnames
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1102, T1041
// CONF: high | FP: low
// FALSE POSITIVES: none expected
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate on the DNS query action in DeviceEvents: the queried name is one of the five hostnames and the initiating process is ovhcloud. The name is read from AdditionalFields because there is no endpoint DNS table.
// REQUIRES: Defender for Endpoint DNS query events (DeviceEvents, ActionType DnsQueryResponse)
// TUNING: None expected.
// LOOKBACK: 7d
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: Tenants that do not record DnsQueryResponse return nothing here, which reads as a clean estate.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0
DeviceEvents
| where Timestamp >= ago(7d)
| where ActionType == "DnsQueryResponse"
| where InitiatingProcessFileName in~ ("ovhcloud", "ovhcloud.exe")
| extend QueryName = tostring(coalesce(parse_json(AdditionalFields)["DnsQueryString"], parse_json(AdditionalFields)["QueryName"]))
| where QueryName matches regex @"(?i)^(solana\.drpc\.org|solana[-]rpc\.publicnode\.com|solana\.api\.pocket\.network|api\.mainnet[-]beta\.solana\.com|solana\.api\.onfinality\.io)$"
| project Timestamp, ReportId, DeviceId, DeviceName, InitiatingProcessFileName, QueryName
| sort by Timestamp desc
| take 200Looks for: the same five Solana RPC hostnames resolved by any process, grouped by host and process. Use it where Q3 is blind and to find the stealer if it was renamed. FP: blockchain developers, wallets and node operators resolve these names legitimately. These are public services and are intentionally not in the IOC import.
Known limitations and deployment notes
- PowerQuery: A resolution made through a proxy or a DNS-over-HTTPS client is not recorded as a DNS Resolved event.
- PowerQuery assumes: event.dns.request is the populated DNS name field on this tenant; some schema versions use dns.request.
- PowerQuery assumes: src.process.name names the resolving process on DNS Resolved events.
- PowerQuery assumes: The hyphenated hostnames are written with a [-] class; the literal check reads their tails rpc.publicnode.com and beta.solana.com as separate names, so they are listed here.
- KQL – Defender XDR: Tenants that do not record DnsQueryResponse return nothing here, which reads as a clean estate.
- KQL – Defender XDR assumes: DnsQueryResponse is populated on this tenant and AdditionalFields carries DnsQueryString or QueryName.
- KQL – Defender XDR assumes: The hyphenated hostnames are written with a [-] class; the literal check reads their tails rpc.publicnode.com and beta.solana.com as separate names, so they are listed here.
// HUNT: Solana RPC hostname lookups by any process // MITRE: T1102 // CONF: medium // FP: medium // COST: low // REQUIRES: Falcon sensor DNS telemetry // FALSE POSITIVES: crypto wallets, blockchain developer tooling, Solana node or explorer apps // TUNING: exclude known wallet/dev hosts with a lookup table of ComputerName or aid; alert only on hosts with no blockchain role (build runners, servers, CI agents) // LOOKBACK: 7d #event_simpleName=DnsRequest | DomainName=/^(solana\.drpc\.org|solana\-rpc\.publicnode\.com|solana\.api\.pocket\.network|api\.mainnet\-beta\.solana\.com|solana\.api\.onfinality\.io)$/i | groupBy([ComputerName, aid, ContextBaseFileName, DomainName], function=[count(as=lookups), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)], limit=1000) | sort(lookups, order=desc) | table([ComputerName, aid, ContextBaseFileName, DomainName, lookups, first_seen, last_seen])
4 required fields · View requirements
Required telemetry
- DNS Resolved
Required fields
event.type: Event typeevent.dns.request: DNS nameevent.time: Event timeendpoint.name: Host
Recommended fields
src.process.name: Resolving process
Optional fields
agent.uuid: Agent identifier
Fallbacks
event.dns.request→dns.request: Different schema versions populate one or the other.
Readiness test
event.type = 'DNS Resolved' and event.dns.request = * | limit 5 | columns event.time, endpoint.name, event.type, event.dns.request
// HUNT: Solana RPC hostname lookups by any process // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1102 // CONF: medium | FP: medium // FALSE POSITIVES: crypto wallets, blockchain developer tooling, Solana node or explorer apps // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate and aggregation: DNS lookups of the five Solana RPC hostnames by any process, counted per host, process and name with first and last seen. // REQUIRES: SentinelOne DNS telemetry // TUNING: Exclude known wallet and developer hosts with a lookup table of endpoint names; alert only on hosts with no blockchain role (build runners, servers, CI agents). // LOOKBACK: 7d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: A resolution made through a proxy or a DNS-over-HTTPS client is not recorded as a DNS Resolved event. // VALIDATION LEVEL: static-reviewed event.type = 'DNS Resolved' and event.dns.request matches '^(solana\\.drpc\\.org|solana[-]rpc\\.publicnode\\.com|solana\\.api\\.pocket\\.network|api\\.mainnet[-]beta\\.solana\\.com|solana\\.api\\.onfinality\\.io)$' | group lookups=count(), first_seen=min(event.time), last_seen=max(event.time) by endpoint.name, agent.uuid, src.process.name, event.dns.request | sort -lookups | columns endpoint.name, agent.uuid, src.process.name, event.dns.request, lookups, first_seen, last_seen | limit 1000
Automated validation: static-passed
4 required fields · View requirements
Required telemetry
- DeviceEvents
Required fields
Timestamp: Event timeActionType: Action typeAdditionalFields: DNS query detailDeviceName: Host
Recommended fields
InitiatingProcessFileName: Resolving process
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceEvents | where Timestamp > ago(1d) | where ActionType == "DnsQueryResponse" | project Timestamp, DeviceName, ActionType, AdditionalFields | take 5
// HUNT: Solana RPC hostname lookups by any process // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1102 // CONF: medium | FP: medium // FALSE POSITIVES: crypto wallets, blockchain developer tooling, Solana node or explorer apps // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate and aggregation on the DNS query action in DeviceEvents, with the name read from AdditionalFields because there is no endpoint DNS table. // REQUIRES: Defender for Endpoint DNS query events (DeviceEvents, ActionType DnsQueryResponse) // TUNING: Exclude known wallet and developer hosts by DeviceName; alert only on hosts with no blockchain role. // LOOKBACK: 7d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: Tenants that do not record DnsQueryResponse return nothing here, which reads as a clean estate. // VALIDATION LEVEL: schema-aligned // AUTOMATED VALIDATION: static-passed; static=passed-with-warnings; fixtures=0/0 DeviceEvents | where Timestamp >= ago(7d) | where ActionType == "DnsQueryResponse" | extend QueryName = tostring(coalesce(parse_json(AdditionalFields)["DnsQueryString"], parse_json(AdditionalFields)["QueryName"])) | where QueryName matches regex @"(?i)^(solana\.drpc\.org|solana[-]rpc\.publicnode\.com|solana\.api\.pocket\.network|api\.mainnet[-]beta\.solana\.com|solana\.api\.onfinality\.io)$" | summarize lookups = count(), first_seen = min(Timestamp), last_seen = max(Timestamp) by DeviceName, DeviceId, InitiatingProcessFileName, QueryName | sort by lookups desc | take 1000
Looks for: a process named ovhcloud launching a shell, archiver or a cloud, container or secret-store CLI. The advisory says the stealer prepares credential files by running various commands, but does not list them, so the child list is an informed guess. FP: legitimate subcommands may launch helper programs; baseline first.
Known limitations and deployment notes
- PowerQuery: A child launched through a process that is not named ovhcloud, such as a renamed copy, is not captured.
- PowerQuery assumes: src.process.user is the user of the creating process, which matches the CQL's user on a single-user launch.
- KQL – Defender XDR: A child launched through a process that is not named ovhcloud is not captured.
- KQL – Defender XDR assumes: The regexes are anchored on the file name only, so a renamed copy of the CLI is not matched.
// HUNT: ovhcloud spawning shell or credential-collection children // MITRE: T1119, T1552.001, T1059 // CONF: medium // FP: medium // COST: low // REQUIRES: Falcon sensor process telemetry with parent process // FALSE POSITIVES: CLI subcommands that shell out (ssh helpers, browser launchers), wrapper scripts // TUNING: exclude parent command lines of sanctioned wrapper scripts after a 14-day baseline; keep the shell and archiver children // LOOKBACK: 7d #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | ParentBaseFileName=/^ovhcloud(\.exe)?$/i | FileName=/^(sh|bash|zsh|dash|cmd|powershell|pwsh|security|aws|gcloud|az|doctl|kubectl|docker|git|npm|terraform|tar|zip|curl|wget|sqlite3|osascript|7z|7za)(\.exe)?$/i | table([@timestamp, aid, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine]) | sort(@timestamp, order=desc)
3 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.type: Event typesrc.process.name: Creator processtgt.process.name: Created process
Recommended fields
tgt.process.cmdline: Created command lineendpoint.name: Host
Optional fields
src.process.user: Creator user
Readiness test
event.type = 'Process Creation' and tgt.process.name = * | limit 5 | columns event.type, src.process.name, tgt.process.name
// HUNT: ovhcloud spawning shell or credential-collection children // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1119, T1552.001, T1059 // CONF: medium | FP: medium // FALSE POSITIVES: CLI subcommands that shell out (ssh helpers, browser launchers), wrapper scripts // EQUIVALENCE: equivalent-with-schema-assumptions - Same lineage predicate: a process named ovhcloud is the creator and the created process is a shell, archiver, cloud CLI or credential tool. On Process Creation the creator is src.process.name and the new process is tgt.process.name. matches is case-insensitive, which reproduces the CQL intent. // REQUIRES: SentinelOne process-creation telemetry with creator process // TUNING: Exclude creator command lines of sanctioned wrapper scripts after a 14-day baseline; keep the shell and archiver children. // LOOKBACK: 7d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: A child launched through a process that is not named ovhcloud, such as a renamed copy, is not captured. // VALIDATION LEVEL: static-reviewed event.type = 'Process Creation' and src.process.name matches '^ovhcloud(\\.exe)?$' and tgt.process.name matches '^(sh|bash|zsh|dash|cmd|powershell|pwsh|security|aws|gcloud|az|doctl|kubectl|docker|git|npm|terraform|tar|zip|curl|wget|sqlite3|osascript|7z|7za)(\\.exe)?$' | columns event.time, endpoint.name, agent.uuid, src.process.user, src.process.name, tgt.process.name, tgt.process.cmdline | sort -event.time | limit 200
Automated validation: static-passed
3 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeInitiatingProcessFileName: Creator processFileName: Created process
Recommended fields
ProcessCommandLine: Created command line
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, InitiatingProcessFileName, FileName | take 5
// HUNT: ovhcloud spawning shell or credential-collection children // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1119, T1552.001, T1059 // CONF: medium | FP: medium // FALSE POSITIVES: CLI subcommands that shell out (ssh helpers, browser launchers), wrapper scripts // EQUIVALENCE: equivalent-with-schema-assumptions - Same lineage predicate: the initiating process is named ovhcloud and the created process is a shell, archiver, cloud CLI or credential tool. The CQL's parent field is InitiatingProcessFileName. // REQUIRES: Defender for Endpoint process-creation telemetry (DeviceProcessEvents) // TUNING: Exclude initiating command lines of sanctioned wrapper scripts after a 14-day baseline; keep the shell and archiver children. // LOOKBACK: 7d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: A child launched through a process that is not named ovhcloud is not captured. // VALIDATION LEVEL: schema-aligned // AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0 DeviceProcessEvents | where Timestamp >= ago(7d) | where InitiatingProcessFileName matches regex @"(?i)^ovhcloud(\.exe)?$" | where FileName matches regex @"(?i)^(sh|bash|zsh|dash|cmd|powershell|pwsh|security|aws|gcloud|az|doctl|kubectl|docker|git|npm|terraform|tar|zip|curl|wget|sqlite3|osascript|7z|7za)(\.exe)?$" | project Timestamp, ReportId, DeviceId, DeviceName, AccountName, InitiatingProcessFileName, InitiatingProcessCommandLine, FileName, ProcessCommandLine | sort by Timestamp desc | take 200
Looks for: any process command line containing the temporary working folder name the advisory documents (envchecker followed by 9 or 10 digits). The stealer writes this folder from inside the Go process, so endpoint process telemetry is unlikely to see its creation. This query only catches follow-on tooling or an analyst command. The native check in Section 7 (N1) is the primary test. FP: none expected.
Known limitations and deployment notes
- PowerQuery: The folder name is only seen when it appears on a created process's command line; file activity inside it is not covered.
- PowerQuery assumes: src.process.user is the user of the creating process.
- KQL – Defender XDR: The folder name is only seen on a created process's command line; file activity inside it is not covered.
- KQL – Defender XDR assumes: ProcessCommandLine holds the full command line of the created process.
// HUNT: Command line referencing an envchecker-NNNNNNNNN folder // MITRE: T1119, T1560 // CONF: low // FP: low // COST: low // REQUIRES: Falcon sensor process telemetry // FALSE POSITIVES: none expected // LOOKBACK: 7d #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | CommandLine=/envchecker-[0-9]{9,10}/i | table([@timestamp, aid, ComputerName, UserName, FileName, ParentBaseFileName, CommandLine]) | sort(@timestamp, order=desc)
2 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.type: Event typetgt.process.cmdline: Created command line
Recommended fields
tgt.process.name: Created processendpoint.name: Host
Optional fields
src.process.user: Creator user
Readiness test
event.type = 'Process Creation' and tgt.process.cmdline = * | limit 5 | columns event.type, tgt.process.cmdline
// HUNT: Command line referencing an envchecker-NNNNNNNNN folder
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1119, T1560
// CONF: low | FP: low
// FALSE POSITIVES: none expected
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: a process-creation command line containing envchecker- followed by nine or ten digits. matches is case-insensitive, which reproduces the CQL intent.
// REQUIRES: SentinelOne process-creation telemetry with command lines
// TUNING: None expected.
// LOOKBACK: 7d
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: The folder name is only seen when it appears on a created process's command line; file activity inside it is not covered.
// VALIDATION LEVEL: static-reviewed
event.type = 'Process Creation' and tgt.process.cmdline matches 'envchecker-[0-9]{9,10}'
| columns event.time, endpoint.name, agent.uuid, src.process.user, tgt.process.name, src.process.name, tgt.process.cmdline
| sort -event.time
| limit 200Automated validation: static-passed
2 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeProcessCommandLine: Created command line
Recommended fields
FileName: Created process
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, ProcessCommandLine | take 5
// HUNT: Command line referencing an envchecker-NNNNNNNNN folder
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1119, T1560
// CONF: low | FP: low
// FALSE POSITIVES: none expected
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: a created process whose command line contains envchecker- followed by nine or ten digits. The (?i) prefix reproduces the CQL case-insensitive intent.
// REQUIRES: Defender for Endpoint process-creation telemetry (DeviceProcessEvents)
// TUNING: None expected.
// LOOKBACK: 7d
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: The folder name is only seen on a created process's command line; file activity inside it is not covered.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0
DeviceProcessEvents
| where Timestamp >= ago(7d)
| where ProcessCommandLine matches regex @"(?i)envchecker-[0-9]{9,10}"
| project Timestamp, ReportId, DeviceId, DeviceName, AccountName, FileName, InitiatingProcessFileName, ProcessCommandLine
| sort by Timestamp desc
| take 200Looks for: every host that ran a binary named ovhcloud, with image path and hashes. Compare MD5HashData to the five advisory values (Q1 does this automatically) and review any host that ran the CLI on or after 2026-10-19 12:47 UTC (14:47 Paris) and before the files were deleted at 14:21 UTC. A binary built by OVHcloud is not the same file as the swapped one, so group by hash. FP: none, this is an inventory.
Known limitations and deployment notes
- PowerQuery: A copy of the CLI renamed to something else is not inventoried.
- PowerQuery assumes: tgt.process.image.md5 and tgt.process.image.sha256 are populated on this tenant's Process Creation events.
- KQL – Defender XDR: A copy of the CLI renamed to something else is not inventoried.
- KQL – Defender XDR assumes: MD5 is populated in DeviceProcessEvents; SHA256 is often empty.
// HUNT: ovhcloud execution inventory with hashes // MITRE: T1195.002, T1204.002 // CONF: high // FP: low // COST: low // REQUIRES: Falcon sensor process telemetry // FALSE POSITIVES: none, inventory query // LOOKBACK: 7d #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | FileName=/^ovhcloud(\.exe)?$/i | groupBy([ComputerName, aid, ImageFileName, MD5HashData, SHA256HashData], function=[count(as=runs), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)], limit=1000) | sort(last_seen, order=desc) | table([ComputerName, aid, ImageFileName, MD5HashData, SHA256HashData, runs, first_seen, last_seen])
4 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.type: Event typetgt.process.name: Created processevent.time: Event timeendpoint.name: Host
Recommended fields
tgt.process.image.path: Created image pathtgt.process.image.sha256: Created image SHA256
Optional fields
tgt.process.image.md5: Created image MD5agent.uuid: Agent identifier
Readiness test
event.type = 'Process Creation' and tgt.process.name = * | limit 5 | columns event.time, endpoint.name, event.type, tgt.process.name, tgt.process.image.sha256
// HUNT: ovhcloud execution inventory with hashes // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1195.002, T1204.002 // CONF: high | FP: low // FALSE POSITIVES: none, inventory query // EQUIVALENCE: equivalent-with-schema-assumptions - Same inventory: process starts of a binary named ovhcloud, counted per host and image with its hashes and first and last seen. The new process is tgt.process.* on Process Creation. // REQUIRES: SentinelOne process-creation telemetry with image hashes // TUNING: Compare each hash with the replaced-binary list on Q1; any other hash is a different build. // LOOKBACK: 7d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: A copy of the CLI renamed to something else is not inventoried. // VALIDATION LEVEL: static-reviewed event.type = 'Process Creation' and tgt.process.name matches '^ovhcloud(\\.exe)?$' | group runs=count(), first_seen=min(event.time), last_seen=max(event.time) by endpoint.name, agent.uuid, tgt.process.image.path, tgt.process.image.md5, tgt.process.image.sha256 | sort -last_seen | columns endpoint.name, agent.uuid, tgt.process.image.path, tgt.process.image.md5, tgt.process.image.sha256, runs, first_seen, last_seen | limit 1000
Automated validation: static-passed
3 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeFileName: Created processDeviceName: Host
Recommended fields
FolderPath: Created image pathMD5: Created image MD5
Optional fields
SHA256: Hash. SHA256 is often unpopulated in DeviceProcessEvents; MD5 is the safer pivot here.
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, DeviceName, FileName, FolderPath, MD5 | take 5
// HUNT: ovhcloud execution inventory with hashes // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1195.002, T1204.002 // CONF: high | FP: low // FALSE POSITIVES: none, inventory query // EQUIVALENCE: equivalent-with-schema-assumptions - Same inventory: process starts of a binary named ovhcloud, counted per host and image with its hashes and first and last seen. // REQUIRES: Defender for Endpoint process-creation telemetry (DeviceProcessEvents) // TUNING: Compare each hash with the replaced-binary list on Q1; any other hash is a different build. // LOOKBACK: 7d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: A copy of the CLI renamed to something else is not inventoried. // VALIDATION LEVEL: schema-aligned // AUTOMATED VALIDATION: static-passed; static=passed-with-warnings; fixtures=0/0 DeviceProcessEvents | where Timestamp >= ago(7d) | where FileName matches regex @"(?i)^ovhcloud(\.exe)?$" | summarize runs = count(), first_seen = min(Timestamp), last_seen = max(Timestamp) by DeviceName, DeviceId, FolderPath, MD5, SHA256 | sort by last_seen desc | take 1000
Looks for: curl, wget, a shell, brew or PowerShell command lines that fetch an ovhcloud-cli release archive or run the project install script. Run it over 2026-10-19 12:47 to 14:21 UTC to find hosts that pulled a swapped file. The reporting disagrees on Homebrew: one analysis lists it as exposed, another says the cask pinned the original hashes so the install would have failed. Treat brew hits as an inventory item, not proof of exposure. FP: normal installs and CI jobs outside the window.
Known limitations and deployment notes
- PowerQuery: A download made by a process outside the listed names, such as a browser, is not captured.
- PowerQuery assumes: src.process.user is the user of the creating process.
- PowerQuery assumes: The unescaped dots in releases.download and ovh.tap.ovhcloud-cli match any single character, as in the CQL.
- KQL – Defender XDR: A download made by a process outside the listed names, such as a browser, is not captured.
- KQL – Defender XDR assumes: The unescaped dots in releases.download and ovh.tap.ovhcloud-cli match any single character, as in the CQL.
// HUNT: OVHcloud CLI release archive download or install.sh run // MITRE: T1195.002, T1105 // CONF: medium // FP: medium // COST: low // REQUIRES: Falcon sensor process telemetry // FALSE POSITIVES: legitimate installs and CI jobs outside 2026-10-19 12:47-14:21 UTC // TUNING: restrict the time picker to 2026-10-19 12:47 to 14:21 UTC for exposure scoping; outside that window these are normal installs // LOOKBACK: 7d #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | CommandLine=/ovhcloud-cli/i | CommandLine=/(releases.download|install\.sh|ovh.tap.ovhcloud-cli|ovhcloud-cli_(Linux|Darwin|Windows)_(x86_64|arm64)\.(tar\.gz|zip))/i | FileName=/^(curl|wget|sh|bash|zsh|dash|brew|powershell|pwsh|python3?|ruby|ovhcloud)(\.exe)?$/i | table([@timestamp, aid, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine]) | sort(@timestamp, order=desc)
3 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.type: Event typetgt.process.name: Created processtgt.process.cmdline: Created command line
Recommended fields
src.process.name: Creator processendpoint.name: Host
Optional fields
src.process.user: Creator user
Readiness test
event.type = 'Process Creation' and tgt.process.cmdline = * | limit 5 | columns event.type, tgt.process.name, tgt.process.cmdline
// HUNT: OVHcloud CLI release archive download or install.sh run // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1195.002, T1105 // CONF: medium | FP: medium // FALSE POSITIVES: legitimate installs and CI jobs outside 2026-10-19 12:47-14:21 UTC // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: a created process from a downloader, shell, interpreter or the CLI itself whose command line names ovhcloud-cli and a release download, install script, tap or release archive. Both regexes are kept as written; matches is case-insensitive, which reproduces the CQL intent. // REQUIRES: SentinelOne process-creation telemetry with command lines // TUNING: Restrict the time picker to 2026-10-19 12:47 to 14:21 UTC for exposure scoping; outside that window these are normal installs. // LOOKBACK: 7d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: A download made by a process outside the listed names, such as a browser, is not captured. // VALIDATION LEVEL: static-reviewed event.type = 'Process Creation' and tgt.process.name matches '^(curl|wget|sh|bash|zsh|dash|brew|powershell|pwsh|python3?|ruby|ovhcloud)(\\.exe)?$' and tgt.process.cmdline matches 'ovhcloud-cli' and tgt.process.cmdline matches '(releases.download|install\\.sh|ovh.tap.ovhcloud-cli|ovhcloud-cli_(Linux|Darwin|Windows)_(x86_64|arm64)\\.(tar\\.gz|zip))' | columns event.time, endpoint.name, agent.uuid, src.process.user, src.process.name, tgt.process.name, tgt.process.cmdline | sort -event.time | limit 200
Automated validation: static-passed
3 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeFileName: Created processProcessCommandLine: Created command line
Recommended fields
InitiatingProcessFileName: Creator process
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, FileName, ProcessCommandLine | take 5
// HUNT: OVHcloud CLI release archive download or install.sh run // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1195.002, T1105 // CONF: medium | FP: medium // FALSE POSITIVES: legitimate installs and CI jobs outside 2026-10-19 12:47-14:21 UTC // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: a created process from a downloader, shell, interpreter or the CLI itself whose command line names ovhcloud-cli and a release download, install script, tap or release archive. The (?i) prefix reproduces the CQL case-insensitive intent. // REQUIRES: Defender for Endpoint process-creation telemetry (DeviceProcessEvents) // TUNING: Restrict the time range to 2026-10-19 12:47 to 14:21 UTC for exposure scoping; outside that window these are normal installs. // LOOKBACK: 7d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: A download made by a process outside the listed names, such as a browser, is not captured. // VALIDATION LEVEL: schema-aligned // AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0 DeviceProcessEvents | where Timestamp >= ago(7d) | where FileName matches regex @"(?i)^(curl|wget|sh|bash|zsh|dash|brew|powershell|pwsh|python3?|ruby|ovhcloud)(\.exe)?$" | where ProcessCommandLine contains "ovhcloud-cli" | where ProcessCommandLine matches regex @"(?i)(releases.download|install\.sh|ovh.tap.ovhcloud-cli|ovhcloud-cli_(Linux|Darwin|Windows)_(x86_64|arm64)\.(tar\.gz|zip))" | project Timestamp, ReportId, DeviceId, DeviceName, AccountName, InitiatingProcessFileName, FileName, ProcessCommandLine | sort by Timestamp desc | take 200
Looks for: every public address an ovhcloud process connects to, grouped by host. The real CLI talks to OVHcloud API endpoints, so the baseline is small and anything else stands out, including the upload address in Q2. FP: OVHcloud API and update endpoints, which change over time.
Known limitations and deployment notes
- PowerQuery: Connections through a proxy appear as the proxy address only.
- PowerQuery assumes: src.process.name names the connecting process on IP Connect events.
- PowerQuery assumes: dst.ip.address holds an IPv4 address, as the CQL's IPv4-only connection event does.
- KQL – Defender XDR: Connections through a proxy appear as the proxy address only.
- KQL – Defender XDR assumes: RemoteIP holds an IPv4 address; IPv6 destinations evaluate to null in the range test and are dropped, as the CQL's IPv4-only event does.
// HUNT: Public destinations contacted by ovhcloud // MITRE: T1041, T1071.001 // CONF: low // FP: medium // COST: low // REQUIRES: Falcon sensor network telemetry with process name // FALSE POSITIVES: OVHcloud API endpoints, proxies, CDN addresses used by the real CLI // TUNING: baseline 14 days of ovhcloud egress, then exclude the stable OVHcloud API addresses and your proxy; review what is left // LOOKBACK: 7d #event_simpleName=NetworkConnectIP4 | ContextBaseFileName=/^ovhcloud(\.exe)?$/i | !cidr(RemoteAddressIP4, subnet=["10.0.0.0/8","172.16.0.0/12","192.168.0.0/16","127.0.0.0/8"]) | groupBy([ComputerName, aid, RemoteAddressIP4, RemotePort], function=[count(as=conns), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)], limit=1000) | sort(conns, order=desc) | table([ComputerName, aid, RemoteAddressIP4, RemotePort, conns, first_seen, last_seen])
5 required fields · View requirements
Required telemetry
- IP Connect
Required fields
event.type: Event typesrc.process.name: Connecting processdst.ip.address: Destination addressevent.time: Event timeendpoint.name: Host
Recommended fields
dst.port.number: Destination port
Optional fields
agent.uuid: Agent identifier
Readiness test
event.type = 'IP Connect' and dst.ip.address = * | limit 5 | columns event.time, endpoint.name, event.type, src.process.name, dst.ip.address
// HUNT: Public destinations contacted by ovhcloud // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1041, T1071.001 // CONF: low | FP: medium // FALSE POSITIVES: OVHcloud API endpoints, proxies, CDN addresses used by the real CLI // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate and aggregation: IP connections by a process named ovhcloud to destinations outside 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and 127.0.0.0/8, counted per host, address and port. The connecting process is src.process.name on IP Connect events. // REQUIRES: SentinelOne network (IP Connect) telemetry with process name // TUNING: Baseline 14 days of ovhcloud egress, then exclude the stable OVHcloud API addresses and your proxy; review what is left. // LOOKBACK: 7d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: Connections through a proxy appear as the proxy address only. // VALIDATION LEVEL: static-reviewed event.type = 'IP Connect' and src.process.name matches '^ovhcloud(\\.exe)?$' and !(net_ipsubnet(dst.ip.address, '10.0.0.0/8') or net_ipsubnet(dst.ip.address, '172.16.0.0/12') or net_ipsubnet(dst.ip.address, '192.168.0.0/16') or net_ipsubnet(dst.ip.address, '127.0.0.0/8')) | group conns=count(), first_seen=min(event.time), last_seen=max(event.time) by endpoint.name, agent.uuid, dst.ip.address, dst.port.number | sort -conns | columns endpoint.name, agent.uuid, dst.ip.address, dst.port.number, conns, first_seen, last_seen | limit 1000
Automated validation: static-passed
4 required fields · View requirements
Required telemetry
- DeviceNetworkEvents
Required fields
Timestamp: Event timeInitiatingProcessFileName: Connecting processRemoteIP: Remote addressDeviceName: Host
Recommended fields
RemotePort: Remote port
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceNetworkEvents | where Timestamp > ago(1d) | project Timestamp, DeviceName, InitiatingProcessFileName, RemoteIP | take 5
// HUNT: Public destinations contacted by ovhcloud // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1041, T1071.001 // CONF: low | FP: medium // FALSE POSITIVES: OVHcloud API endpoints, proxies, CDN addresses used by the real CLI // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate and aggregation: network events by a process named ovhcloud to remote addresses outside 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and 127.0.0.0/8, counted per host, address and port. // REQUIRES: Defender for Endpoint network telemetry (DeviceNetworkEvents) // TUNING: Baseline 14 days of ovhcloud egress, then exclude the stable OVHcloud API addresses and your proxy; review what is left. // LOOKBACK: 7d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: Connections through a proxy appear as the proxy address only. // VALIDATION LEVEL: schema-aligned // AUTOMATED VALIDATION: static-passed; static=passed-with-warnings; fixtures=0/0 DeviceNetworkEvents | where Timestamp >= ago(7d) | where InitiatingProcessFileName matches regex @"(?i)^ovhcloud(\.exe)?$" | where not(ipv4_is_in_range(RemoteIP, "10.0.0.0/8") or ipv4_is_in_range(RemoteIP, "172.16.0.0/12") or ipv4_is_in_range(RemoteIP, "192.168.0.0/16") or ipv4_is_in_range(RemoteIP, "127.0.0.0/8")) | summarize conns = count(), first_seen = min(Timestamp), last_seen = max(Timestamp) by DeviceName, DeviceId, RemoteIP, RemotePort | sort by conns desc | take 1000
Detection Rules by Platform
CrowdStrike: Custom IOA
| IOA name | Rule type | Pattern | Exclusions | Promotes | Deployment path |
|---|---|---|---|---|---|
| ovhcloud resolving Solana RPC | Domain Name / DNS request | Process image name ovhcloud or ovhcloud.exe; domain matches the Solana RPC hostnames in Q3 | None | Promote Q3 | Detect for 7 days, then Prevent; the CLI has no legitimate use |
| ovhcloud spawning shells or credential tools | Process Creation | Parent image ovhcloud or ovhcloud.exe; child image a shell, archiver or cloud/secret CLI from Q5 | Sanctioned wrapper scripts after baseline | Promote Q5 | Detect for 14 days, then decide |
Hash matches (Q1) and the upload address (Q2) belong in Falcon IOC Management. Use the CSV in Section 10. Solana RPC hostnames are shared public services and stay out of IOC Management.
Alert package: Q3
Q3: ovhcloud process resolved a Solana RPC endpoint
Subject: [HIGH] Detection: OVHcloud CLI compromised build, Solana RPC lookup from ovhcloud
A process named ovhcloud looked up a Solana RPC hostname. The OVHcloud advisory lists these as the stealer's exfiltration helpers. Treat the host as holding stolen secrets until proven otherwise.
Triage checklist
- Isolate the host (Falcon network containment).
- Run Q7 for the host: note MD5HashData and SHA256HashData of the ovhcloud image and compare with Section 4.
- Check for an envchecker-NNNNNNNNN folder in the temp directory (N1).
- Run Q2 and Q9 for the host for the upload address.
- List credentials readable by the user (Section 13 playbook P5) and rotate from another machine.
- Find how the binary arrived (Q8, shell history, CI logs).
- Replace with v0.16.0 or later and verify the attestation.
Pivot queries
- Host scope: Q7 filtered to the host aid.
- Environment scope: Q4 and Q8 over 2026-10-19.
SentinelOne: custom detection rules
| Rule | Source query | Run as | Notes |
|---|---|---|---|
| ovhcloud resolving Solana RPC | Q3, PowerQuery tab | Scheduled custom detection rule | Bind entities so the alert names the host instead of "Unknown Device" |
| ovhcloud spawning shells or credential tools | Q5, PowerQuery tab | Scheduled custom detection rule | Bind entities so the alert names the host instead of "Unknown Device". This pack's own text mentions a baseline or pilot; follow it as written |
Create the rule with POST /web/api/v2.1/cloud-detection/rules, queryType: "scheduled" and queryLang: "2.0", with the query in data.scheduledParams.query. Use 2.0 operator spellings: a rule written with the 1.0 token ContainsCIS was accepted and never fired on a tenant. The API does not warn, so lint the body before deploying.
Microsoft Defender: custom detection rules
| Rule | Source query | Run as | Notes |
|---|---|---|---|
| ovhcloud resolving Solana RPC | Q3, KQL – Defender XDR tab | Custom detection rule, alert only, every hour | The query returns Timestamp, ReportId and DeviceId, which Microsoft recommends for Defender for Endpoint tables so alerts carry the device group scope and the process tree. Remove the Timestamp filter and the final sort and take before saving the rule: the service applies the rule's lookback itself |
| ovhcloud spawning shells or credential tools | Q5, KQL – Defender XDR tab | Custom detection rule, alert only, every hour | The query returns Timestamp, ReportId and DeviceId, which Microsoft recommends for Defender for Endpoint tables so alerts carry the device group scope and the process tree. Remove the Timestamp filter and the final sort and take before saving the rule: the service applies the rule's lookback itself. This pack's own text mentions a baseline or pilot; follow it as written |
Q1, Q2, Q4, Q6, Q7, Q8 and Q9 stay a hunt in this pack, not a rule. A rule can generate only 150 alerts per run. Continuous (NRT) frequency needs a single table and no comment lines, so delete the generated comment header from the query first.
Machine-Readable IOC Appendix
Import the CSV below only. The five MD5s and the upload address come from one source (the OVHcloud advisory), so they ship as detect. Only the swapped-archive SHA-256, named by three independent write-ups, ships as prevent. The legitimate original archive hash and the Solana hostnames are deliberately excluded.
type,value,action,severity,expiration,description,tags md5,6f2f086239f3d88eb2750e871901927d,detect,high,2027-04-24,malicious ovhcloud Darwin_arm64 replaced binary,campaign:OVHcloud-CLI-Release-Backdoor md5,347e65eb7f2d78d1c6872690272a6cc3,detect,high,2027-04-24,malicious ovhcloud Darwin_x86_64 replaced binary,campaign:OVHcloud-CLI-Release-Backdoor md5,01f57206d37c28cf711677f612052913,detect,high,2027-04-24,malicious ovhcloud Linux_arm64 replaced binary,campaign:OVHcloud-CLI-Release-Backdoor md5,301bff0b51980b9e76c93633e7cfd3bd,detect,high,2027-04-24,malicious ovhcloud Linux_x86_64 replaced binary,campaign:OVHcloud-CLI-Release-Backdoor md5,6a3cf153338c416169cda2fe704c8848,detect,high,2027-04-24,malicious ovhcloud.exe Windows x86_64 replaced binary,campaign:OVHcloud-CLI-Release-Backdoor sha256,3833daffcaaaab27e4e0e0833240ffcb1df8df648f4ec0bc4a75c176e28933cb,prevent,critical,2027-04-24,swapped v0.15.0 Linux arm64 archive,campaign:OVHcloud-CLI-Release-Backdoor sha256,e9c31dca232cdaa8058cb59eadf425c182a23afb6ddb057eb92eda13b850dff4,detect,medium,2027-04-24,sample linked in OVHcloud advisory (type unstated),campaign:OVHcloud-CLI-Release-Backdoor sha256,3c10bd782cd0dc5f720a33570af4651814cb82917e54d27ffce3849a3c5cef2a,detect,medium,2027-04-24,sample linked in OVHcloud advisory (type unstated),campaign:OVHcloud-CLI-Release-Backdoor sha256,3cbe1ac592beeca7ce0ad0ba945349a9ef33ef0f1324935c3a115f30b1d0c927,detect,medium,2027-04-24,sample linked in OVHcloud advisory (type unstated),campaign:OVHcloud-CLI-Release-Backdoor sha256,25c68c5e9ae23883aebda59f4b6a7df78e1ce8aae5e4b4ba636419497d54bbec,detect,medium,2027-04-24,sample linked in OVHcloud advisory (type unstated),campaign:OVHcloud-CLI-Release-Backdoor sha256,f97284530343b3903ffd19bbe70ab6f62e36f04eb7e5f885934bd21d10899e8a,detect,medium,2027-04-24,sample linked in OVHcloud advisory (type unstated),campaign:OVHcloud-CLI-Release-Backdoor ipv4,45.32.148.174,detect,high,2027-04-24,stealer upload endpoint TCP/443 /upload,campaign:OVHcloud-CLI-Release-Backdoor # NOT imported, LEGITIMATE original v0.15.0 Linux arm64 archive (known-good reference only): 5c2beb56e6d83a4b426565a1557791ef759316abcc4effb38dd627c90aeb80e0 # NOT imported, public Solana RPC hostnames are shared services: hunt with Q3 and Q4 # NOT imported, the other swapped archives have no published hash: REPLACE_WITH_SHA256_WHEN_PUBLISHED
indicatorType,indicatorValue,action,title,description,expirationTime,severity,recommendedActions,rbacGroups,category,mitretechniques,GenerateAlert FileSha256,3833daffcaaaab27e4e0e0833240ffcb1df8df648f4ec0bc4a75c176e28933cb,Block,swapped v0.15.0 Linux arm64 archive,swapped v0.15.0 Linux arm64 archive,2027-04-24T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True FileSha256,e9c31dca232cdaa8058cb59eadf425c182a23afb6ddb057eb92eda13b850dff4,Audit,sample linked in OVHcloud advisory,sample linked in OVHcloud advisory (type unstated),2027-04-24T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True FileSha256,3c10bd782cd0dc5f720a33570af4651814cb82917e54d27ffce3849a3c5cef2a,Audit,sample linked in OVHcloud advisory,sample linked in OVHcloud advisory (type unstated),2027-04-24T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True FileSha256,3cbe1ac592beeca7ce0ad0ba945349a9ef33ef0f1324935c3a115f30b1d0c927,Audit,sample linked in OVHcloud advisory,sample linked in OVHcloud advisory (type unstated),2027-04-24T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True FileSha256,25c68c5e9ae23883aebda59f4b6a7df78e1ce8aae5e4b4ba636419497d54bbec,Audit,sample linked in OVHcloud advisory,sample linked in OVHcloud advisory (type unstated),2027-04-24T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True FileSha256,f97284530343b3903ffd19bbe70ab6f62e36f04eb7e5f885934bd21d10899e8a,Audit,sample linked in OVHcloud advisory,sample linked in OVHcloud advisory (type unstated),2027-04-24T00:00:00.0Z,Medium,Investigate the device and pivot into the pack's queries,,,,True IpAddress,45.32.148.174,Audit,stealer upload endpoint TCP/443 /upload,stealer upload endpoint TCP/443 /upload,2027-04-24T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True
Column names and allowed values are from Microsoft's documented parameter table. Left out because this block maps only SHA-1, SHA-256, IPv4, domain and URL indicators: md5 5. Prevent rows import as Block, without remediation; network indicators cannot use BlockAndRemediate. Check each expirationTime before importing: an indicator whose date has passed is already expired. Download the sample CSV from the Import page to confirm the column order before a bulk import. Each file takes at most 500 indicators. Learn describes the import per entity type, so if a mixed file is rejected, split it by indicatorType. An imported indicator does nothing unless the matching Advanced features are on (for example Custom network indicators and Allow or block file).
This pack does not ship a SentinelOne import block: the import format could not be verified against SentinelOne's own documentation. Export a sample from your console and the block can be added with confidence.
# Hunt strings (not IOC Management values)
Go build info (go version -m): mod github.com/ovh/ovhcloud-cli v0.15.1-0.20261013083505-5ead8ef9b43d+dirty
vcs.revision=5ead8ef9b43d354dfb5caadff803b11afc8c5c58 (real v0.15.0 tag = 760fd72de93cbea3b507eb36dc0095991e4048e5)
# Do not alert on +dirty or vcs.modified=true alone: the clean v0.16.0 build reports both
Temp folder: envchecker-[0-9]{9,10} (created each run, never deleted)
Exfil: 45.32.148.174:443 path /upload (time-sensitive)
Solana wallet used by the binary: BHaDaVbdCYFNaKZGn7yLXFneWoJHZgLiBYkHxVrjyALF
RPC endpoints: solana.drpc.org, solana-rpc.publicnode.com, solana.api.pocket.network, api.mainnet-beta.solana.com, solana.api.onfinality.io (advisory writes it with a /public path)
Process lineage: ovhcloud -> shell / archiver / aws / gcloud / az / kubectl / doctl / docker / git / terraformProduct: OVHcloud CLI (ovh/ovhcloud-cli), binary names ovhcloud and ovhcloud.exe Affected releases: v0.11.0, v0.12.0, v0.13.0, v0.14.0, v0.15.0 Clean: v0.10.0 and earlier, v0.16.0 and later Replaced assets: Darwin arm64, Darwin x86_64, Linux arm64, Linux x86_64, Windows x86_64, checksums.txt Not replaced: Linux i386, Windows i386, Windows arm64, Js wasm Uploader seen by the reporter: HassenBF (a hijacked account, not an attacker handle) Exposure window: 2026-10-19 12:47 to 14:21 UTC (14:47 to 16:21 Paris) Attribution: unconfirmed (see Section 2)
# Paths the stealer targets per the OVHcloud advisory (rotate these) ~/.aws/credentials ~/.aws/config ~/.aws/cli/cache ~/.config/gcloud/ ~/.azure/ ~/.docker/config.json ~/.dockercfg ~/.kube/config ~/.ssh ~/.netrc ~/.gitconfig ~/.config/doctl/config.yaml ~/.terraform.d/credentials.tfrc.json ~/.claude ~/.hermes/ ~/.local/share/opencode/ ~/.config/opencode/ VS Code and Cursor globalStorage state.vscdb; Continue and Sourcegraph Cody databases # Verify v0.16.0 or later (do not run first) # gh release verify-asset v0.16.0 <FILE> # gh attestation verify <FILE> --repo ovh/ovhcloud-cli
Detection Validation Gates
| Gate | Check | Pass condition |
|---|---|---|
| Telemetry ready | Process creation with command line, hashes and parent (CrowdStrike: ProcessRollup2. SentinelOne: Process Creation. Defender: DeviceProcessEvents); outbound network connection (CrowdStrike: NetworkConnectIP4. SentinelOne: IP Connect. Defender: DeviceNetworkEvents); DNS lookup with process (CrowdStrike: DnsRequest. SentinelOne: DNS Resolved. Defender: DeviceEvents); plus proxy and CI logs. | A sample event from each source is searchable in the last hour. Confirm ContextBaseFileName is filled on DnsRequest for Q3. |
| Benign baseline | Run Q4, Q5, Q8 and Q9 over 14 days before alerting. | Fewer than 10 hits per week per query, each explained by a named tool, wrapper or install job. |
| Positive tests | Q7: run an ovhcloud --help on a lab host with the legitimate CLI and confirm it appears. Q6: run echo envchecker-123456789 on a lab host. Q4: run nslookup solana.drpc.org on a lab host. Q3 cannot be exercised safely without the malicious binary, so validate its field mapping with Q4 output. Never run a malicious sample. | Each test fires its query within ingest delay. Use no live malware. |
| Promotion | Promote Q1, Q2, Q3 first (high confidence, low FP), then Q5 after baselining. Q7 and Q8 stay as scoping hunts. | Detect for 14 days with no unexplained hits, then enable prevention where supported. |
Hardening: Tiered & Deployable
- T1. Turn on immutable releases for every repository that publishes binaries, at the organisation level. A release created after the setting is on cannot have assets changed or removed. OVHcloud's own fix. Existing releases are not protected retroactively, so re-publish critical old releases. Framework: ATT&CK T1195.002 and M1051; GitHub immutable-releases documentation (GA since Oct 2025 per [#4]). Verify: Playbook P1.
- T2. Remove personal accounts and excess Write access from release-publishing repositories. The uploader here was a contributor who was not a public org member. Publish only through a service account or workflow identity with short-lived credentials. Framework: M1018, M1026; T1078. Verify: N4.
- T3. Pin and verify the hash of every release asset you download in CI, Dockerfiles and install scripts. The single pinned-hash job here failed and exposed the swap. Pin against a source other than the same release page (checksums.txt was swapped too). Framework: ATT&CK T1195.002 and M1051; ⚠ best-practice, no formal benchmark. Verify: Playbook P2.
- T4. Rotate every secret readable on any host that ran a swapped build, from a different machine. Framework: M1027 Password Policies, M1026; T1552.001. Verify: Playbook P5.
- T5. Block or alert on Solana RPC lookups from build runners and servers with no blockchain role. Public services, so block by role, never globally. Framework: M1037 Filter Network Traffic; T1102. Verify: Q4 and Playbook P4.
- T6. Require signature or attestation verification before installing release binaries. OVHcloud v0.16.0 ships a release attestation; check it with the GitHub CLI. Framework: ⚠ best-practice, no formal benchmark; vendor guidance [#3] [#4]. Verify: Playbook P2.
- T7. Stop piping install scripts to a shell on developer machines. Download, hash-check, then run. Framework: M1017 User Training; ⚠ best-practice.
- T8. Keep developer credentials short-lived and out of well-known files. Use SSO-backed cloud sessions, hardware-backed SSH keys and a secret manager, so a file-scraping stealer finds nothing durable. Framework: M1041 Encrypt Sensitive Information, M1022 Restrict File and Directory Permissions; T1552.
- T9. Sign commits and tags, use CODEOWNERS on release workflows, and link release rights to central identity management. OVHcloud lists these as its own next steps [#1]. Framework: M1032 Multi-factor Authentication; T1078.
- T10. Control IDE extensions on developer machines. The advisory attributes the original account takeover to a malicious editor extension [#1]. Allowlist extensions. ⚠ best-practice, no formal benchmark.
Deployable Playbooks
P1 · Enable immutable releases (GitHub, organisation owner)
1. GitHub org Settings > Repository defaults (or each repo: Settings > General > Releases) > enable "Release immutability". ⚠ Menu names change; confirm in GitHub's current immutable-releases documentation. 2. Publish new releases from CI as a draft, attach all assets, then publish. Assets are locked at publish. 3. Verify an immutable release locally: gh release verify v0.16.0 --repo ovh/ovhcloud-cli Prerequisites: organisation owner rights; release process that attaches assets before publishing. Reboot required: no Rollback: untick the setting. Releases already published while it was on stay immutable by design.
P2 · Hash-pin and verify a release asset in CI (POSIX shell)
#!/bin/sh
set -eu
VERSION="v0.16.0"
ASSET="ovhcloud-cli_Linux_x86_64.tar.gz"
# EXPECTED_SHA256 comes from your own reviewed config, not from the same release page
EXPECTED_SHA256="${OVH_CLI_SHA256:?set the pinned hash}"
curl -fsSL -o "$ASSET" "https://github.com/ovh/ovhcloud-cli/releases/download/${VERSION}/${ASSET}"
echo "${EXPECTED_SHA256} ${ASSET}" | sha256sum -c -
gh attestation verify "$ASSET" --repo ovh/ovhcloud-cli
tar -xzf "$ASSET"
Prerequisites: curl, sha256sum (shasum -a 256 on macOS), GitHub CLI authenticated; the hash pinned after you verified it once.
Reboot required: no
Rollback: remove the step from the pipeline; nothing on the host changes.
P3 · Read-only exposure triage on a host (do not run the CLI)
# macOS / Linux
ls -ld "${TMPDIR:-/tmp}"/envchecker-* 2>/dev/null
for f in $(command -v ovhcloud); do md5sum "$f" 2>/dev/null || md5 "$f"; done
go version -m "$(command -v ovhcloud)" 2>/dev/null | grep -E 'mod|vcs.revision'
# Windows PowerShell
Get-ChildItem $env:TEMP -Directory -Filter "envchecker-*" -ErrorAction SilentlyContinue
Get-Command ovhcloud.exe -ErrorAction SilentlyContinue | ForEach-Object { Get-FileHash $_.Source -Algorithm MD5 }
Prerequisites: shell access; Go toolchain optional (go version -m only reads the file).
Reboot required: no
Rollback: none required, every command is read-only.
P4 · Resolver block for Solana RPC on build hosts (dnsmasq example)
# /etc/dnsmasq.d/block-solana-rpc.conf (apply ONLY to build and server networks) address=/solana.drpc.org/0.0.0.0 address=/solana-rpc.publicnode.com/0.0.0.0 address=/solana.api.pocket.network/0.0.0.0 address=/api.mainnet-beta.solana.com/0.0.0.0 address=/solana.api.onfinality.io/0.0.0.0 Prerequisites: a resolver you control for the build network; confirm no blockchain workload depends on these names. Reboot required: no (restart dnsmasq: systemctl restart dnsmasq) Rollback: delete the file and restart dnsmasq.
P5 · Secret rotation checklist after exposure
Work from a different, clean machine. For the affected user and host: 1. OVHcloud API keys / consumer keys; ~/.ovh.conf and any OVH_* environment variables 2. AWS, GCP, Azure sessions and keys; Terraform Cloud tokens; doctl tokens 3. SSH keys in ~/.ssh (generate new, remove old public keys from servers and GitHub) 4. Kubernetes configs; registry logins in ~/.docker/config.json 5. npm / .netrc / git credentials; GitHub personal access tokens 6. Tokens held by IDEs and AI coding assistants (editor databases, assistant data directories) 7. Review cloud and identity audit logs for use of old credentials since 2026-10-19 Prerequisites: admin rights on each service; a clean workstation. Reboot required: no Rollback: not applicable; a rotated secret should not be restored. Keep the old value revoked.
Containment Runbook
| Phase | Actions | Owner | Evidence |
|---|---|---|---|
| Isolate | Network-contain any host with a Q1, Q2 or Q3 hit or an envchecker folder (Falcon network containment). Pause CI jobs that fetched the CLI in the window. Do not run the binary. | SOC / DevOps | Containment timestamp, host list from Q7 and Q8 |
| Isolate: by platform | CrowdStrike: network-contain the host. SentinelOne: network quarantine (Disconnect from Network; confirm the label in your console version). Microsoft Defender: Isolate device (full network isolation). | SOC / DevOps | Containment timestamp, host list from Q7 and Q8 |
| Scope | Run Q7 and Q8 for 2026-10-19 12:47 to 14:21 UTC. Check N2 build logs and N3 egress logs. Mark each host as swapped, clean, or unknown by hash. | SOC | Hash table per host |
| Eradicate | Rotate secrets (P5) from a clean machine. Delete the binary and any envchecker folders after imaging if evidence is needed. Reinstall v0.16.0 or later with a verified attestation (P2). | IR / Owner of each secret | Rotation ticket per secret class, attestation check output |
| Recover | Return hosts to service after reinstall. Re-enable CI with pinned hashes. Watch for use of old credentials in cloud and identity audit logs for 30 days. | DevOps / Cloud security | Audit-log queries, clean Q1 to Q3 runs |
| Harden | Enable immutable releases, restrict Write, and apply Section 12 controls. | Engineering leadership | Org settings screenshot or export |
Detection Coverage Map
| Technique | Behavior | CQL | IOA | CrowdStrike coverage | SentinelOne | Defender XDR |
|---|---|---|---|---|---|---|
| T1078 | Hijacked GitHub account uploads assets | GAP | GAP | GAP | ||
| T1195.002 | Swapped release assets installed or downloaded | Q1, Q7, Q8 | Partial | Partial: same limit as CrowdStrike | Partial: Q1 telemetry gap | |
| T1204.002 | Swapped binary executed | Q1, Q7 | Good | Good, with assumptions | Partial: Q1 telemetry gap | |
| T1119, T1005, T1552 | Credential and file collection into the temp folder | Q5, Q6 | Q5 | Partial | Partial: same limit as CrowdStrike | Partial: same limit as CrowdStrike |
| T1560 | Archive of the working folder | Q5, Q6 | GAP | GAP: same limit as CrowdStrike | GAP: same limit as CrowdStrike | |
| T1102 | Solana RPC endpoint lookups | Q3, Q4 | Q3 | Good | Good, with assumptions | Good, with assumptions |
| T1041 | Upload to hard-coded address | Q2, Q9 | Partial | Partial: same limit as CrowdStrike | Partial: same limit as CrowdStrike |
Gaps. The account takeover and release swap are outside endpoint telemetry (N4). The stealer's file reads and temp-folder creation are in-process and not logged by the sensor, so only the execution itself, the DNS lookup and the upload connection are visible. The other swapped archives have no published hash, so Q1 covers only the five binaries, one archive and the five advisory sample links.
Validation. Run Section 11 before promoting: telemetry ready, 14-day baseline, safe positive tests, then staged promotion.
Hunt Summary Ticket
TITLE: OVHcloud CLI release-asset backdoor (v0.11.0 to v0.15.0, 2026-10-19)
SEVERITY: High. Supply-chain compromise stealing developer and operator secrets; exposure
window closed (12:47 to 14:21 UTC on 2026-10-19), roughly 250 downloads per OVHcloud
SCOPE: Developer workstations and CI runners on macOS, Linux (x86_64, arm64) and Windows x86_64
that downloaded or ran the CLI in the window; any secret readable on those machines
HYPOTHESIS: A hijacked contributor GitHub account replaced release assets through the Releases
API because immutable releases was off; the swapped binary copies credentials into an
envchecker temp folder and uploads them, using Solana RPC endpoints.
QUERIES: 9 CQL queries (Q1-Q9). Q1, Q2, Q3 high confidence; Q3 and Q5 promotable to Custom IOA;
hashes and the upload address go to IOC Management.
DO FIRST: 1) Run Q7 and Q8 for 2026-10-19. 2) Run Q1, Q2, Q3. 3) Check N1 on any host that ran
the CLI. 4) Rotate secrets (P5) from a clean machine. 5) Enable immutable releases.
FINDINGS: [fill in after the hunt]
GAPS: GitHub account takeover and release changes are outside endpoint telemetry. File reads
and the temp-folder creation are not visible in process, network or DNS telemetry.
ACTIONS: Rotate secrets; reinstall v0.16.0+ with attestation check; pin hashes in CI; immutable
releases; remove personal accounts from release repos; block Solana RPC on build hosts.
OWNER: REPLACE_WITH_OWNER
VERSION: v0.1 (Draft) 2026-10-24Changelog
References
| Tier | Source | Used for | Access date |
|---|---|---|---|
| T1 | OVHcloud: ovhcloud-cli security incident, October 19th, 2026 (GHSA-862f-55f5-vmvw) | Timeline, affected versions, MD5s, upload address, Solana endpoints, targeted files, root cause, remediation. | 2026-10-24 |
| T1 | GitHub issue #300: Release assets of v0.13.0 to v0.15.0 replaced | First-hand hashes of the original and swapped archive, build metadata, maintainer responses. | 2026-10-24 |
| T2 | OpenSourceMalware: OVHcloud CLI supply chain compromise | Server-side timeline, replaced-asset table, how to check a binary, attribution inference. | 2026-10-24 |
| T2 | The Hacker News: Hackers Hijack Five OVHcloud CLI Releases | Independent exposure analysis, Homebrew correction, caution about +dirty. | 2026-10-24 |
| T2 | Help Net Security: Info-stealing malware slipped into OVHcloud CLI releases | Corroboration of versions, times, immutability root cause. | 2026-10-24 |
Generated 2026-10-24 by HuntPack. Defensive content only.