
Investigación original y PoC para un desbordamiento de búfer en la pila de contraseñas decodificadas en Base64 previo a la autenticación en Netis NC63 login.cgi
login.cgi de Netis NC63 que conduce a RCEInvestigador: Özcan Ersan (@ozcanpng)
CVE-2026-76070NC63_V3.0.0.3327/bin/netis.cgiPOST /cgi-bin/login.cgipassword codificado en Base64El controlador de inicio de sesión público en el firmware V3.0.0.3327 del Netis NC63 recupera el
parámetro password controlado por el atacante y lo decodifica con la rutina
Base64 personalizada FUN_00402bd4. El llamador proporciona un búfer local de pila
de 64 bytes pero no pasa su capacidad al decodificador. El decodificador deriva su
trabajo de la entrada codificada y escribe los bytes decodificados sin comprobar el final
del destino.
La dirección de retorno guardada de MIPS está a 136 bytes desde el inicio del búfer decodificado. Las pruebas dinámicas contra el CGI de producción con hash original confirmaron:
B produce un fallo en 0x42424242;ra guardada con 0x0041a2e0 provoca una segunda entrada observada en
el controlador de inicio de sesión, lo que demuestra el control del contador de programa; ysystem() del binario
original con un valor a0 de MIPS seleccionado por el atacante. El /bin/sh de reemplazo
registró /bin/sh -c NC63_RCE_PROOF y no ejecutó ningún comando.El PoC público en este repositorio se detiene deliberadamente en un patrón de fallo. No
contiene cadena de retorno, shellcode, comando, shell inversa ni persistencia.
El script de RCE mantenido por separado en poc/exploit-db/ está preparado para un
envío a Exploit-DB y está destinado únicamente a pruebas autorizadas de sistemas
afectados.
193f6a5e2ce65972b1805bf076f8d3521379a8441c8aaeb5ad0ba174bbee0792 netis_NC63_V3.0.0.3327.bin
23faa747b7d2f067aa5431bcc227ceca97a7977cf3e7c372f715cbba57f9209b squashfs-root/bin/boa
eb298774c27070dc595fefcabb4e8c12a46cb5f4fd08f91c3ca92282c3a289a2 squashfs-root/bin/netis.cgi
La copia de /bin/netis.cgi probada dinámicamente tiene el mismo SHA-256 que el ejecutable extraído del proveedor.

El frontend del proveedor envía la contraseña al endpoint público como Base64:
obj.password = base64encode(utf16to8(password));
request({
url: "/cgi-bin/login.cgi",
data: obj
});
El campo HTML usa maxlength="63", pero eso es solo una restricción del lado del navegador. Un cliente HTTP directo puede enviar un valor codificado más grande.

login.cgi es necesariamente accesible antes de la autenticación. La decodificación insegura ocurre antes de que la contraseña decodificada se compare con la contraseña de administrador configurada. No se requiere una sesión válida, el encabezado Cookie, el encabezado Authorization ni la contraseña correcta.
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
Pseudocódigo derivado de Ghidra, con nombres normalizados para facilitar la lectura:
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;
}

El decodificador en FUN_00402bd4 recibe únicamente punteros de destino y origen.
Su bucle avanza el puntero de destino y almacena hasta tres bytes decodificados
por cada cuatro símbolos Base64. Ninguna comparación verifica el destino contra
decoded + 64.

Base64 es la transformación de entrada, no el defecto subyacente. La causa raíz es la discrepancia entre la longitud decodificada controlada por el atacante y un destino de tamaño fijo cuya capacidad nunca se aplica. Para una entrada con relleno ordinaria, cuatro caracteres codificados representan hasta tres bytes decodificados; por lo tanto, las comprobaciones del lado del servidor deben calcular y validar el tamaño decodificado antes de escribir.
FUN_0041a2e0 comienza en 0x0041a2e0 y crea un marco de 0xa8 bytes:
0041a2e0 addiu sp,sp,-168
0041a2e4 sw ra,164(sp)
0041a2e8 sw s8,160(sp)
0041a2ec move s8,sp
El destino decodificado comienza en s8+0x1c; s8 guardado y ra guardado están en
s8+0xa0 y s8+0xa4:
decoded[64] s8+0x1c decoded offset 0
saved s8 s8+0xa0 decoded offset 132
saved ra s8+0xa4 decoded offset 136
La distancia exacta de la dirección de retorno es 0xa4 - 0x1c = 0x88, o 136 bytes.

Un patrón B decodificado de 140 bytes reemplazó la dirección de retorno guardada de cuatro bytes:
--- SIGSEGV {si_signo=SIGSEGV, si_code=1, si_addr=0x42424242} ---
qemu: uncaught target signal 11 (Segmentation fault)