Supply chain атака на OptinMonster: как один украденный ключ открыл доступ к 1,2 млн сайтов WordPress
Supply chain атака на OptinMonster: как один украденный ключ открыл доступ к 1,2 млн сайтов WordPress
Представьте: ваш WordPress полностью обновлён, все плагины актуальны, никаких уязвимостей в вашем коде. Администратор заходит в панель управления — обычный рабочий день. Несколько часов спустя на сайте появляется скрытая учётная запись с правами администратора и бэкдор-плагин с веб-шеллом. Никакого взлома вашего сервера не было. Проблема пришла от доверенного инструмента, которому вы платите деньги.
12 июня 2026 года злоумышленники провели атаку на цепочку поставок, затронувшую три популярных WordPress-плагина компании Awesome Motive: OptinMonster (более 1,2 млн активных установок), TrustPulse и PushEngage. Атака не эксплуатировала уязвимости в самих плагинах — она ударила выше по цепочке, по CDN-инфраструктуре, через которую плагины раздают свой JavaScript клиентам. Первыми её обнаружили исследователи Sansec, специализирующиеся на безопасности e-commerce. Детальный анализ впоследствии опубликовал Patchstack.
Это не CVE с номером и CVSS-баллом. Это атака на доверие: злоумышленники использовали тот факт, что сайты доверяют JavaScript, который им раздаёт поставщик плагина. И это доверие сработало против них.
КАК ЭТО СТАЛО ВОЗМОЖНЫМ
Атакующие не ломали OptinMonster. Они не искали уязвимость в коде плагина, не брутфорсили API, не ковырялись в WordPress-ядре. Они зашли через маркетинговый сайт самой компании — обычный WordPress с UpdraftPlus, на котором висела известная уязвимость. Взломать маркетинговый сайт производителя плагинов несравнимо проще, чем взломать сам плагин. И награда там оказалась непропорционально огромной.
На этом маркетинговом сервере лежал API-ключ от CDN-аккаунта компании. Именно через этот CDN Awesome Motive раздаёт JavaScript-файлы всем клиентам — на все 1,2 млн сайтов с OptinMonster. Ключ был один. Уровень доступа — полный. Атакующий не взламывал каждый сайт отдельно: он просто изменил файл в одном месте, и CDN сам развёз заражённый скрипт по всем клиентам. Это и есть supply chain атака в чистом виде — вместо миллиона замков взломали одну мастерскую, которая их делает.
Атакующие заменили легитимные JavaScript SDK плагинов вредоносными версиями на следующих CDN-доменах:
a.omappapi.com/app/js/api.min.js (OptinMonster)
a.opmnstr.com/app/js/api.min.js (OptinMonster)
a.optnmstr.com/app/js/api.min.js (OptinMonster)
a.trstplse.com/app/js/api.min.js (TrustPulse)
clientcdn.pushengage.com/sdks/pushengage-web-sdk.js (PushEngage)
Вредоносный код не заменял оригинальный — он был дописан в конец легитимного минифицированного SDK. Плагин продолжал работать в штатном режиме, а вредоносная логика выполнялась параллельно. Именно поэтому обычный мониторинг не поднял тревогу сразу.
КАК РАБОТАЛ ВРЕДОНОСНЫЙ КОД
Скрипт был написан с хирургической точностью — он активировался только в нужных условиях и избегал обнаружения на нескольких уровнях.
Первым делом он проверял среду выполнения: искал navigator.webdriver, маркеры headless-браузеров (window._phantom, window.__nightmare), нулевые размеры окна. Это защита от автоматических сканеров и исследователей. Затем убеждался, что работает именно в контексте WordPress-администратора: проверял наличие admin bar, путей /wp-admin/ и cookie wordpress_logged_in_. Если администратора нет — просто останавливался. После успешного срабатывания ставил метку в localStorage, чтобы не повторяться на том же браузере в течение 24 часов.
Найдя администратора, скрипт собирал всё необходимое для действий от его имени: определял WordPress root и путь к admin, извлекал валидный REST nonce из wpApiSettings, через admin-ajax.php?action=rest-nonce или со страницы добавления пользователей. Имея сессию администратора и валидный nonce, все последующие запросы выглядели на сетевом уровне как легитимные действия самого администратора.
Создание вредоносного администратора происходило через четыре параллельных метода — скрипт перебирал их до первого успеха:
// Метод 1: REST API
POST /wp-json/wp/v2/users
{"username": "dev_3m6nyp", "email": "[email protected]", "roles": ["administrator"]}
// Метод 2: форма WordPress
POST /wp-admin/user-new.php?action=createuser
// Метод 3: AJAX
POST /wp-admin/admin-ajax.php (с тем же payload)
// Метод 4: скрытый iframe 1x1px загружающий user-new.php
Скрипт создавал как фиксированную учётную запись developer_api1 с адресом [email protected], так и рандомизированные аккаунты вида dev_xxxxxx / [email protected]. У него даже был встроенный словарь сообщений «пользователь уже существует» на разных языках — чтобы корректно определять успех в локализованных WordPress-установках.
Получив admin-доступ, скрипт загружал ZIP-архив с бэкдором с C2-сервера и устанавливал его через POST /wp-admin/update.php?action=upload-plugin. Бэкдор-плагин менял названия: в разных наблюдениях он маскировался под «Content Delivery Helper» (content-delivery-helper, версия 2.7.1) или «Database Optimizer» (database-optimizer, версия 2.9.4). При этом он скрывал себя из списка плагинов, из очереди обновлений и из логов активности WordPress — обнаружить его через dashboard было практически невозможно.
Установленный бэкдор открывал два канала удалённого выполнения кода. Через параметр ?developer_api1_fm работал веб-шелл, исполнявший system($_POST['cmd']). Через параметр developer_api1_eval выполнялся произвольный base64-декодированный PHP-код. Собранные данные XOR-шифровались ключом jX9kM2nP4qR6sT8v, кодировались в base64 и отправлялись на домен tidio.cc (умышленная имитация легитимного tidio.com) с цепочкой fallback: navigator.sendBeacon → fetch → XMLHttpRequest → image pixel.
РЕАЛЬНАЯ ЦЕПОЧКА АТАКИ
Масштаб подтвердила статистика Patchstack. За 36 часов — 14–15 июня — их WAF-правило заблокировало 271 попытку создания вредоносных администраторов на 13 сайтах с 81 уникального IP-адреса. Распределение по векторам: 263 запроса через REST API /wp-json/wp/v2/users, 5 через форму /wp-admin/user-new.php, 3 через /wp-admin/admin-ajax.php.
Особенно показательна природа трафика: 81 разный IP-адрес, примерно 60% мобильные браузеры (Android/Samsung), остальные — Windows, macOS, Linux. Это не централизованная атака с серверов злоумышленника. Это браузеры реальных администраторов WordPress, которые стали невольными орудиями атаки. Именно поэтому WAF-у так сложно было различить вредоносные запросы и легитимные: они несли валидную сессию и валидный nonce.
Домен tidio.cc был зарегистрирован ещё 28 апреля 2026 года — за полтора месяца до атаки. Тогда же получен TLS-сертификат. Атака готовилась заблаговременно.
ХРОНОЛОГИЯ
28 апреля 2026: регистрация C2-домена tidio.cc и получение TLS-сертификата — подготовка инфраструктуры.
12 июня 2026, 22:17 UTC: вредоносный код впервые зафиксирован в api.min.js OptinMonster и TrustPulse.
12 июня 2026, 22:42 UTC: последнее подтверждённое присутствие вредоносного кода на CDN OptinMonster и TrustPulse. Окно заражения — около 25 минут на основной CDN, но из-за кеширования на edge-серверах ряд узлов продолжал раздавать заражённый скрипт дольше.
13 июня 2026, 19:02 UTC: SDK PushEngage всё ещё раздаёт заражённый код с части CDN-узлов.
14 июня 2026: вредоносный код удалён из CDN PushEngage; Awesome Motive публикует официальное уведомление об инциденте.
14–15 июня 2026: Patchstack активно блокирует попытки эксплуатации на защищённых сайтах.
ПОЧЕМУ ЭТО ВАЖНО
Всё обновлено. Плагины актуальны. Сервер пропатчен. И всё равно скомпрометирован. Именно это делает данную атаку неприятной — она бьёт не в слабое место вашей защиты, а в слепое. Стандартная модель «обновляй плагины — будет безопасно» здесь просто не работает, потому что уязвимость была не в плагине, а в инфраструктуре его доставки. Вы не могли её увидеть, не могли от неё защититься обновлением и не могли её обнаружить стандартными средствами.
Есть ещё один момент, который важно понять: запросы на создание вредоносного администратора генерировал не сервер атакующего — их генерировал браузер вашего собственного администратора. С его сессией, с его nonce, с его cookies. WordPress видел легитимного пользователя, выполняющего легитимное действие. Именно поэтому большинство WAF-правил промолчали: разве можно блокировать администратора, создающего пользователя? Оказывается — можно, но только если точно знаешь сигнатуры конкретного атакующего. Patchstack именно так и сделал, но уже после того, как атака была в разгаре.
Если на вашем сайте работает WooCommerce — ставки выше вдвойне. Данные платёжных карт, адреса, история заказов, токены подписок. Бэкдор с полным RCE на таком сайте — это уже не технический инцидент. Это утечка данных со всеми вытекающими: уведомление регулятора, уведомление клиентов, репутационный ущерб.
КАК ПРОВЕРИТЬ СВОЙ САЙТ
Если на вашем сайте был активен OptinMonster, TrustPulse или PushEngage, и администратор заходил в панель управления в период 12–14 июня 2026 года — проверка обязательна. Важно: панель управления WordPress ненадёжна для этой проверки, потому что бэкдор активно скрывает себя из dashboard. Он исключает себя из списка плагинов, из очереди обновлений и из логов активности. Все ключевые проверки — только на уровне файловой системы сервера.
Первым делом проверьте список администраторских аккаунтов. Если есть WP-CLI, это делается одной командой — она выводит логин и email всех пользователей с ролью administrator, отсортированных по дате регистрации, новые вверху:
wp user list --role=administrator --fields=user_login,user_email
Команду нужно запускать от имени пользователя, которому принадлежат файлы WordPress — иначе WP-CLI не сможет прочитать wp-config.php. Если получили ошибку Permission denied, узнайте владельца и запустите через sudo -u:
# Узнать владельца файлов WordPress
ls -la /var/www/html/wp-config.php
# Запустить от имени владельца (обычно www-data)
sudo -u www-data wp user list --role=administrator --fields=user_login,user_email --path=/var/www/html
Если WP-CLI нет, то же самое можно сделать напрямую через MySQL. Запрос джойнит таблицу пользователей с таблицей метаданных и фильтрует тех, у кого в capabilities прописана роль administrator:
SELECT user_login, user_email FROM wp_users
JOIN wp_usermeta ON wp_users.ID = wp_usermeta.user_id
WHERE meta_key = 'wp_capabilities'
AND meta_value LIKE '%administrator%'
ORDER BY user_registered DESC;
Ищите в выводе developer_api1 / [email protected] и аккаунты с именами вида dev_xxxxxx — шесть случайных символов после dev_. Если нашли — это прямой IoC.
Дальше проверяйте файловую систему. Флаг -la в ls показывает скрытые файлы и директории (начинающиеся с точки), а также права доступа и дату последнего изменения — это поможет заметить свежесозданные папки:
ls -la /var/www/html/wp-content/plugins/
Бэкдор устанавливается под именами content-delivery-helper или database-optimizer, но название ротируется — поэтому смотрите на любые незнакомые директории, особенно созданные в период 12–14 июня.
Даже если по названию ничего подозрительного нет, поищите сигнатуры бэкдора прямо в коде. Флаг -r запускает рекурсивный поиск по всем файлам в указанной директории. Три команды ищут: имя параметра веб-шелла, имя параметра для выполнения произвольного PHP, и XOR-ключ шифрования — если хоть одна что-то вернула, бэкдор есть:
grep -r "developer_api1_fm" /var/www/html/wp-content/plugins/
grep -r "developer_api1_eval" /var/www/html/wp-content/plugins/
grep -r "jX9kM2nP4qR6sT8v" /var/www/html/wp-content/plugins/
Если сайт был активен в период атаки — дополнительно заблокируйте C2-домен, чтобы отрезать любые возможные обратные соединения. Первый вариант через nftables добавляет правило в цепочку output, которое сбрасывает все исходящие пакеты на IP 84.201.6.54. Второй вариант через /etc/hosts перенаправляет все DNS-запросы к tidio.cc на несуществующий адрес — работает как резервная мера если nftables не используется:
# Вариант 1: nftables — блокировка на уровне пакетов
nft add rule inet filter output ip daddr 84.201.6.54 drop
# Вариант 2: /etc/hosts — блокировка на уровне DNS
echo "0.0.0.0 tidio.cc" | sudo tee -a /etc/hosts
Если нашли хоть один из этих индикаторов — сайт считается полностью скомпрометированным. Удаление бэкдора и вредоносного аккаунта это только начало: при наличии RCE атакующий мог разместить дополнительные точки входа в любом месте файловой системы. Нужно ротировать всё: пароли администраторов, API-ключи, учётные данные базы данных, а также security keys и salts в wp-config.php — именно там хранятся соли для хеширования сессионных cookies, и их смена инвалидирует все текущие сессии.
КАК ЗАЩИТИТЬСЯ ЗАРАНЕЕ
Эта атака уже позади, но механика supply chain через CDN никуда не делась. Вот что реально снижает риск на вашем сервере прямо сейчас.
Мониторинг создания новых администраторов — первое, что нужно настроить. WordPress не уведомляет вас по умолчанию, когда появляется новая учётная запись с правами admin. Это можно исправить через хук user_register — добавьте следующий код в functions.php или mu-plugin. Он срабатывает при каждой новой регистрации, проверяет роль и отправляет письмо, если это administrator. Новый администратор — событие достаточно редкое, чтобы каждый такой случай требовал вашего внимания немедленно:
add_action('user_register', function($user_id) {
$user = get_userdata($user_id);
if (in_array('administrator', $user->roles)) {
wp_mail(
get_option('admin_email'),
'Новый администратор: ' . $user->user_login,
'Логин: ' . $user->user_login . "
Email: " . $user->user_email
);
}
});
Контроль целостности файлов через AIDE фиксирует состояние файловой системы в момент чистой установки и при каждом последующем запуске сравнивает с эталоном. Бэкдор-плагин, появившийся в wp-content/plugins/, будет замечен при следующей проверке — даже если он скрывается из dashboard. Настройте AIDE на ежедневный запуск через systemd timer и отправку отчёта на email:
# Инициализация базы эталонных состояний
sudo aide --init
sudo mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
# Проверка текущего состояния относительно эталона
sudo aide --check
Двухфакторная аутентификация для всех администраторов WordPress не остановила бы эту конкретную атаку — скрипт использовал уже активную сессию. Но она закрывает смежные векторы: перехват паролей, credential stuffing, фишинг. Supply chain атаки редко приходят в одиночку — часто они сопровождаются попытками получить учётные данные другими путями.
Наконец, регулярно проверяйте список администраторов вручную. Раз в неделю запускайте sudo -u www-data wp user list --role=administrator и сверяйте с тем, что должно быть. Это занимает тридцать секунд и позволяет поймать нежелательные аккаунты задолго до того, как они нанесут реальный ущерб.
КАК ПРЕДОТВРАТИТЬ ПОДОБНОЕ В БУДУЩЕМ
Эта атака обнажила проблему, которую трудно устранить полностью, но можно существенно ограничить. Первый урок: CDN-ключи с правами на запись не должны храниться на серверах, которые сами работают на WordPress или другом CMS. Маркетинговый сайт — это не защищённая инфраструктура. Секреты с prod-доступом там неуместны.
Второй урок: мониторинг создания администраторских аккаунтов должен быть частью вашей системы оповещений. Новый администратор WordPress — это событие, о котором вы должны узнать немедленно, не из еженедельного отчёта. Это можно настроить через плагины безопасности или напрямую через хуки WordPress с отправкой уведомления на email.
Третий урок: Subresource Integrity (SRI) — механизм, позволяющий браузеру проверять хеш внешнего скрипта перед его выполнением. Если бы тег <script> включал атрибут integrity с SHA-256 хешем ожидаемого файла, браузер отказался бы выполнять изменённый SDK. К сожалению, большинство SaaS-плагинов не реализуют SRI для своих SDK — именно потому, что они регулярно обновляют эти файлы. Но для критически важных внешних скриптов стоит рассмотреть этот механизм там, где это применимо.
Четвёртый урок: двухфакторная аутентификация для всех администраторов WordPress замедлила бы не эту атаку (скрипт использовал уже активную сессию), но снизила бы риски от других сценариев компрометации учётных данных, которые часто идут в паре с supply chain атаками.
ИНДИКАТОРЫ КОМПРОМЕТАЦИИ
Вредоносные учётные записи:
developer_api1 / [email protected] (фиксированная)
dev_xxxxxx / [email protected] (рандомизированные, 6 символов)
Бэкдор-плагины (название меняется, проверяйте диск):
content-delivery-helper "Content Delivery Helper" v2.7.1
database-optimizer "Database Optimizer" v2.9.4
Параметры веб-шелла:
?developer_api1_fm (веб-шелл, выполняет system($_POST['cmd']))
developer_api1_eval (выполняет base64-декодированный PHP)
C2-инфраструктура:
tidio.cc (IP: 84.201.6.54, AS214036 Ultahost)
Пути: /cdn-cgi/p, /cdn-cgi/b, /cdn-cgi/l, /cdn-cgi/pe-p, /cdn-cgi/pe-b, /cdn-cgi/pe-l
Ключ шифрования малвари:
jX9kM2nP4qR6sT8v
Заражённые CDN-файлы (уже очищены, но для ретроспективного анализа логов):
a.omappapi.com/app/js/api.min.js
a.opmnstr.com/app/js/api.min.js
a.optnmstr.com/app/js/api.min.js
a.trstplse.com/app/js/api.min.js
clientcdn.pushengage.com/sdks/pushengage-web-sdk.js
ВЫВОДЫ
Атака на OptinMonster неудобна тем, что у неё нет простого урока в стиле «обновляйте плагины» или «используйте сильные пароли». Сайты, которые пострадали, делали всё правильно. Проблема была не в них — она была у вендора, в месте, куда у владельцев сайтов нет никакого доступа и никакой видимости.
Для Awesome Motive это учебник по тому, почему production-секреты нельзя хранить на инфраструктуре с пониженным уровнем защиты. Маркетинговый сайт на WordPress с UpdraftPlus и API-ключом от CDN с правами на запись — это была бомба с таймером. Рвануло через уязвимость в стороннем плагине, которую кто-то нашёл и использовал именно с прицелом на это.
Для вас как администратора WordPress — это напоминание о том, что ваш периметр безопасности шире, чем вы думаете. Он включает каждый внешний скрипт, каждый CDN, каждого SaaS-вендора, которому ваш сайт доверяет в браузере посетителя. Мониторинг создания административных аккаунтов, файловый контроль через AIDE, WAF с сигнатурными правилами — это не перестраховка. Это единственное, что реально поможет, когда проблема придёт не из вашего кода, а из чужого.
