
CVE-2026-42533: переполнение кучи в nginx до аутентификации и утечка информации из-за затирания захватов PCRE в движке map/script, объединённые в цепочку с RCE.
Переполнение буфера в куче до аутентификации и утечка информации из кучи в nginx, вызванные отсутствием сохранения/восстановления состояния захватов PCRE между двумя проходами скриптового движка. Вычисление переменной map с регулярным выражением между двумя ссылками на захваты перезаписывает r->captures, поэтому проход LEN и проход VALUE расходятся в оценке размера захвата. Буфер выделяется под один захват, а заполняется другим. Более крупная перезапись приводит к переполнению кучи с управляемыми атакующим содержимым и длиной. Меньшая перезапись приводит к появлению буфера завышенного размера, неинициализированный хвост которого возвращается клиенту, раскрывая указатели libc и кучи.
Два примитива объединяются в надёжное удалённое выполнение кода до аутентификации. Утечка обходит ASLR за один GET, поэтому переполнению не требуется отключённый ASLR.
Разбор: https://cyberstan.co.uk/nginx-rce/ Уведомление (Advisory): F5 K000162097 Исследователь: Stan Shaw (cyberstan)
nginx с 0.9.6 по 1.30.3 (stable) и 1.31.2 (mainline); уязвимость достижима с 2011 года, когда директива map получила поддержку регулярных выражений. Подвержены оба модуля — http и stream. Около 50 директив в 13 местах вызова, плюс второй путь через именованные захваты (r->variables[]). Исправлено в версиях 1.30.4 и 1.31.3.
CVE-2026-42533-PoC/
├── exploits/ exploit and validator scripts
│ ├── poc.py numbered-capture proxy_method chain (crash/leak/rce/rce-det)
│ ├── calibrate.py find PL_OFF / HEAP_PAGE_OFF for --rce-det on your build
│ ├── leak_multi.py info leak on the return and set sinks
│ ├── test_all_sites.py ASan validator, all 13 overflow sites
│ └── named_capture_poc.py named-capture r->variables[] variant
├── configs/ nginx configs the exploits run against
│ ├── nginx_poc.conf crash / leak / rce
│ └── nginx_det.conf deterministic rce-det
├── docs/
│ └── EXPLOITATION.md full exploitation writeup
└── README.md
Запускайте все команды ниже из корня репозитория, чтобы пути exploits/, configs/ и дерево сборки ../nginx-1.30.1 корректно разрешались.
exploits/poc.py — основной эксплойт (нумерованный захват, сток proxy_method). Его режимы:
| Режим | Назначение |
|---|---|
poc.py --crash | Запускает переполнение кучи; на ASan-сборке выводит размер записи и стек в ngx_http_script_copy_capture_code. |
poc.py --leak | Утечка информации: извлекает указатели libc и кучи из тела ответа завышенного размера. |
poc.py --rce | Полноценное RCE до аутентификации. Одиночный выстрел в общем случае (configs/nginx_poc.conf), ~66% за попытку, при промахе — повторный запуск. |
poc.py --rce-det | Полноценное RCE до аутентификации, детерминированное против управляемой конфигурации configs/nginx_det.conf. |
Отдельные скрипты, покрывающие остальную поверхность уязвимости:
| Скрипт | Назначение |
|---|---|
exploits/leak_multi.py | Утечка информации через ещё два вычислителя (return, set); каждый указатель libc+heap подтверждается через /proc/<pid>/maps. Стандартная конфигурация. |
exploits/test_all_sites.py | Валидатор AddressSanitizer, активирующий все 13 мест вызова переполнения (http + stream). |
exploits/named_capture_poc.py | Вариант с именованными захватами (?P<name>...) через r->variables[] / copy_var_code — вторая первопричина. |
Конфигурации находятся в configs/: nginx_poc.conf (crash/leak/rce), nginx_det.conf (rce-det). Полный разбор — в docs/EXPLOITATION.md.
Linux, gcc, python3 и исходники nginx 1.30.1. Разработано и протестировано на Ubuntu 24.04.4, glibc 2.39, PCRE2 10.42, python 3.12, при полностью включённом ASLR.
Нужны две сборки. Чистая сборка — для утечки и RCE, чтобы остатки в куче были реальными. Сборка с AddressSanitizer — для краха и валидаторов мест вызова, чтобы переполнение фиксировалось с точным размером записи и стеком.
tar xf nginx-1.30.1.tar.gz
cd nginx-1.30.1
# clean build -> objs.dbg/nginx (leak, rce)
./configure --with-pcre --with-http_ssl_module --with-debug --builddir=objs.dbg
make -j"$(nproc)"
# ASan build with all the modules the site validator needs -> objs/nginx
./configure --with-pcre --with-http_ssl_module --with-http_v2_module \
--with-stream --with-stream_ssl_preread_module --with-stream_ssl_module \
--with-debug \
--with-cc-opt='-g -O0 -fsanitize=address -fno-omit-frame-pointer' \
--with-ld-opt=-fsanitize=address --builddir=objs
make -j"$(nproc)"
exploits/poc.py обращается к запущенному nginx на 127.0.0.1:8950. Запустите его в одном терминале с configs/nginx_poc.conf, затем в другом выполните нужный режим. Остальные три скрипта сами запускают и останавливают собственный экземпляр nginx, поэтому им требуется только NGINX_BIN.
mkdir -p run/logs
../nginx-1.30.1/objs/nginx -p run -c "$PWD/configs/nginx_poc.conf" # ASan build, foreground
python3 exploits/poc.py --crash
Ожидаемый результат: переполнение кучи (heap-buffer-overflow), WRITE of size 200 в функции ngx_http_script_copy_capture_code (файл ngx_http_script.c:1404), вызываемой из ngx_http_complex_value в ngx_http_proxy_create_request.
../nginx-1.30.1/objs.dbg/nginx -p run -c "$PWD/configs/nginx_poc.conf" # clean build
python3 exploits/poc.py --leak
Ожидаемый результат: тело размером 8161 байт, из которых записано 2 байта, а остальное — остатки кучи. Указатель libc на смещении 0x08 и указатель кучи на 0x10.
../nginx-1.30.1/objs.dbg/nginx -p run -c "$PWD/configs/nginx_poc.conf" # clean build
python3 exploits/poc.py --rce # writes /tmp/PWNED via system()
Ожидаемый результат: утечка, около 40 spray-соединений, срабатывание переполнения, затем файл /tmp/PWNED с выводом команды id. Это одиночный выстрел, который на dev-сборке срабатывает примерно в двух третях случаев; при промахе worker падает, и вы запускаете эксплойт снова. См. раздел «Reliability» в docs/EXPLOITATION.md.
Против управляемой конфигурации та же ошибка срабатывает детерминированно с первого раза. Эксплойт восстанавливает абсолютный базовый адрес кучи из утечки одной строкой (heap_base = (leaked_ptr & ~0xfff) - 0x22000), размещает единственный поддельный cleanup-обработчик пула в удерживаемом соединении по известному адресу и направляет cleanup пула жертвы на него вместо временного тела триггера, которое nginx освобождает до teardown.
mkdir -p run/logs
../nginx-1.30.1/objs.dbg/nginx -p run -c "$PWD/configs/nginx_det.conf" # controlled config
python3 exploits/poc.py --rce-det
nginx_det.conf — лабораторная конфигурация (один worker, фиксированные буферы), чьё расположение кучи воспроизводимо; именно поэтому смещения в poc.py (PL_OFF, HEAP_PAGE_OFF) остаются верными. PL_OFF — это смещение от базы кучи, по которому попадает тело cleanup удерживаемого запроса POST /b/; оно зависит от точной последовательности выделений памяти, поэтому сдвигается между сборками, версиями glibc и конфигурациями. Если --rce-det сообщает Recalibrate, снимите корректные значения с живого worker-процесса с помощью calibrate.py:
NGINX_BIN=../nginx-1.30.1/objs.dbg/nginx python3 exploits/calibrate.py
# prints e.g. set PL_OFF = 0x14426 , then edit it in exploits/poc.py
Единственный оставшийся вариант неудачи — расклад ASLR, при котором байт 0x0a попадает внутрь адреса cleanup или тела переполнения; регулярное выражение map не может перенести такой байт. Инструмент сообщает об этом, и вы запускаете эксплойт снова. Обычное развёртывание не настолько предсказуемо, поэтому там используйте --rce. См. раздел «A deterministic build» в docs/EXPLOITATION.md.
NGINX_BIN=../nginx-1.30.1/objs/nginx python3 exploits/test_all_sites.py # ASan build
NGINX_BIN=../nginx-1.30.1/objs/nginx python3 exploits/test_all_sites.py 1 7 12 # a subset
NGINX_BIN=../nginx-1.30.1/objs/nginx python3 exploits/named_capture_poc.py # ASan build
NGINX_BIN=../nginx-1.30.1/objs.dbg/nginx python3 exploits/leak_multi.py # clean build
Ожидаемый результат: return и set — каждая даёт утечку указателя libc и указателя кучи, оба подтверждаются через /proc/<pid>/maps worker-процесса.
Чаще всего спотыкаются о две вещи: использование не той сборки (ASan вместо чистой) и несоответствие смещений --rce-det вашему окружению. Обе проблемы разобраны ниже.
| Симптом | Причина | Что делать |
|---|---|---|
--leak не показывает указатели, или --rce / --rce-det никогда не срабатывают | Вы используете ASan-сборку; AddressSanitizer помечает освобождённую память как отравленную, поэтому в остатках нет реальных указателей | Используйте чистую сборку objs.dbg для --leak, --rce, --rce-det и leak_multi.py. ASan-сборка objs предназначена только для --crash, test_all_sites.py и named_capture_poc.py. |
--rce срабатывает только примерно в 2 из 3 попыток | Одиночный выстрел против общей конфигурации; расположение освобождённого тела триггера варьируется | Это ожидаемо. При промахе worker падает и перезапускается — просто запустите эксплойт снова. Для детерминированного одиночного выстрела используйте --rce-det. |
--rce-det при каждом запуске печатает No RCE. Recalibrate PL_OFF/HEAP_PAGE_OFF | PL_OFF зависит от вашей сборки, glibc, конфигурации и длины пути префикса -p nginx. Поставляемое значение рассчитано на dev-сборку этого репозитория, запускаемую с -p run | Запустите exploits/calibrate.py, вставьте напечатанный им PL_OFF в exploits/poc.py и запускайте nginx с тем же префиксом -p, который вы передали calibrate.py (в README используется -p run). |
--rce-det изредка печатает 0x0a (regex-hostile) ... retry | Расклад ASLR поместил байт 0x0a (перевод строки) в адрес, а регулярное выражение map не может перенести такой байт | Это не ошибка. Перезапустите; следующий расклад почти всегда позволяет это обойти. |
nginx или calibrate.py: bind() to 127.0.0.1:8950 failed (Address already in use) | Предыдущий nginx всё ещё удерживает порт | Выполните pkill -x nginx, подождите секунду, повторите. Держите на 8950 только один nginx. |
--rce восстанавливает базу libc и указатель кучи во время выполнения из данных утечки, поэтому в нём не захардкожен ни один адрес. Вместе с тем в нём захардкожены смещения, специфичные для той сборки и той libc, на которых он разрабатывался:
LIBC_LEAK_OFFSET libc base to the leaked arena pointer
SYSTEM_OFFSET libc base to system()
BODY_DELTA_* leaked heap pointer to the overflow body buffer
POOL_OFF_FROM_BUF, D_LAST_OFF, D_END_OFF, LOG_OFF forged pool geometry
На другом дистрибутиве, glibc или сборке nginx эти значения необходимо перекалибровать. Реальное смещение system() можно узнать командой readelf -sW /lib/x86_64-linux-gnu/libc.so.6 | grep '\bsystem\b', а смещения пула — из структуры ngx_pool_t целевой системы. Режимы --crash и --leak не содержат подобных смещений и воспроизводятся на любой сборке уязвимой версии.
Утечка не требует особой настройки. Она работает на стандартной конфигурации events {}: значение worker_connections по умолчанию (512) задаёт арену такого размера, что освобождённый чанк запроса попадает в бин glibc, всё ещё хранящий указатели арены и кучи, которые переиспользует буфер утечки завышенного размера. Обойти её позволяет лишь необычно низкий worker_connections (меньше ~256); все продакшн-значения (512–1024) дают утечку.
Статический сканер конфигураций, который выявляет уязвимый паттерн без какой-либо эксплуатации, доступен по адресу https://github.com/0xCyberstan/CVE-2026-42533-Config-Scanner.
Данный материал посвящён исправленной, публично раскрытой уязвимости. Он создан для защитников, проверяющих подверженность уязвимости, и для воспроизведения в исследовательских целях. Запускайте его только против nginx, которым вы владеете или на тестирование которого получили явное разрешение. Обновитесь до версии 1.30.4 или 1.31.3.
Отдельный скрипт выводит nginx not found |
NGINX_BIN не задан или указывает не на ту сборку |
Задайте NGINX_BIN (чистая сборка для leak_multi.py, ASan-сборка для валидаторов). |
No such file для конфига, скрипта или бинарника nginx | Вы не в корне репозитория | Сначала выполните cd в корень репозитория; каждая команда предполагает его (exploits/..., configs/..., ../nginx-1.30.1/...). |
| Утечка работает на одной конфигурации, но не на другой | worker_connections меньше ~256 уменьшает арену, поэтому освобождённый чанк не содержит указателей | Используйте обычное значение worker_connections (от 512 до 1024). Все продакшн-значения дают утечку. |