
Оригинальное исследование и PoC для переполнения буфера стека пароля, декодированного из Base64, до аутентификации в Netis NC63 login.cgi
login.cgi, приводящее к RCEИсследователь: Özcan Ersan (@ozcanpng)
CVE-2026-76070NC63_V3.0.0.3327/bin/netis.cgiPOST /cgi-bin/login.cgipassword в кодировке Base64Публичный обработчик входа в прошивке Netis NC63 V3.0.0.3327 извлекает
контролируемый атакующим параметр password и декодирует его с помощью
собственной процедуры Base64 FUN_00402bd4. Вызывающая сторона предоставляет
64-байтовый локальный буфер стека, но не передаёт его ёмкость декодеру. Декодер
определяет объём работы по закодированным входным данным и записывает
декодированные байты без проверки конца назначения.
Сохранённый адрес возврата MIPS находится на расстоянии 136 байт от начала декодированного буфера. Динамические тесты против производственного CGI с оригинальным хешем подтвердили:
B вызывает сбой по адресу 0x42424242;ra на 0x0041a2e0 вызывает второе наблюдаемое
попадание в обработчик входа, что доказывает контроль над счётчиком команд; иsystem() в оригинальном бинарнике с выбранным атакующим значением MIPS a0.
Подменённый /bin/sh зарегистрировал /bin/sh -c NC63_RCE_PROOF и не
выполнил никакой команды.Публичный PoC в этом репозитории намеренно останавливается на шаблоне сбоя. Он
не содержит цепочки возврата, шелл-кода, команды, обратной оболочки или
механизма сохранения. Отдельно поддерживаемый скрипт RCE в poc/exploit-db/
подготовлен для отправки в Exploit-DB и предназначен только для
авторизованного тестирования затронутых систем.
193f6a5e2ce65972b1805bf076f8d3521379a8441c8aaeb5ad0ba174bbee0792 netis_NC63_V3.0.0.3327.bin
23faa747b7d2f067aa5431bcc227ceca97a7977cf3e7c372f715cbba57f9209b squashfs-root/bin/boa
eb298774c27070dc595fefcabb4e8c12a46cb5f4fd08f91c3ca92282c3a289a2 squashfs-root/bin/netis.cgi
Динамически протестированная копия /bin/netis.cgi имеет тот же SHA-256, что и
исполняемый файл, извлечённый из прошивки вендора.

Вендорский фронтенд отправляет пароль на публичную конечную точку в виде Base64:
obj.password = base64encode(utf16to8(password));
request({
url: "/cgi-bin/login.cgi",
data: obj
});
Поле HTML использует maxlength="63", но это лишь ограничение на стороне браузера. Прямой HTTP-клиент может отправить закодированное значение большего размера.

login.cgi неизбежно доступен до аутентификации. Небезопасное декодирование происходит до того, как декодированный пароль сравнивается с настроенным паролем администратора. Не требуется ни действительная сессия, ни заголовок Cookie, ни заголовок Authorization, ни правильный пароль.
Unauthenticated HTTP client
|
| POST /cgi-bin/login.cgi
| password=<attacker-controlled Base64>
v
/bin/netis.cgi: FUN_0041a2e0
|
| get_request_param("password")
v
FUN_00402bd4(decoded_stack_buffer, encoded_password)
|
| no destination-capacity argument
| decoded output exceeds 64 bytes
v
saved s8 at decoded offset 132
saved ra at decoded offset 136
|
v
attacker-selected MIPS PC
Псевдокод, полученный из Ghidra, с именами, нормализованными для удобочитаемости:
int login_cgi(void *request)
{
char decoded[64];
char stored[68];
char *password;
memset(decoded, 0, 64);
memset(stored, 0, 64);
password = get_request_param(request, "password");
if (password != NULL)
FUN_00402bd4(decoded, password); /* no capacity argument */
apmib_get(0x15e, stored);
if (strcmp(decoded, stored) == 0)
printf("[\"SUCCESS\"]");
else {
system("echo 0 >/tmp/boa_auth");
printf("[\"%d\"]", 0x15);
}
return 0;
}

Декодер в FUN_00402bd4 получает только указатели на назначение и источник.
Его цикл продвигает указатель назначения и сохраняет до трёх декодированных байт
на каждые четыре символа Base64. Никакая проверка не сравнивает назначение с
decoded + 64.

Base64 — это преобразование входных данных, а не основная уязвимость. Корневая причина — несоответствие между контролируемой злоумышленником длиной декодированных данных и буфером назначения фиксированного размера, ёмкость которого никогда не проверяется. Для обычных входных данных с дополнением четыре закодированных символа представляют до трёх декодированных байт; поэтому серверные проверки должны вычислять и валидировать размер декодированных данных перед записью.
FUN_0041a2e0 начинается с 0x0041a2e0 и создаёт кадр размером 0xa8 байт:
0041a2e0 addiu sp,sp,-168
0041a2e4 sw ra,164(sp)
0041a2e8 sw s8,160(sp)
0041a2ec move s8,sp
Декодированный адрес назначения начинается с s8+0x1c; сохранённые s8 и сохранённый ra находятся по адресам
s8+0xa0 и s8+0xa4:
decoded[64] s8+0x1c decoded offset 0
saved s8 s8+0xa0 decoded offset 132
saved ra s8+0xa4 decoded offset 136
Точное расстояние до адреса возврата составляет 0xa4 - 0x1c = 0x88, или 136 байт.

Декодированный шаблон B длиной 140 байт заменил четырёхбайтовый сохранённый адрес возврата:
--- SIGSEGV {si_signo=SIGSEGV, si_code=1, si_addr=0x42424242} ---
qemu: uncaught target signal 11 (Segmentation fault)
