
SolarWinds Serv-U CVE-2026-28318: сбой Content-Encoding: deflate без аутентификации. Анализ первопричины (недопустимое освобождение внутреннего указателя -> повреждение кучи) + PoC только для DoS. Исправлено в 15.5.4 Hotfix 1.
Анализ первопричины + PoC для DoS. В публичном бюллетене эта уязвимость классифицируется как отказ в обслуживании / неконтролируемое потребление ресурсов без аутентификации. Бинарный анализ показывает, что основным дефектом является нарушение безопасности памяти: некорректный
free()внутреннего указателя в пути декодирования HTTPdeflate, который повреждает кучу процесса (STATUS_HEAP_CORRUPTION,0xC0000374). Это не бомба декомпрессии, и краш не зависит от размера распакованных данных.
Затрагивает только HTTP/HTTPS слушатель (веб/путь управления). FTP/FTPS/SFTP не находятся на этом пути кода.
Любой HTTP-запрос с Content-Encoding: deflate и корректным телом deflate роняет сервис.
Обработчик распаковывает тело, затем освобождает указатель на сжатое тело и подставляет
распакованный буфер — но этот указатель является внутренним указателем в приёмный буфер HTTP
(он указывает на тело, сразу после \r\n\r\n), а не базой выделения кучи.
Вызов free() для небазового, невыровненного указателя повреждает кучу, и процесс завершается.
Поскольку ошибка в том, как освобождается указатель на тело, а не в том, сколько данных производится, даже тело размером ~25 байт (которое распаковывается в несколько КБ) стабильно вызывает краш. Потребление памяти не растёт; это не «zip/deflate-бомба».
Процедура декодирования deflate находится в RhinoNET.dll (сетевая библиотека Serv-U) и вызывается
из пути приёма HTTP (ProcessReceive) после того, как парсер заголовков устанавливает флаг
«deflate» при обнаружении Content-Encoding: deflate.
Упрощённо, процедура вызывается как decode(this, &bodyPtr, &bodyLen) и делает следующее:
1. inflate bodyPtr[0..bodyLen] -> наращивает аккумулирующий буфер `acc` (штатный zlib, ограниченный, корректный)
2. free(*bodyPtr) <-- *bodyPtr — это ВНУТРЕННИЙ указатель в приёмный буфер
3. *bodyPtr = acc // заменяет сжатое тело распакованным буфером
4. *bodyLen = total
Шаг 2 — это ошибка. *bodyPtr не является независимо выделенным блоком: он указывает на тело запроса
внутри единого приёмного буфера HTTP, т.е. receive_buffer + header_length. Освобождение
внутреннего (и невыровненного по 16 байтам) указателя заставляет аллокатор интерпретировать область,
подконтрольную атакующему, как заголовок блока кучи → повреждение метаданных кучи → 0xC0000374.
Обе «очевидные» гипотезы неверны, что проверено в рантайме:
zlib1.dll!inflate,
который учитывает avail_out при каждом вызове (наблюдалось на 100+ чанках; ни одной записи за
границы).prev_total + produced + 1 на каждом шаге и записывается ровно столько же — плотно, без переполнения.Повреждение вызвано исключительно некорректным free() на шаге 2.
Захвачено в рантайме (Frida) на Serv-U 15.5.4.108 при отправке запроса с Content-Encoding: deflate,
тело которого распаковывается в "A" * 8192:
…ce — mod 16 == 14. Базы выделения кучи выровнены по 16 байтам,
поэтому это не база выделения; это внутренний указатель.\r\n\r\n:
…tream\r\nContent-Length: 26\r\nConnection: close\r\n\r\n
ed c1 01 0d 00 …).ntdll.dll, исключение 0xC0000374
(STATUS_HEAP_CORRUPTION), при этом вызов исходит из free в deflate-обработчике RhinoNET.dll.
Последняя операция с кучей перед крашем — это именно free(bodyPtr).0xC0000374) в нашем тестировании, и мы не добились выполнения кода. Рассматривайте
это как pre-auth heap-corruption DoS с первопричиной в безопасности памяти; не предполагайте RCE.Требуется Python 3 (только стандартная библиотека). Направьте его на HTTP-слушатель Serv-U, который
вам разрешено тестировать. На Windows он использует один вызов Get-NetTCPConnection, чтобы узнать PID
слушателя, и журнал приложений Windows, чтобы подтвердить краш из-за повреждения кучи.
python poc_verify.py # по умолчанию 127.0.0.1:80, маленький пакет, 1 выстрел
python poc_verify.py --host <ip> --port 80
python poc_verify.py --big # тело распаковывается в 8192 байт
python poc_verify.py --shots 3
python poc_verify.py --no-events # пропустить проверку журнала событий (не требует прав)
PoC формирует минимальный запрос:
POST / HTTP/1.1
Host: <target>
Content-Encoding: deflate
Content-Type: application/octet-stream
Content-Length: <n>
Connection: close
<raw-deflate тело — даже несколько десятков байт достаточно>
Он сообщает PASS, если сервис упал (PID слушателя изменился / появилось событие 0xC0000374), и
FAIL, если он выжил (уже исправлен, не затронут или тело не было обработано как deflate).
Content-Encoding на HTTP/HTTPS
слушателе, например на обратном прокси:
if ($http_content_encoding) { return 400; }
RhinoNET.dll / Serv-U.exe (база образа
0x180000000) для построения пути приём → разбор заголовков → deflate-декодирование и поиска ошибочного
free.zlib учитывает avail_out, трассировка каждого alloc/free/memcpy декодирующего вызова на
диск, а также обработчик исключений процесса для захвата аварийной инструкции и стека 0xC0000374
в момент повреждения. Освобождение внутреннего указателя затем было подтверждено дампом байтов вокруг
освобождаемого указателя (хвост HTTP-заголовка + raw-deflate тело, невыровненное по 16 байтам).Адреса, на которые даются ссылки в анализе, специфичны для сборки 15.5.4.108 и будут различаться между сборками.
Опубликовано как оборонительное исследование после доступности исправления от вендора. PoC предназначен только для DoS и для авторизованного тестирования.
| Продукт | SolarWinds Serv-U (FTP / MFT / File Server) |
| Уязвимые | 15.5.4 и более ранние в этой ветке без Hotfix 1 (анализ выполнен на сборке 15.5.4.108) |
| Исправленные | Serv-U 15.5.4 Hotfix 1 (выпущен 2026-06-04) |
| Вектор | Без аутентификации, по сети (HTTP/HTTPS порт управления/веб) |
| Публичный класс | CWE-400 Неконтролируемое потребление ресурсов · DoS · CVSS 7.5 · CISA KEV |
| Фактический класс | CWE-763 Освобождение некорректного указателя / CWE-590 Освобождение памяти не из кучи → повреждение кучи |