Как в официальный CLI OVHcloud подложили стилер и почему спас только прописанный хеш

19 октября в официальном CLI OVHcloud полтора часа раздавался стилер. Первым это заметил клиент OVH, у которого в CI был жёстко прописан SHA-256 одного архива. Сама OVH узнала о подмене из его отчёта: через час после подмены сборка у клиента упала, и он завёл issue #300.
Про этот случай уже написали французские СМИ и OpenSourceMalware. Я прошёл по их следам сам: GitHub API, репозитории взломанного аккаунта, блокчейн Solana. Нашлось кое-что, чего в пересказах нет.
Что произошло
ovhcloud — Go-утилита, которой управляют серверами, VPS, Public Cloud и доменами OVH из терминала. Ключи API она хранит в ~/.ovh.conf или в переменных окружения, то есть на машине разработчика или в CI.
С 15:47 до 16:12 по Москве (12:47–13:12 UTC) через аккаунт HassenBF в пяти уже выпущенных релизах, с v0.11.0 по v0.15.0, заменили бинарники. Новых версий не появилось и тегов не добавилось. GitHub Actions не запускались. Файлы просто перезалили через Releases API поверх старых, последним шёл checksums.txt, чтобы хеши на странице релиза совпадали с подменой.
Подменили не всё. Из десяти файлов релиза заменены сборки для macOS (обе), Linux x86_64 и arm64, Windows x86_64 и файл с хешами. Сборки для i386, Windows arm64 и WASM не тронули — самые редкие платформы. Это видно и сейчас: у v0.11.0–v0.15.0 на странице осталось ровно по четыре файла, остальные шесть OVH удалила.
В 17:21 по Москве (14:21 UTC) подменённые файлы удалили. Вечером вышла v0.16.0, уже с включёнными у GitHub immutable releases и аттестацией релиза. Октав Клаба, основатель OVH, написал в X, что злоумышленник воспользовался GitHub-аккаунтом одного из разработчиков, а бинарник ищет на диске секреты. Качество бинарника он назвал посредственным, но рабочим.
Почему спас только прописанный хеш
Проверка по checksums.txt из того же релиза здесь бесполезна: файл с хешами подменили вместе с архивами. Официальный install.sh не проверяет хеши вообще:
$ grep -ci 'sha256\|shasum\|hash' install.sh
0
Скрипт берёт архив по адресу releases/download/<тег>/<файл> и сразу ставит. В этом окне малварь получили все, кто запускал curl … install.sh | sh, скачивал архив со страницы релиза или тянул его в CI без своего хеша.
С Homebrew вышло иначе. Cask в ovh/homebrew-tap хранит SHA-256 каждого архива, а в окне атаки его никто не правил: последний коммит перед инцидентом — 16 сентября, к выходу v0.15.0. Хеш в нём для Linux_arm64 тот же 5c2beb56…, что был у клиента в CI. Подменённый архив brew должен был отвергнуть по несовпадению хеша.
Не задело и тех, кто собирал из исходников, ставил через go install или брал Docker-образ ovhcom/ovhcloud-cli: его OVH собирает своим конвейером из исходников.
Перезаливка архивов в старом релизе для проекта была штатной операцией. В .goreleaser.yaml так и написано:
release:
mode: keep-existing
replace_existing_artifacts: true
Поймал подмену пользователь alink-tothepast. В его CI был вписан хеш ovhcloud-cli_Linux_arm64.tar.gz версии 0.15.0: 5c2beb56…. После подмены архив стал 3833daff…, и сборка упала. Обновлять хеш вслепую он не стал и вытащил из нового бинарника метаданные сборки:
mod github.com/ovh/ovhcloud-cli v0.15.1-0.20261013083505-5ead8ef9b43d+dirty
build vcs.revision=5ead8ef9b43d354dfb5caadff803b11afc8c5c58
build vcs.modified=true
Go вшивает это в каждый бинарник. Для сравнения я скачал оригинальный архив v0.15.0 под i386, его не подменяли, и посмотрел то же самое в контейнере с Go (сам бинарник при этом не запускается):
mod github.com/ovh/ovhcloud-cli v0.15.0
build -trimpath=true
build vcs.revision=760fd72de93cbea3b507eb36dc0095991e4048e5
build vcs.modified=false
У настоящего релиза версия из тега, коммит 760fd72, на который указывает тег, и чистое дерево. У подмены псевдоверсия от коммита 13 октября, который вышел уже после v0.15.0, и vcs.modified=true: обычный go build в рабочей копии с незакоммиченными правками. Похоже, вредонос дописали локально: в истории репозитория его нет.
Проверить сохранившийся бинарник можно, не запуская его:
go version -m /path/to/ovhcloud
Не запускайте ovhcloud version на подозрительной копии: это и есть запуск стилера. Признак подмены — бинарник, скачанный как официальный релиз (v0.11.0–v0.15.0), в котором стоит псевдоверсия v0.15.1-0.20261013083505-5ead8ef9b43d+dirty. Если вы собирали CLI сами, такая строка ничего не значит.
Аккаунт угнали за семь месяцев до атаки
HassenBF — настоящий контрибьютор OVH: его пулреквесты есть в ovh/ovhcloud-docs и ovh/manager. Публичным членом организации он не значится, коммитов в ovhcloud-cli у него нет. Право на запись в репозиторий у аккаунта тем не менее было: без него Releases API не даёт загружать файлы. Откуда оно взялось, OVH пока не объяснила.
OpenSourceMalware заметили, что 5 марта 2026 года в личных репозиториях этого аккаунта за 42 секунды переписали историю трёх проектов. В два добавили preinstall.js и строчку "preinstall": "node preinstall.js" в package.json, в третьем тот же декодер встроили прямо в src/index.js. Шестого марта так же заразили четвёртый. Почерк GlassWorm: последний коммит подменяют через amend, автора и дату сохраняют и делают force push.
Файл до сих пор лежит в HassenBF/treasure-hunt-game. Я скачал его как текст и ничего не запускал. SHA-256 совпадает с индикатором из разбора OSM (43df42fd…). В редакторе это одна строка на 181 символ: декодер и eval. На самом деле символов в файле 9304. Остальные 9123 — невидимые variation selectors Unicode (U+FE00–U+FE0F и U+E0100–U+E01EF), каждый кодирует один байт. На ревью такой файл выглядит почти пустым.
Байты раскодировались в первый этап: AES-256-CBC, ключ и IV лежат там же открытым текстом. Я расшифровал его через openssl enc -d и получил второй этап, 4407 байт, хеш тоже совпал с разбором. Во втором этапе есть строки ru_RU, ru-RU и Europe/Moscow: на системах с русской локалью и подходящим часовым поясом загрузчик молча выходит. Адрес следующей стадии он берёт из блокчейна.
Тайник в Solana
Загрузчик опрашивает публичные RPC-узлы Solana, берёт транзакции кошелька BjVeAjPrSKFiingBn4vZvghsGj9KCE8AJVtbc9S8o8SC и читает поле memo у самой свежей. В memo лежит JSON с base64-ссылкой на следующую стадию. Удалить такую запись нельзя, а адрес сервера оператор меняет одной транзакцией.
Историю кошелька может посмотреть любой: вызов getSignaturesForAddress у публичного RPC. Я посмотрел:
71 транзакция, в 70 из них есть memo;
первая 27 ноября 2025 года, последняя 29 мая 2026 года;
68 записей со ссылкой на следующую стадию, одна с конфигом (25 февраля,
c2serverна порту 5000 и адрес для DHT) и одна чужая, о ней ниже;за полгода адреса вели на 12 разных IP;
больше всего записей указывают на 45.32.151[.]157 (19) и 45.32.150[.]97 (11), последняя — на 137.184.198[.]91.
Самое забавное нашлось 29 мая. В 19:42 UTC в memo появилась запись {"link":"SSBrbm93IHlvdQ=="}. В base64 это «I know you». Транзакцию оплатил другой кошелёк, не тот, с которого оператор публикует ссылки. Ровно через минуту, в 19:43, оператор записал рабочую ссылку обратно.
Причина в коде загрузчика:
memo = signatures.filter(x => x?.memo)[0].memo
Отправителя он не проверяет и берёт первую попавшуюся memo. Значит, кто угодно может за пару центов подсунуть всем заражённым машинам свою запись. Кто-то так и сделал, похоже исследователь. После 29 мая записей не было: оставшиеся четыре с лишним месяца до атаки на CLI тайник никто не трогал.
Все 12 IP из истории тайника
45.32.151[.]157 19
45.32.150[.]97 11
217.69.2[.]135 10
217.69.11[.]57 9
217.69.11[.]99 6
137.184.198[.]91 5
217.69.0[.]159 3
162.33.177[.]177 2
45.77.60[.]153 1
45.76.44[.]240 1
45.32.147[.]205 1
217.69.11[.]60 1
Делал ли загрузку в CLI тот же оператор, доказать нельзя. Связывает их только общий аккаунт, а публичных копий подменённого бинарника не осталось: OVH удалила файлы, разбора самого стилера пока никто не опубликовал. Известно другое: аккаунт был скомпрометирован ещё в марте, а в октябре у него оставалось право записи в репозиторий популярного CLI.
Если вы ставили CLI 19 октября
Под удар попали машины, где с 15:47 до 17:21 по Москве ставили ovhcloud через install.sh или скачивали архив со страницы релиза: macOS, Linux x86_64/arm64, Windows x86_64. OVH просит сменить все секреты, которые лежат на диске такой машины. Кроме ключей API OVH это SSH-ключи, ~/.kube/config, .npmrc, .netrc, .env, токены других облаков. Менять их стоит с другой машины, а потом уже ставить v0.16.0. У новой версии можно проверить аттестацию:
gh release verify-asset v0.16.0 ovhcloud-cli_Linux_x86_64.tar.gz -R ovh/ovhcloud-cli
Это аттестация релиза: она подтверждает, что файл тот самый, что опубликован в неизменяемом релизе. Как именно его собрали, она не доказывает.
OVH обещает подробный постмортем «в ближайшие дни». Главные вопросы к нему: как атакующий получил доступ к аккаунту HassenBF и что с этим аккаунтом происходило между мартом и октябрём.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.