В начале 2025-го ко мне за месяц пришли три сайта с одинаковой картиной: нагрузка на CPU в полке, в ps aux — процесс с невнятным именем, в корне сайта — пара свежих PHP-файлов, которых там быть не должно. Все три — Битрикс с решением Aspro, все три давно не обновлялись.
Разбор ниже — это то, что нужно сделать по факту компрометации. Начну с главного тезиса, который обычно не нравится клиенту: вычистить взломанный сайт «точечно» нельзя. Если на сервере отработал веб-шелл, вы не знаете, что ещё было изменено, — файлы могли быть подменены, задания в cron добавлены, ключи в authorized_keys дописаны. Всё, что ниже, — это либо аварийная мера на время подготовки чистого развёртывания, либо действия после него.
Описание уязвимости
Серия целевых взломов сайтов на платформе 1С-Битрикс с модулем Aspro. Уязвимость затрагивает пользователей, которые не обновили систему до версии Aspro: Next 1.9.9.
Проблема заключается в недостаточной проверке пользовательского ввода и использовании небезопасной PHP-функции unserialize, позволяющей внедрять вредоносный код через POST-запросы.
Точки входа для атак
Злоумышленники использовали три уязвимых скрипта в директории /ajax/:
reload_basket_fly.phpshow_basket_fly.phpshow_basket_popup.php
Механизм атаки: Через уязвимости злоумышленники загружали веб-шеллы, которые подгружали майнеры криптовалюты (XMRig). Это позволяло выполнять произвольные команды на сервере.
Почему это работает
unserialize() восстанавливает не просто данные, а PHP-объекты, вызывая при этом магические методы (__wakeup, __destruct). Если в коде проекта или в любой подключённой библиотеке найдётся класс с подходящим деструктором — атакующий выстраивает из таких классов цепочку (POP-chain) и получает выполнение произвольного кода, ничего не «внедряя» в буквальном смысле. Именно поэтому unserialize над данными из запроса — это не «недостаточная валидация», а прямое выполнение кода на сервере по запросу пользователя.
Обновление модуля важнее правок кода. Всё, что ниже, закрывает конкретные известные точки входа. Опубликованные патчи вендора закрывают их же плюс те, о которых вы не знаете. Правка файлов руками — это то, что делают в момент, когда сайт уже под атакой, а окно для обновления ещё не согласовано.
Исправления кода
Файлы корзины (ajax)
В файлах:
/ajax/reload_basket_fly.php/ajax/show_basket_fly.php/ajax/show_basket_popup.php/bitrix/wizards/aspro/max/site/public/ru/ajax/reload_basket_fly.php/bitrix/wizards/aspro/max/site/public/ru/ajax/show_basket_fly.php/bitrix/wizards/aspro/max/site/public/ru/ajax/show_basket_popup.php
Замените:
$arParams = unserialize(urldecode($_REQUEST["PARAMS"]));На:
$arParams = json_decode($_REQUEST["PARAMS"]);Замена снимает главную проблему: json_decode не создаёт объекты произвольных классов и не вызывает магических методов, худшее его последствие — null при некорректном вводе. Но обратите внимание на два момента.
Во-первых, это смена формата данных: фронтенд, который раньше слал сериализованную строку, должен теперь слать JSON, иначе корзина просто перестанет работать. Проверьте это до выкладки на прод, а не после.
Во-вторых, параметры компонента по-прежнему приходят от клиента. Даже с json_decode атакующий управляет тем, какие параметры получит компонент корзины. Правильная архитектура — не принимать параметры извне вообще, а хранить их на сервере и передавать в запросе только идентификатор. Замена unserialize на json_decode закрывает RCE, но не превращает эндпоинт в безопасный.
Файлы каталога
В файлах:
/include/mainpage/comp_catalog_ajax.php/bitrix/wizards/aspro/max/site/public/ru/include/mainpage/comp_catalog_ajax.php
Замените:
$arIncludeParams = ($bAjaxMode ? $_POST["AJAX_PARAMS"] : $arParamsTmp);
$arGlobalFilter = ($bAjaxMode ? unserialize(urldecode($_POST["GLOBAL_FILTER"])) : ($_GET['GLOBAL_FILTER'] ? unserialize(urldecode($_GET['GLOBAL_FILTER'])) : array()));
$arComponentParams = unserialize(urldecode($arIncludeParams));На:
if ($_POST["AJAX_PARAMS"] && !is_array(unserialize(urldecode($_POST["AJAX_PARAMS"]), ["allowed_classes" => false]))) {
header('HTTP/1.1 403 Forbidden');
$APPLICATION->SetTitle('Error 403: Forbidden');
echo 'Error 403: Forbidden';
require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/epilog_after.php');
die();
}
$arIncludeParams = ($bAjaxMode ? $_POST["AJAX_PARAMS"] : $arParamsTmp);
$arGlobalFilter = ($bAjaxMode ? unserialize(urldecode($_POST["GLOBAL_FILTER"]), ["allowed_classes" => false]) : array());
$arComponentParams = unserialize(urldecode($arIncludeParams), ["allowed_classes" => false]);Ключевое здесь — второй аргумент ["allowed_classes" => false]: с ним unserialize откажется создавать объекты и вернёт вместо них __PHP_Incomplete_Class, что и обезвреживает POP-цепочки. Заметьте, что в проверке unserialize вызывается дважды подряд — сначала для валидации, потом для результата. Для горячего фикса под атакой это приемлемо, для постоянного кода — вынесите результат в переменную.
Компонент быстрого заказа
В файле /bitrix/components/aspro/oneclickbuy.max/script.php (около строки 797):
Замените:
$res = CSaleOrderProps::GetList(array(), array('@CODE' => unserialize($_POST["PROPERTIES"]), 'PERSON_TYPE_ID' =>$personType));На:
$res = CSaleOrderProps::GetList(array(), array('@CODE' => unserialize($_POST["PROPERTIES"], ['allowed_classes' => false]), 'PERSON_TYPE_ID' =>$personType));Индикаторы компрометации
Поиск по хэшам ловит только известные образцы. Меняется один байт — меняется MD5, и файл перестаёт находиться. Хэши ниже — это отправная точка расследования («заражение точно было»), а не критерий чистоты: пустой результат поиска ничего не доказывает.
Вредоносные файлы в разобранных случаях имели такие MD5-хэши:
07a3fe9875d3a8b7c57874c4cc509929
50b7604d856f36983b9bb3066f894f3fПоиск вредоносных файлов
# Поиск по хэшу
find /home/bitrix/www -type f -exec md5sum {} + | grep '^07a3fe9875d3a8b7c57874c4cc509929'
find /home/bitrix/www -type f -exec md5sum {} + | grep '^50b7604d856f36983b9bb3066f894f3f'
# Поиск eval(base64 конструкций
grep -rn "eval(base64" /home/bitrix/wwwГораздо надёжнее хэшей работает поиск по времени изменения и сверка с системой контроля версий. Файлы, изменившиеся в окне атаки, — первое, что стоит просмотреть:
# PHP-файлы, изменённые за последние 30 дней
find /home/bitrix/www -name '*.php' -mtime -30 -printf '%TY-%Tm-%Td %p\n' | sort
# Расхождения с эталоном (если проект в git)
git status --porcelain && git diff --statПроект под git превращает расследование из гадания в одну команду — это, пожалуй, лучший аргумент за то, чтобы держать в репозитории и bitrix/, и решение вендора.
Скрипт массового исправления
Когда сайтов на одном решении несколько, править шесть файлов руками на каждом — верный способ где-нибудь ошибиться. Скрипт ниже делает две вещи: применяет замены по списку и удаляет файлы с известными хэшами.
Скрипт не делает бэкап и не спрашивает подтверждения. Он пишет прямо в файлы и удаляет то, что нашёл. Снимите копию файлов и базы до запуска — и запускайте из CLI, а не через веб: файл в DOCUMENT_ROOT, умеющий удалять файлы, на взломанном сайте выглядит несколько иронично.
<?php
/**
* Скрипт для автоматической чистки сайта от известных вредоносных файлов
* и исправления уязвимых участков кода.
*
* ВАЖНО: Сделайте резервную копию файлов перед запуском!
*/
$documentRoot = '.';
// Файлы с одиночной заменой
$replacements = [
'/ajax/reload_basket_fly.php' => [
'search' => '$arParams = unserialize(urldecode($_REQUEST["PARAMS"]));',
'replace' => '$arParams = json_decode($_REQUEST["PARAMS"]);'
],
'/ajax/show_basket_fly.php' => [
'search' => '$arParams = unserialize(urldecode($_REQUEST["PARAMS"]));',
'replace' => '$arParams = json_decode($_REQUEST["PARAMS"]);'
],
'/ajax/show_basket_popup.php' => [
'search' => '$arParams = unserialize(urldecode($_REQUEST["PARAMS"]));',
'replace' => '$arParams = json_decode($_REQUEST["PARAMS"]);'
],
];
// Функция замены в файле
function updateFile($filePath, $search, $replace) {
if (file_exists($filePath)) {
$content = file_get_contents($filePath);
if (strpos($content, $search) !== false) {
$newContent = str_replace($search, $replace, $content);
if (file_put_contents($filePath, $newContent) !== false) {
echo "[OK] Обновлён: $filePath\n";
} else {
echo "[FAIL] Не удалось записать: $filePath\n";
}
} else {
echo "[SKIP] Фрагмент не найден (уже исправлено?): $filePath\n";
}
} else {
echo "[SKIP] Файл отсутствует: $filePath\n";
}
}
// Обрабатываем файлы
foreach ($replacements as $relativePath => $data) {
$filePath = $documentRoot . $relativePath;
updateFile($filePath, $data['search'], $data['replace']);
}
// Удаление вредоносных файлов по MD5
$maliciousHashes = [
"07a3fe9875d3a8b7c57874c4cc509929",
"50b7604d856f36983b9bb3066f894f3f"
];
function scanAndRemoveMaliciousFiles($dir, $maliciousHashes) {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir)
);
foreach ($iterator as $file) {
if ($file->isFile()) {
$filePath = $file->getPathname();
$hash = md5_file($filePath);
if (in_array($hash, $maliciousHashes)) {
if (unlink($filePath)) {
echo "[CLEAN] Удалён вредоносный файл: $filePath\n";
} else {
echo "[FAIL] Не удалось удалить: $filePath\n";
}
}
}
}
}
echo "\nПоиск вредоносных файлов по хэшам...\n";
scanAndRemoveMaliciousFiles($documentRoot, $maliciousHashes);
echo "\nГотово. Проверьте вывод: [SKIP] на файлах, которые должны были исправиться, — повод посмотреть их руками.\n";Запуск:
php -f autofix.phpПорядок действий при компрометации
Последовательность важна: если начать с чистки файлов, атакующий вернётся через ещё живую сессию или незакрытую точку входа, и вы будете чистить по кругу.
- Закрыть точку входа. Обновить решение или внести правки выше. Пока эндпоинт принимает
unserializeиз запроса, всё остальное бессмысленно. - Отозвать все доступы. Пароли администраторов Битрикс, FTP/SSH, БД, API-ключи платёжных систем и интеграций. Плюс
authorized_keys— дописанный туда ключ переживает любую смену пароля. - Инвентаризовать изменения. Поиск по времени модификации, сверка с git, проверка cron (
crontab -lдля каждого пользователя,/etc/cron.*), проверка.htaccessна добавленные редиректы, проверкаb_userна новых администраторов. - Остановить майнер — но только после инвентаризации:
kill -9уничтожает и процесс, и возможность посмотреть, откуда он запущен.bashps aux | grep -i xmrig ls -l /proc/<PID>/exe # откуда реально запущен процесс ls -l /proc/<PID>/cwd kill -9 <PID> - Развернуть чистую копию. Файлы ядра и решения — из дистрибутива нужной версии, свой код — из репозитория, данные — из базы (её тоже проверив на посторонние скрипты в свойствах и шаблонах писем).
- Настроить мониторинг целостности файлов, чтобы следующая попытка была видна на первом же изменении.
Про пункт 5. Клиент почти всегда предлагает «просто удалить вирусы и жить дальше», и почти всегда после этого сайт заражается повторно — потому что где-то остался второй веб-шелл, который никто не нашёл. Развёртывание с нуля из проверенных источников — единственный способ утверждать, что сервер чист, а не надеяться на это.
Итоги
Технически история про unserialize — это классическая PHP object injection: небезопасная десериализация данных из запроса даёт выполнение кода. Практически же вывод шире и скучнее: сайты ломают не потому, что уязвимость была неизвестна, а потому, что патч вышел, а обновление никто не поставил.
Что стоит сделать сегодня, если у вас Битрикс с готовым решением:
- проверить версию решения и запланировать обновление (не «когда-нибудь», а с датой);
- поискать
unserializeв собственном коде —grep -rn "unserialize(" /local/— и убедиться, что ни в один вызов не приходит$_GET,$_POSTили$_COOKIE; - завести проект в git, если его там ещё нет: расследование инцидента без эталона занимает дни вместо минут.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.