Полный репозиторий исследования CVE-2026-42945 с анализом переполнения буфера в куче, эксплойтом RCE (распыление кучи + Feng Shui), скриптами обнаружения и руководством по устранению уязвимости модуля перезаписи NGINX.
Переполнение буфера кучи в NGINX ngx_http_rewrite_module
| Метрика | Значение |
|---|---|
| CVSS v4.0 | 9.2 (Критический) |
| CVSS v3.1 | 8.1 (Высокий) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | 122 — Переполнение буфера кучи |
| Введена | Июнь 2008 — v0.6.27 |
| Обнаружена | Апрель 2026 — DepthFirst Research |
| Исправлена | 13 мая 2026 — v1.30.1, v1.31.0 |
| CVE опубликована | 21 мая 2026 |
| Время жизни | ~18 лет (не обнаружена) |
| Коммит исправления | 524977e7c534e87e5b55739fa74601c9f1102686 |
Неаутентифицированный удаленный злоумышленник может вызвать детерминированное переполнение буфера кучи в рабочих процессах NGINX, отправив специально сформированный HTTP-запрос на сервер с определенным шаблоном конфигурации rewrite + set/if/rewrite. Переполнение повреждает метаданные кучи (указатели ngx_pool_cleanup_t), что позволяет выполнить удаленный код (RCE) с помощью техник heap spray и Feng Shui.
server { listen 19321;
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /internal?migrated=true;
set $original_endpoint $1;
}
}
**Ключевые требования:**
- Директива `rewrite`, замена которой содержит `?` (разделитель строки запроса)
- Последующая директива `set`, `if` или `rewrite`, которая ссылается на **безымянный захват PCRE** (`$1`, `$2` и т.д.)
- Символ `?` в замене rewrite запускает `ngx_http_script_start_args_code`, который устанавливает `e->is_args = 1`
### Что может сделать атакующий
| Возможность | Описание |
|-----------|-------------|
| **Отказ в обслуживании (DoS)** | Детерминированное аварийное завершение рабочих процессов, вызывающее циклы перезапуска (работает независимо от ASLR) |
| **Удаленное выполнение кода (RCE)** | При отключенном ASLR (или обходе через частичную перезапись) достигается полное RCE от имени пользователя nginx |
| **Кража данных** | С помощью примитивов чтения памяти извлекаются конфиденциальные данные из кучи рабочего процесса |
| **Постоянство (устойчивость)** | Внедрение бэкдоров через выполнение кода в памяти рабочего процесса |
---
## 2. Анализ первопричины
### Двухпроходный механизм скриптов
Модуль `ngx_http_rewrite_module` NGINX использует **двухпроходный механизм скриптов** в `src/http/ngx_http_script.c`:
1. **Проход длины** (`ngx_http_script_run`): перебирает все коды скрипта для вычисления необходимого общего размера буфера. Записывает длины в `le.ip` и `le.pos`.
2. **Проход копирования** (`ngx_http_script_copy_len`/`_code`): повторно перебирает, записывая фактические байты в предварительно выделенный буфер по адресам `e->ip` и `e->pos`.
Каждый код скрипта имеет два обработчика: для каждого прохода. Например:
- `ngx_http_script_copy_len` → `ngx_http_script_copy_code`
- `ngx_http_script_start_args_len` → `ngx_http_script_start_args_code`
### Флаг `is_args`
Флаг `e->is_args` в **структуре движка** (`ngx_http_script_engine_t`) управляет тем, как проход копирования обрабатывает определенные символы:```c
typedef struct {
u_char *ip;
u_char *pos;
ngx_http_variable_value_t *sp;
ngx_str_t buf;
int flushed;
unsigned is_args:1; // <-- THE BUG
unsigned ncaptures:1;
ngx_uint_t captures_size;
// ...
} ngx_http_script_engine_t;
Когда e->is_args = 1, код копирования для ссылок захвата $N вызывает ngx_escape_uri() с NGX_ESCAPE_ARGS, который расширяет:
+ → %2B (1 байт → 3 байта, +200%)% → %25 (1 байт → 3 байта, +200%)& → %26 (1 байт → 3 байта, +200%)Поток выполнения для уязвимого шаблона:``` rewrite ^/api/(.*)$ /internal?migrated=true;
1. Во время **оценки rewrite**, движок встречает `?` в строке замены, что запускает `ngx_http_script_start_args_code`, устанавливая `e->is_args = 1`.
2. Rewrite изменяет URI запроса и затем продолжает выполнение следующей директивы.
3. **`e->is_args` НИКОГДА НЕ СБРАСЫВАЕТСЯ**.
Затем:```
set $original_endpoint $1;
le): ```c
ngx_memzero(&le, sizeof(ngx_http_script_engine_t));
Это правильно обнуляет le.is_args = 0, поэтому проход длины возвращает сырую, неэкранированную длину захвата.
e, который всё ещё имеет e->is_args = 1 из шага 1. Копирующий проход применяет URI-экранирование, расширяя каждый экранируемый символ с 1 байта до 3 байт внутри буфера, который был рассчитан на сырую длину — переполнение кучи.Pass 1 (Length — sub-engine le): le.is_args = 0 capture $1 = "A+++++B" → length = 7
Buffer allocated: 7 bytes
Pass 2 (Copy — main engine e): e.is_args = 1 ← LEAKED from rewrite capture $1 = "A+++++B" ngx_escape_uri("A+++++B", NGX_ESCAPE_ARGS): A → A (1 byte) + → %2B (3 bytes) ← EXPANSION + → %2B (3 bytes) + → %2B (3 bytes) + → %2B (3 bytes) + → %2B (3 bytes) B → B (1 byte) total written: 17 bytes buffer size: 7 bytes OVERFLOW: 10 bytes
Коэффициент расширения равен `7 + (n_escapable * 2)`, где `n_escapable` — количество символов `+`, `%` и `&` в захвате.
---
## 3. Механика эксплуатации
### Обзор
| Шаг | Техника | Описание |
|------|-----------|-------------|
| 1 | Переполнение | Отправить специально сформированный URI с заполнением `+` для переполнения буфера в куче |
| 2 | Заливка кучи | POST большие тела на `/spray` для заполнения кучи управляемыми данными |
| 3 | Фэн-шуй | Организовать выделения так, чтобы цель переполнения (`ngx_pool_cleanup_t`) оказалась смежной |
| 4 | Повреждение обработчика | Переполнение перезаписывает `ngx_pool_cleanup_t.handler` адресом `system()` |
| 5 | Запуск очистки | Дождаться уничтожения пула → `system(cmd)` выполняет команду атакующего |
| 6 | Обратная оболочка | Цепочка к полезной нагрузке обратной оболочки для интерактивного доступа |
### Фэн-шуй между запросами
**Фэн-шуй в рамках одного запроса не работает**, потому что переполнение повреждает метаданные пула (`->d.next`, `->d.failed`) до достижения указателя `cleanup`. Когда пул уничтожается в конце запроса, поврежденные метаданные вызывают **аварийное завершение до вызова `system()`**.