
Investigación original y PoC no destructivo para un desbordamiento de búfer de pila pre-autenticación en la contraseña decodificada en Base64 en login.cgi de Netis NC63.
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 manejador público de inicio de sesión del firmware Netis NC63 V3.0.0.3327 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 MIPS guardada está a 136 bytes del comienzo del búfer decodificado. Las pruebas dinámicas contra el CGI de producción con hash original confirmaron:
B produce una falla en 0x42424242;ra guardado con 0x0041a2e0 provoca una segunda entrada observada en el manejador de inicio de sesión, lo que demuestra el control del contador de programa; ysystem() del binario original con un valor a0 MIPS seleccionado por el atacante. El /bin/sh de reemplazo registró /bin/sh -c NC63_RCE_PROOF y no ejecutó ningún comando.La PoC pública de este repositorio se detiene deliberadamente en un patrón de fallo. No contiene cadena de retorno, shellcode, comando, shell inversa ni persistencia.
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, encabezado Cookie, encabezado Authorization ni 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 solo 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 comprueba el destino contra decoded + 64.

Base64 es la transformación de entrada, no el defecto subyacente. La causa raíz es el desajuste entre la longitud decodificada controlada por el atacante y un destino de tamaño fijo cuya capacidad nunca se aplica. Para una entrada normal con relleno, 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; el s8 guardado y el 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, es decir, 136 bytes.

Un patrón decodificado de 140 bytes de B 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)

Una entrada separada de 140 bytes estableció el ra guardado en 0x0041a2e0. El rastreo de CPU de QEMU registró una primera entrada ordinaria del manejador seguida de una segunda entrada con s8=0x41414141 y ra=0x0041a2e0.

El binario original contiene un jal system directo en 0x0041a3cc. En la validación aislada privada, las instrucciones existentes de base fija cargaron un marcador en a0 y alcanzaron esa llamada. Se montó un programa estático de observación sobre /bin/sh; registró los argumentos del intérprete de comandos y no ejecutó nada:
argv[0]=</bin/sh>
argv[1]=<-c>
argv[2]=<NC63_RCE_PROOF>
CONTROLLED_MARKER_REACHED
PASS: attacker-controlled a0 reached system() and /bin/sh argv.
PASS: the guard logged the request and executed no command.
Esto demuestra una primitiva de RCE en la ruta de código de producción aislada. No establece una fiabilidad de explotación idéntica en un router físico bajo su kernel implementado y su configuración de aleatorización de pila.
La configuración original de Boa especifica User root, Group root y una ruta CGI que contiene /bin y /web/cgi-bin. El ejecutable de producción es de base fija (0x00400000), no tiene canario de pila ni RELRO, y declara una pila GNU ejecutable con segmentos RWX.


El script incluido usa por defecto el modo de ejecución en seco y solo genera un cuerpo de formulario Base64 que contiene 140 bytes de B después de la decodificación:
python3 poc/poc.py
El envío requiere un objetivo autorizado explícito y --send:
python3 poc/poc.py --target http://192.168.1.1 --send
Enviar el patrón puede hacer fallar el proceso CGI. Úselo solo en un entorno autorizado y desechable. La PoC no implementa la cadena de validación RCE privada.
Una explotación exitosa puede ejecutar código o comandos seleccionados por el atacante en el contexto de gestión del router. Con la configuración original de Boa, ese contexto se ejecuta como root. Las consecuencias potenciales incluyen la divulgación de configuración y secretos, la manipulación de DNS/firewall/enrutamiento, la redirección de tráfico, la interrupción del servicio y el compromiso total del dispositivo.
FUN_00402bd4.Consulte evidence/README.md para ver las capturas de pantalla y el mapeo de trazados. Los extractos normalizados de Ghidra se encuentran en attachments/decompiled-functions/.
CVE-2026-76070 y autorizó la divulgación pública.No se flasheó ningún router físico. No se utilizó ningún comando shell real, shell inversa, persistencia, conexión externa, robo de credenciales ni operación destructiva de firmware.