
Эксплойт-доказательство концепции для CVE-2026-9256, переполнение кучи в модуле NGINX ngx_http_rewrite_module. Подтверждает удаленное падение рабочего процесса через специально сформированный URI со спецсимволами, с многостадийным зондом keep-alive для надежного обнаружения.
Область применения: только для локальных полигонов, авторизованных сред воспроизведения, проверки уязвимостей и анализа защитных правил. Не используйте на неавторизованных целях. Идеи PoC в этом документе проверяют только удаленно наблюдаемое поведение аварийного завершения worker NGINX, не включают RCE, обход ASLR или стабильную цепочку эксплуатации.
CVE-2026-9256 — это уязвимость переполнения кучи в модуле ngx_http_rewrite_module NGINX. Триггер уязвимости — не просто обращение по определенному фиксированному URI, а зависимость от конкретного шаблона конфигурации rewrite: в rewrite-регулярном выражении существуют перекрывающиеся группы захвата PCRE, а в части replacement (замены) используются несколько переменных захвата, например $1, $2.
Когда злоумышленник создает специальный URI, который приводит к выполнению логики rewrite по соответствующему пути, NGINX при обработке захваченного контента, склейке результатов rewrite или экранировании URI/параметров может столкнуться с несоответствием рассчитанной длины и реальной записи, что в итоге приводит к повреждению памяти кучи worker-процесса.
Поэтому ключ к этой уязвимости — не сам путь /api, а наличие в конфигурации целевого NGINX уязвимого правила rewrite, которое может быть активировано запросом. Путь /api, используемый в PoC по умолчанию, — это всего лишь пример пути в текущей среде воспроизведения; при реальном тестировании путь запроса необходимо корректировать в соответствии с правилами rewrite, содержащими перекрывающиеся группы захвата и ссылающимися на несколько переменных захвата.
Текущая цель PoC — проверка поведения аварийного завершения worker / отказа в обслуживании. Он не пытается создать точную раскладку кучи, не пытается перезаписать адрес возврата или указатели функций, не доказывает удаленное выполнение кода. Стабильные свидетельства, наблюдаемые на удаленной стороне, в основном: аномальное разрывы соединения при отправке триггерного запроса, затем восстановление работы службы NGINX, прерывание keep-alive соединений сбойным worker после триггера.
После срабатывания этой уязвимости не обязательно проявляется фиксированный код 500, 502 или 400. Это связано с тем, что модель master-worker NGINX: после аварийного завершения worker-процесса мастер запускает новый worker. Удаленно атакующий обычно наблюдает не полную недоступность всего сервиса, а внезапный разрыв соединения, тайм-аут чтения, сброс соединения, после чего повторный запрос к / снова дает нормальный ответ.
Поэтому PoC не может судить о наличии уязвимости только по коду состояния HTTP одного запроса. Если отправить один длинный URI, увидеть разрыв соединения и сразу сделать вывод «уязвимость есть», вероятность ложного срабатывания высока. Разрыв соединения может быть также вызван сетевыми колебаниями, тайм-аутом прокси, перехватом промежуточным устройством, ограничением на бэкенде или активным закрытием соединения сервером.
Следовательно, PoC должен быть спроектирован как многоэтапная проверка:
Только когда одновременно наблюдаются «аномальный разрыв соединения при триггере + последующее восстановление службы + многократные разрывы keep-alive», можно с большей уверенностью судить о наличии поведения CVE-2026-9256 (аварийное завершение worker).
Текущий основной путь триггера PoC:
GET /api/++++++++++++++++++++++++++++++++... HTTP/1.1
Host: 127.0.0.1:19321
Здесь /api/ — примерный маршрут в текущем полигоне для попадания в правило rewrite, после него добавляется большое количество символов +. По умолчанию их 4096.
Выбор + обусловлен тремя основными причинами.
Во-первых, + — допустимый символ URI. При отправке обычным HTTP-клиентом он обычно не обрезается и не принудительно перекодируется, как пробел, # и т.д. Поэтому текущий PoC не требует использования raw-сокетов для создания недопустимой строки запроса, как в некоторых обходах request-target.
Во-вторых, большое количество повторяющихся символов позволяет группам захвата rewrite получить достаточно длинный ввод, расширяя масштаб вывода при последующей склейке replacement или экранировании, что упрощает проявление несоответствия между рассчитанной длиной и реальной записью.
В-третьих, структура полезной нагрузки из повторяющихся + проста, что облегчает наблюдение в трафике, журналах и правилах IDS, а также удобно для регулировки длины и тестирования порогов.
Однако следует отметить: + — не единственный теоретически возможный символ-триггер. Реальное условие срабатывания по-прежнему состоит в «попадании в уязвимую конфигурацию rewrite + ввод обрабатывается соответствующими группами захвата + обработка вывода rewrite вызывает переполнение кучи». В разных средах могут потребоваться корректировки маршрута, типа символа и порога длины.
Текущий PoC по умолчанию выбирает символ + в большом количестве. Это не значит, что только + может вызвать проблему. + — наиболее подходящий символ для написания универсального PoC, так как он относительно стабилен в URI, легко отправляется обычными HTTP-клиентами, а его характеристики в захвате трафика четкие.
С точки зрения принципа уязвимости: если символ в процессе обработки rewrite NGINX попадает в логику экранирования NGX_ESCAPE_ARGS и при этом расширяется с исходного 1 байта до 3 байт в виде %XX, это может вызвать несоответствие «рассчитанная длина < реальная запись». То есть триггерная точка в основе своей — не сам +, а «плотное появление символов, подлежащих экранированию в режиме args».
Помимо +, теоретически стоит учитывать следующие символы:
пробел: 0x20
#: 0x23
%: 0x25
&: 0x26
?: 0x3F
управляющие символы: 0x00-0x1F
старшие байты: 0x7F-0xFF
Если эти символы попадут в соответствующий захват и будут обрабатываться как содержимое args при экранировании в replacement rewrite, они вызовут аналогичный эффект расширения. Например:
+ -> %2B
& -> %26
% -> %25
# -> %23
? -> %3F
пробел -> %20
Каждый такой символ теоретически расширяется с 1 до 3 байт, увеличивая длину реальной записи на 2 байта. Если во вводе содержится много таких символов, реальная длина записи может значительно превысить ошибочно рассчитанную длину буфера, что упрощает вызов heap buffer overflow.
Однако пригодность разных символов в PoC неодинакова.
+ — самый стабильный. Он обычно может быть напрямую включен в HTTP request-target, редко обрезается браузером или инструментами командной строки, и не меняет структуру path/query URI. Поэтому текущий PoC использует 4096 символов + в качестве полезной нагрузки по умолчанию.
& также может быть кандидатом, так как в режиме args он экранируется в %26. Но в shell & означает выполнение в фоне, а в URL часто используется как разделитель параметров запроса, поэтому при тестировании нужно обращать внимание на кавычки и положение, иначе запрос может быть отправлен не так, как планировалось.
% также может быть кандидатом, так как он экранируется в %25. Однако % сам по себе является префиксом URL-кодирования, и некоторые клиенты, прокси или фреймворки могут попытаться интерпретировать последовательности %XX. При неправильном составлении цель может получить не исходный символ %, а предварительно обработанный клиентом контент.
? теоретически относится к символам, подлежащим экранированию, но в HTTP request-target он разделяет path и query. Если поместить его прямо в путь, последующее содержимое может быть интерпретировано как query string, что изменит диапазон захвата rewrite. Поэтому он больше подходит для дополнительных тестов, а не как основной символ PoC по умолчанию.
# теоретически может вызвать экранирование, но браузер не отправляет фрагмент после # серверу, многие продвинутые HTTP-клиенты также кодируют или обрезают его. Поэтому для тестирования буквального # обычно требуются raw-сокеты, Burp Repeater или инструменты, сохраняющие исходный request-target, нельзя полагаться на адресную строку браузера.
Пробел 0x20 также относится к экранируемым символам, но в строке запроса HTTP/1.1 пробел сам является разделителем. Если поместить его напрямую в request-target, будет нарушена структура строки запроса. При тестировании, если записать как %20, то в какой форме его увидит сервер на этапе обработки (кодированной или декодированной) зависит от конкретного процесса разбора и позиции rewrite. Поэтому пробел больше подходит для пояснения принципов и вспомогательного тестирования, а не как полезная нагрузка по умолчанию.
Управляющие символы 0x00-0x1F и старшие байты 0x7F-0xFF также входят в диапазон экранирования, но в реальных HTTP-каналах они чаще перехватываются, нормализуются или отклоняются клиентами, прокси, WAF или HTTP-парсером NGINX. Они могут служить примерами объектов экранирования на уровне исходного кода, но не рекомендуются в качестве символов-триггеров по умолчанию для обычного PoC.
Поэтому причина использования + в текущем PoC не в том, что уязвимость может быть вызвана только +, а в том, что + одновременно удовлетворяет трем условиям: может вызвать расширение при экранировании args, легко стабильно отправляется и не изменяет существенно структуру URI. При разработке защитных правил или анализе трафика нельзя ограничиваться только обнаружением последовательностей +. Необходимо также учитывать комбинации с высокой плотностью других экранируемых символов, особенно +, &, %, ?, # и т.п., появляющихся в длинных URI.
С точки зрения обнаружения, более разумное обобщение не:
после /api/ много символов +
а:
в длинных URI много специальных символов, которые в режиме NGX_ESCAPE_ARGS расширяются до %XX
Если правило обнаруживает только ++++, оно может покрыть только текущее написание PoC. Если злоумышленник заменит полезную нагрузку на &&&&, %%%%, ???? или смешает +%&?#, единичный признак + может пропустить атаку. Более надежный подход к обнаружению — совместная оценка длины URI, плотности специальных символов, количества повторений, опасного пути rewrite и поверхности NGINX.
Функция normalize_target в PoC обрабатывает ввод из командной строки, поддерживая три формы:
python3 CVE-2026-9256-poc.py 127.0.0.1:19321
python3 CVE-2026-9256-poc.py 127.0.0.1 19321
python3 CVE-2026-9256-poc.py http://127.0.0.1:19321
Если пользователь вводит только host:port без схемы, скрипт автоматически добавляет http://host:port. Затем с помощью urllib.parse.urlparse извлекаются hostname и port, и формируется base, например:
host = 127.0.0.1
port = 19321
base = http://127.0.0.1:19321
Текущая реализация в основном ориентирована на HTTP-полигоны с открытым текстом. Хотя normalize_target принимает https://, последующая проверка keep-alive использует обычный TCP-сокет без TLS, поэтому в сценарии HTTPS проверка сбоя будет неточной. Для поддержки HTTPS необходимо добавить ssl.wrap_socket или ssl.create_default_context().wrap_socket() в сокет.
Сначала PoC вызывает check_alive(base) для обращения к корневому пути /:
GET /
Если цель возвращает любой код состояния HTTP, это означает, что сервис в основном активен, и можно продолжать. При неудачном соединении скрипт завершается, чтобы не ошибочно интерпретировать недоступную цель как неудачный триггер уязвимости.
Затем вызывается check_rewrite(base) для обращения:
GET /api/test
Этот запрос нужен, чтобы проверить, может ли /api/* попасть в логику rewrite в текущем полигоне. Если возвращаются коды перенаправления 301, 302, 303, 307, 308, это указывает на явное поведение редиректа rewrite, и PoC выводит заголовок Location как вспомогательное свидетельство.
Однако этот шаг не является обязательным условием успеха. В некоторых конфигурациях воспроизведения сам путь /api/* может войти в проблемный путь rewrite, и даже обычный пробный запрос может вызвать тайм-аут или аномальную обработку. Поэтому скрипт продолжает фазу триггера, даже если не получил нормального ответа rewrite.
Функция триггера: send_trigger(base, plus_count=4096). Основная логика склейки:
payload = "/api/" + ("+" * plus_count)
По умолчанию конечный путь запроса выглядит так:
/api/++++++++++++++++++++++++++++++++... всего 4096 символов +
Затем запрос отправляется через requests.get(base + payload, timeout=10, allow_redirects=False).
Здесь отключено автоматическое перенаправление по двум причинам.
Во-первых, сам rewrite может возвращать редирект. Если HTTP-клиент автоматически следует перенаправлениям, исходный триггерный запрос и последующий редирект смешаются, что затруднит анализ того, что произошло с первым запросом.
Во-вторых, PoC интересует состояние соединения на этапе триггера, а не бизнес-страница после перенаправления. Сохранение исходного ответа упрощает анализ.
Результат триггера делится на несколько категорий:
Если перехвачено ConnectionError, это означает аномальное закрытие соединения во время триггерного запроса — возможно, проявление аварийного завершения worker.
Если перехвачено ReadTimeout, это означает, что запрос был отправлен, но в течение длительного времени не получен нормальный ответ; возможно, worker завис, не вернулся нормально до аварийного завершения или тайм-аут вызван сетевыми условиями.
Если получен нормальный ответ HTTP, выводится код состояния и длина тела ответа, но на основе только нормального ответа нельзя отрицать уязвимость, поскольку в некоторых средах условие триггера может быть не полностью выполнено или длина полезной нагрузки недостаточна.
После триггерного запроса PoC ждет 1 секунду, затем вызывает follow_up(base) для повторного обращения к корневому пути /.
Цель этого шага — не доказательство самого переполнения, а оценка того, проявляется ли у NGINX признак «перезапуск worker после аварийного завершения мастером».
Если соединение триггерного запроса аномально оборвалось, но последующий запрос к / снова возвращает 200 или другой нормальный код состояния HTTP, это означает, что сервис не полностью вышел из строя, а скорее всего упал только один worker-процесс и был восстановлен.
Если после триггера сервис долгое время недоступен, это может быть остановка всего сервиса, падение контейнера или сетевая аномалия; такой результат не может быть напрямую приравнен к успешному срабатыванию CVE-2026-9256.
Таким образом, текущий критерий PoC: удаленно наблюдаемое аварийное завершение worker, а не просто «сервис недоступен».
Наиболее важная проверка стабильности в PoC — keepalive_probe(host, port, rounds=5, plus_count=4096).
Она не использует requests, а напрямую устанавливает TCP-соединение через socket.create_connection и в рамках одного keep-alive соединения последовательно отправляет три запроса.
Первый запрос — обычный:
GET / HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
Этот запрос нужен, чтобы убедиться, что текущее соединение работоспособно, и по возможности заставить триггерный запрос упасть на то же соединение.
Второй запрос — триггерный:
GET /api/++++++++++++++++++++++++++++++++... HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
Если этот запрос вызывает аварийное завершение worker, то поддерживаемое им keep-alive соединение, скорее всего, будет разорвано напрямую.
Третий запрос — снова обычный:
GET / HTTP/1.1
Host: 127.0.0.1
Connection: close
Если на третий запрос все еще получен ответ, значит, соединение не прервалось из-за триггерного запроса, и в этом раунде аварийное завершение worker не засчитывается.
Если третий запрос не удалось отправить, данные не получены или соединение уже закрыто, записывается:
keepalive connection dropped
По умолчанию PoC повторяет 5 раундов. Смысл многократных повторений — снизить ложные срабатывания из-за случайных сетевых ошибок. Если в 5 раундах несколько раз происходит разрыв keep-alive, и при этом сервис все еще восстанавливается, то удаленные свидетельства становятся более убедительными.
PoC делит конечный результат на три категории.
Первая категория: подтверждение уязвимости.
Условие:
crash_count > 0 и recovered == True
То есть как минимум в одном раунде keep-alive зонда зафиксирован разрыв соединения, и последующий обычный запрос (follow-up) доказывает восстановление сервиса. Скрипт выводит:
VULNERABILITY CONFIRMED - CVE-2026-9256 style crash behavior
Impact confirmed: worker crash / denial of service
RCE is not proven by this script
Это означает, что текущая среда демонстрирует поведение аварийного завершения worker, характерное для CVE-2026-9256, но удаленное выполнение кода не доказано.
Вторая категория: подозрение на уязвимость.
Условие:
kind == "connection_error" and recovered == True
То есть основной триггерный запрос вызвал разрыв соединения, и сервис восстановился, но keep-alive зонд не подтвердил стабильно аварийное завершение worker. Скрипт выводит VULNERABILITY SUSPECTED.
Такая ситуация указывает на аномалию, но свидетельства недостаточно стабильны; требуется дополнительная проверка с помощью server-side error.log, core dump, контейнерных логов или отладчика.
Третья категория: не подтверждено.
Если нет ни надежного разрыва keep-alive, ни комбинации аномалии соединения при триггере + восстановление, скрипт выводит:
VULNERABILITY NOT CONFIRMED
Это не обязательно означает, что цель абсолютно не уязвима. Возможные причины: путь не попал в rewrite, длина полезной нагрузки недостаточна, не подходящий выбор символов, целевая версия исправлена, промежуточный прокси изменил URI, или текущий скрипт не адаптирован для HTTPS.
Процесс выполнения скрипта можно обобщить следующим образом:
/, подтверждение активности службы./api/test, попытка определить, активен ли примерный путь rewrite./api/ + 4096 символов + в качестве триггерного запроса./, проверка, восстановился ли worker.Пример локального полигона:
python3 CVE-2026-9256-poc.py http://127.0.0.1:19321
или:
python3 CVE-2026-9256-poc.py 127.0.0.1 19321
При успешном срабатывании типичный вывод будет выглядеть так:
[+] Connection dropped during trigger request
[+] Worker is responding after trigger (HTTP 200)
round 1: worker likely crashed (keepalive connection dropped)
round 2: worker likely crashed (keepalive connection dropped)
...
[+] VULNERABILITY CONFIRMED - CVE-2026-9256 style crash behavior
[+] Impact confirmed: worker crash / denial of service
[*] RCE is not proven by this script
Такой вывод указывает на то, что удаленно наблюдаются достаточно стабильные свидетельства аварийного завершения worker.
Границы безопасности этого PoC достаточно четкие:
Во-первых, он выполняет только проверку аварийного завершения, а не эксплуатацию RCE.
Во-вторых, он не содержит heap spray, ROP, обход ASLR, shellcode или логику выполнения команд.
В-третьих, его критерий успеха — разрыв соединения worker и восстановление сервиса, а не получение shell или чтение файлов.
В-четвертых, он подходит для локального воспроизведения, проверки уязвимости, создания правил IDS/IPS и сравнения до и после исправления.
Для дальнейшего повышения безопасности можно добавить следующие ограничения:
127.0.0.1, localhost, частным адресам или явно авторизованному сегменту сети.--plus-count, чтобы по умолчанию не отправлять слишком большую полезную нагрузку.--route, позволяющий пользователю явно указывать путь триггера вместо жестко заданного /api/.--rounds для управления количеством раундов keep-alive зонда.--print-request для вывода фактически отправленного HTTP-запроса, что облегчит сравнение с результатами захвата трафика.Исходя из анализа PoC, правила обнаружения не должны сосредотачиваться только на /api/, так как /api — не фиксированный путь уязвимости, а всего лишь пример текущего полигона. Действительно ценные точки обнаружения:
+, или смесь специальных символов +, &, %, ?, # и т.д.Если правило жестко привязано к /api/++++, оно покроет только текущий PoC и текущий полигон. Для покрытия более универсального атакующего трафика следует выделять признаки вокруг «длинный URI + большое количество специальных символов с высокой плотностью + направление HTTP запроса + рискованный путь NGINX rewrite».
В то же время, поскольку в легитимных бизнес-сценариях также могут быть длинные URL или много кодированных символов, правила должны снижать ложные срабатывания за счет порога длины, плотности символов, количества повторений и контекста пути. Более надежное направление обнаружения:
длинный URI
+
большое количество специальных символов, которые могут быть расширены экранированием NGX_ESCAPE_ARGS
+
направление запроса to_server
+
поверхность NGINX rewrite
а не просто:
/api/++++
Для правил Suricata / Snort, если требуется покрыть только текущий публичный PoC, можно использовать последовательность + как один из сильных признаков. Для покрытия вариантов необходимо включить диапазон специальных символов в PCRE, например +, %, #, &, ? и другие символы, потенциально расширяемые экранированием. Однако такие правила более подвержены ложным срабатываниям, поэтому их следует использовать в сочетании с urilen, порогом повторения символов, ограничениями пути и диапазоном NGINX-активов.
Вывод PoC для CVE-2026-9256 заключается не в поиске фиксированного пути уязвимости, а в понимании условий триггера: уязвимая конфигурация rewrite, перекрывающиеся группы захвата, ссылки на несколько переменных захвата и специальный URI-ввод, который вызывает аномальное расширение при обработке результатов rewrite.
Текущий скрипт выбирает /api/ + 4096 символов +, потому что этот путь попадает в правило rewrite в текущей среде воспроизведения, а большое количество + стабильно создает длинное входное давление. Скрипт не реализует RCE, а доказывает аварийное завершение worker через разрывы соединения, восстановление сервиса и многократные разрывы keep-alive.
При этом + — только самый стабильный и удобный для отправки символ по умолчанию, но не единственный возможный триггер. Любые символы, которые в режиме NGX_ESCAPE_ARGS расширяются до %XX, должны быть учтены при анализе принципов и разработке защитных правил. Более точное понимание: в длинных URI плотное появление символов, подлежащих расширяемому экранированию, при попадании в уязвимый захват rewrite и обработку replacement приводит к несоответствию между рассчитанной длиной и реальной записью, что в конечном итоге вызывает аварийное завершение worker или более серьезное повреждение памяти.