
Оригинальное исследование и 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 с помощью двух scanset-спецификаторов %[^,], но без
ограничения максимальной ширины полей. Каждое преобразование записывает данные в 16-байтовый локальный буфер стека. Прямой
HTTP-клиент может, таким образом, отправить длинный компонент без запятых и перезаписать
сохранённые управляющие данные функции.
Для динамически протестированного второго компонента destHost сохранённый ra находится ровно
в 112 байтах от локального буфера. Трассировка QEMU на CGI с оригинальным хешем
подтвердила выбранную атакующим третью запись по адресу 0x0040f7f4. Отдельный
тест только для наблюдения перенаправил возврат на исходный путь PLT system()
по адресу 0x00423ab0, сохранив контролируемые атакующим данные запроса в качестве
точного аргумента MIPS a0. Защищённый /bin/sh зарегистрировал маркер и не выполнил никакой
команды.
Публичный PoC намеренно содержит только чрезмерно длинный шаблон B. Он не
включает приватное значение передачи управления или конструкцию границы команды.
Отдельно поддерживаемый скрипт RCE в poc/exploit-db/ подготовлен для отправки
в Exploit-DB и предназначен только для авторизованного тестирования затронутых
систем.
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. Дефект повреждения памяти —
это scanset без указания ширины в 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 зафиксировал две обычные записи парсера, за которыми последовала третья запись, вызванная перезаписанным адресом возврата: