
Технический разбор CVE-2026-88776 — целочисленное переполнение (integer underflow) без предварительной аутентификации в парсере записей Oracle TNS компонента nsppe NetScaler, приводящее к записи за границами буфера и падению пакетного движка.
NetScaler умеет балансировать нагрузку баз данных Oracle. Направьте на него vserver типа ORACLE, и пакетный движок сам начнёт говорить на протоколе Oracle TNS, разбирая клиентский трафик встроенным обработчиком. Этот обработчик хранит небольшой конечный автомат для каждого соединения, чтобы помнить, где он находится внутри записи TNS.
Одно из этих состояний означает «я в середине записи, придут ещё байты». Когда приходит следующий сегмент, обработчик читает его длину и вычитает 10, потому что 10 байт каждого сегмента — это заголовок.
Никто не задался вопросом, что произойдёт, если сегмент короче 10 байт.
Отправьте такой. Сегмент-огрызок длиной от 1 до 9 байт попадает в это состояние, длина становится отрицательной, и поскольку длина хранится в беззнаковом 16-битном регистре, она не становится отрицательной. Она заворачивается в нечто огромное, вплоть до примерно 64 КБ. Затем обработчик с радостью выполняет memcpy такого объёма данных из вашего пакета в фиксированный буфер размером 0x748 байт в .bss. Движок не является PIE, поэтому адрес, который затаптывается, никогда не меняется.
Вам не нужно логиниться. Вам даже не нужна база данных за vserver, потому что само устройство отвечает на рукопожатие TNS. Вся атака — это один TCP-разговор.
Обе половины были продемонстрированы вживую:
Обработчик хранит своё состояние для каждого соединения по адресу [pcb+0x15a]. Большинство состояний скучны. То, что имеет значение здесь, — это 0x1F4, что означает «середина записи, ожидание продолжения», и вы направляете соединение в него, используя не что иное, как обычные байты протокола:
record type 6 -> deep-parser state 6 -> byte 0x03 (TTIFUN)
-> state 0x126 -> TTC function byte 0x5e (OALL8)
-> jump table 0x23589e0, index 0x59 -> writer at bfe27b:
[pcb+0x15a] = 0x1F4
Следующий пакет на этом соединении затем диспетчеризуется по 0x1F4, через bd49a7 в bd5362, и попадает сюда:
bd5432: memset(0x30a7e60, 0, 0x748) ; scratch region (ends 0x30a85a8)
bd5445: mov rcx,[rbp-0x208] ; inbound packet buffer
bd544c: movzx eax,WORD [rcx+0xf2] ; u16 wire length of the segment
bd5453: add eax,0xfffffff6 ; len = len - 10 <== THE UNDERFLOW
bd5456: mov WORD [0x30a8476],ax ; wrapped length PERSISTED to .bss
bd545d: movzx edx,ax ; memcpy length (1..9 -> 0xfff7..0xffff)
bd5460: mov rsi,[rcx+0xe8]
bd5467: add rsi,0xa ; src = payload + 10
bd546b: mov edi,0x30a8075 ; dst = FIXED .bss global
bd5470: call 1b075e0 ; memcpy: up to ~64 KB OOB write
bd5475: mov BYTE [0x30a8478],1
Читается как предложение. Взять длину сегмента. Вычесть 10. Сохранить результат, передать его в memcpy, готово. Ни в какой момент никто не спрашивает, является ли «длина минус 10» всё ещё числом, которое имеет смысл.
Мы подсчитали каждое обращение к этому полю длины в данной функции. Тридцать. Ровно одно из них — проверка, по адресу bd4083, и всё, что она делает, — это сравнение с нулём. Сегмент нулевой длины отклоняется. Любая другая длина, включая от 1 до 9, проходит прямиком в вычитание.
Код явно полагает, что к моменту выполнения состояния 0x1F4 конечный автомат уже доставит достаточно полную запись. Конечный автомат ничего подобного не обещал. Укороченный сегмент, пришедший отдельным пакетом, удовлетворяет всем условиям диспетчеризации и оказывается на пять байт короче.
Самое раздражающее то, что производитель знает об этом классе ошибок. В другом месте того же самого бинарника sub_119c940 выполняет ту же самую арифметику «длина минус константа» и ограждает её через cmp eax,0x3f9 / ja. Безопасный шаблон лежал прямо здесь. Просто в этом месте его не применили.
Назначение, 0x30a8075, находится внутри буферной области размером 0x748 байт, перед которой есть лишь 0x533 байта легитимного пространства. Переполненное копирование выходит за её пределы в соседние глобальные переменные DataStream и согласования — вашими байтами. А сама переполненная длина сохраняется в 0x30a8476, откуда её позже считывает и доверяет ей другой код. Это второй канал повреждения, на шаг удалённый от memcpy.
Настройка — это одна строка конфигурации, стандартное развёртывание DataStream:
add lb vserver v ORACLE <ip> 1521
Бэкенд не нужен. Устройство само завершает TNS и формирует собственный пакет Accept, включая строку вендора "Netscaler" и всё остальное.
Диалог состоит из четырёх шагов:
1. TNS Connect (version 0x13A)
<- the appliance answers Resend(11), then Accept(2), 32 bytes, v0x139
2. one complete, benign TTC round-trip
(some builds like to glue your runt onto the previous segment;
this resets the segmentation so the runt lands alone)
3. the arming segment: a TNS Data packet whose declared length is
actual + 32, carrying TTC bytes {03 5E} (TTIFUN / OALL8)
-> the state machine latches [pcb+0x15a] = 0x1F4, mid-record
4. the runt: 1-9 bytes, sent as its own TCP segment
-> passes the zero check, dispatches on 0x1F4
-> length - 10 wraps
-> memcpy(0x30a8075, payload+10, ~0xFFF6)
-> the packet engine dies
Шаги с 1 по 3 — это чистый диалог. Шаг 4 — пять байт.
Это срабатывало дважды на устройстве 14.1-73.30, на одной и той же машине, одной и той же сборке, в двух независимых запусках. Оба раза пакетный движок получал SIGSEGV с соответствующим core dump, ns.log фиксировал сигнал 11 в этом окне, а dataplane простаивал в течение примерно 60-секундного тёплого перезапуска.
Негативный контроль прошёл в обоих запусках. Запустите идентичный диалог, но сделайте финальный сегмент связным и длиной 10 байт — и ничего не произойдёт. Движок его обработает и продолжит работу. Единственная переменная, которая имеет значение, — это длина огрызка.