
Recherche originale et PoC pour un débordement de tampon de pile avant authentification via un scanset sscanf non borné dans le gestionnaire ipFilterList du Netis NC63
sscanf %[^,] dans le gestionnaire ipFilterList de skk_set.cgi dans Netis NC63Chercheur : Özcan Ersan (@ozcanpng)
CVE-2026-76071NC63_V3.0.0.3327/bin/netis.cgiPOST /cgi-bin/skk_set.cgiipFilterList=moddestHostsrcHostL'analyseur générique MIB/valeur du firmware Netis NC63 V3.0.0.3327 analyse le
champ destHost de ipFilterList avec deux jeux de caractères %[^,] mais sans
largeurs de champ maximales. Chaque conversion écrit dans un tampon local de 16 octets
sur la pile. Un client HTTP direct peut donc soumettre un long composant sans virgule
et écraser les données de contrôle sauvegardées de la fonction.
Pour le second composant destHost testé dynamiquement, le registre ra sauvegardé se trouve exactement
à 112 octets du tampon local. Le traçage QEMU sur le CGI au hachage d'origine
a confirmé une troisième entrée choisie par l'attaquant à 0x0040f7f4. Un test
d'observation seule distinct a redirigé le retour vers le chemin PLT system() d'origine
à 0x00423ab0 tout en préservant les données de requête contrôlées par l'attaquant comme
argument a0 MIPS exact. Un /bin/sh protégé a journalisé le marqueur et n'a exécuté aucune
commande.
Le PoC public contient délibérément uniquement un motif B surdimensionné. Il n'inclut
pas la valeur privée de transfert de contrôle ni la construction de la frontière de commande.
Le script RCE maintenu séparément dans poc/exploit-db/ est préparé pour une
soumission à Exploit-DB et est destiné uniquement aux tests autorisés des systèmes
affectés.
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

Le frontend du fournisseur construit srcHost et destHost sous forme de trois
composants séparés par des virgules et les envoie avec 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
});

La requête CGI de forme HTTP validée ne contenait aucun en-tête Cookie ni Authorization. /tmp/boa_auth était absent, pourtant l'exécution s'est poursuivie dans le chemin du parseur ipFilterList=mod et destHost. Le défaut de corruption de mémoire est le scanset sans largeur dans FUN_0040f7f4 ; la défaillance d'autorisation CGI plus large est la condition qui expose ce gestionnaire privilégié avant authentification.

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
Les métadonnées d'origine de libapmib.so associent à la fois srcHost et destHost au type 0x0c, ce qui sélectionne ce cas du parseur. La validation dynamique a été effectuée avec destHost ; srcHost est inclus comme couverture statique du même parseur plutôt que comme une affirmation dynamique distincte.

Pseudocode normalisé dérivé 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() n'est pas intrinsèquement la vulnérabilité. Le défaut est que %[^,]
n'a pas de largeur de champ maximale, donc sscanf n'a aucune connaissance que chaque destination
ne fait que 16 octets. Un format tenant compte de la capacité utiliserait une largeur telle que
%15[^,], vérifierait que exactement trois conversions ont réussi, puis validerait
les valeurs d'adresse analysées. Il s'agit d'un exemple d'atténuation, et non d'un correctif du fournisseur.
FUN_0040f7f4 commence à 0x0040f7f4 et crée un cadre de 0x1d0 octets :
0040f7f4 addiu sp,sp,-0x1d0
0040f7f8 sw ra,0x1cc(sp)
0040f7fc sw fp,0x1c8(sp)
0040f800 sw s0,0x1c4(sp)
Les destinations de type 0x0c se trouvent à fp+0x14c et fp+0x15c. Le ra sauvegardé est
à fp+0x1cc, ce qui donne la distance exacte depuis le second tampon :
0x1cc - 0x15c = 0x70 = 112 bytes
