Главная/Статьи/Взлом сайтов на Aspro: уязвимость unserialize и как защититься

Взлом сайтов на Aspro: уязвимость unserialize и как защититься

Разбор целевых атак на сайты 1С-Битрикс с модулем Aspro. Уязвимые файлы, механизм атаки, исправления кода и автоматический скрипт очистки.

ДМ
Дмитрий Мещеряков
📅 4 февраля 2025 г.📖 8 мин чтения

В начале 2025-го ко мне за месяц пришли три сайта с одинаковой картиной: нагрузка на CPU в полке, в ps aux — процесс с невнятным именем, в корне сайта — пара свежих PHP-файлов, которых там быть не должно. Все три — Битрикс с решением Aspro, все три давно не обновлялись.

Разбор ниже — это то, что нужно сделать по факту компрометации. Начну с главного тезиса, который обычно не нравится клиенту: вычистить взломанный сайт «точечно» нельзя. Если на сервере отработал веб-шелл, вы не знаете, что ещё было изменено, — файлы могли быть подменены, задания в cron добавлены, ключи в authorized_keys дописаны. Всё, что ниже, — это либо аварийная мера на время подготовки чистого развёртывания, либо действия после него.

Описание уязвимости

Серия целевых взломов сайтов на платформе 1С-Битрикс с модулем Aspro. Уязвимость затрагивает пользователей, которые не обновили систему до версии Aspro: Next 1.9.9.

Проблема заключается в недостаточной проверке пользовательского ввода и использовании небезопасной PHP-функции unserialize, позволяющей внедрять вредоносный код через POST-запросы.

Точки входа для атак

Злоумышленники использовали три уязвимых скрипта в директории /ajax/:

  • reload_basket_fly.php
  • show_basket_fly.php
  • show_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

Замените:

php
$arParams = unserialize(urldecode($_REQUEST["PARAMS"]));

На:

php
$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

Замените:

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));

На:

php
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):

Замените:

php
$res = CSaleOrderProps::GetList(array(), array('@CODE' => unserialize($_POST["PROPERTIES"]), 'PERSON_TYPE_ID' =>$personType));

На:

php
$res = CSaleOrderProps::GetList(array(), array('@CODE' => unserialize($_POST["PROPERTIES"], ['allowed_classes' => false]), 'PERSON_TYPE_ID' =>$personType));

Индикаторы компрометации

⚠️ Важно

Поиск по хэшам ловит только известные образцы. Меняется один байт — меняется MD5, и файл перестаёт находиться. Хэши ниже — это отправная точка расследования («заражение точно было»), а не критерий чистоты: пустой результат поиска ничего не доказывает.

Вредоносные файлы в разобранных случаях имели такие MD5-хэши:

text
07a3fe9875d3a8b7c57874c4cc509929
50b7604d856f36983b9bb3066f894f3f

Поиск вредоносных файлов

bash
# Поиск по хэшу
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

Гораздо надёжнее хэшей работает поиск по времени изменения и сверка с системой контроля версий. Файлы, изменившиеся в окне атаки, — первое, что стоит просмотреть:

bash
# 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
<?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";

Запуск:

bash
php -f autofix.php

Порядок действий при компрометации

Последовательность важна: если начать с чистки файлов, атакующий вернётся через ещё живую сессию или незакрытую точку входа, и вы будете чистить по кругу.

  1. Закрыть точку входа. Обновить решение или внести правки выше. Пока эндпоинт принимает unserialize из запроса, всё остальное бессмысленно.
  2. Отозвать все доступы. Пароли администраторов Битрикс, FTP/SSH, БД, API-ключи платёжных систем и интеграций. Плюс authorized_keys — дописанный туда ключ переживает любую смену пароля.
  3. Инвентаризовать изменения. Поиск по времени модификации, сверка с git, проверка cron (crontab -l для каждого пользователя, /etc/cron.*), проверка .htaccess на добавленные редиректы, проверка b_user на новых администраторов.
  4. Остановить майнер — но только после инвентаризации: kill -9 уничтожает и процесс, и возможность посмотреть, откуда он запущен.
    bash
    ps aux | grep -i xmrig
       ls -l /proc/<PID>/exe   # откуда реально запущен процесс
       ls -l /proc/<PID>/cwd
       kill -9 <PID>
  5. Развернуть чистую копию. Файлы ядра и решения — из дистрибутива нужной версии, свой код — из репозитория, данные — из базы (её тоже проверив на посторонние скрипты в свойствах и шаблонах писем).
  6. Настроить мониторинг целостности файлов, чтобы следующая попытка была видна на первом же изменении.
⚠️ Важно

Про пункт 5. Клиент почти всегда предлагает «просто удалить вирусы и жить дальше», и почти всегда после этого сайт заражается повторно — потому что где-то остался второй веб-шелл, который никто не нашёл. Развёртывание с нуля из проверенных источников — единственный способ утверждать, что сервер чист, а не надеяться на это.

Итоги

Технически история про unserialize — это классическая PHP object injection: небезопасная десериализация данных из запроса даёт выполнение кода. Практически же вывод шире и скучнее: сайты ломают не потому, что уязвимость была неизвестна, а потому, что патч вышел, а обновление никто не поставил.

Что стоит сделать сегодня, если у вас Битрикс с готовым решением:

  • проверить версию решения и запланировать обновление (не «когда-нибудь», а с датой);
  • поискать unserialize в собственном коде — grep -rn "unserialize(" /local/ — и убедиться, что ни в один вызов не приходит $_GET, $_POST или $_COOKIE;
  • завести проект в git, если его там ещё нет: расследование инцидента без эталона занимает дни вместо минут.
🚀

Хотите такое же решение?

Настрою окружение под ваш проект, учту специфику инфраструктуры и обучу команду.

Обсудить проект →
Бесплатная консультация · Ответ в течение дня

Комментарии

Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.