
Ricerca originale e PoC per un buffer overflow su stack pre-auth tramite scanset sscanf senza limiti nel gestore ipFilterList del Netis NC63
sscanf %[^,] nel gestore ipFilterList di skk_set.cgi in Netis NC63Ricercatore: Özcan Ersan (@ozcanpng)
CVE-2026-76071NC63_V3.0.0.3327/bin/netis.cgiPOST /cgi-bin/skk_set.cgiipFilterList=moddestHostsrcHostIl parser generico MIB/valore nel firmware Netis NC63 V3.0.0.3327 analizza il
campo destHost di ipFilterList con due scanset %[^,] ma senza larghezze
massime di campo. Ogni conversione scrive in un buffer locale sullo stack di 16
byte. Un client HTTP diretto può quindi inviare una componente lunga priva di
virgole e sovrascrivere i dati di controllo salvati della funzione.
Per la seconda componente destHost testata dinamicamente, il registro ra
salvato si trova esattamente a 112 byte dal buffer locale. Il tracciamento QEMU
sul CGI con hash originale ha confermato una terza voce scelta dall'attaccante
all'indirizzo 0x0040f7f4. Un test separato di sola osservazione ha reindirizzato
il ritorno al percorso PLT originale di system() all'indirizzo 0x00423ab0,
preservando i dati della richiesta controllati dall'attaccante come esatto
argomento a0 MIPS. Una /bin/sh protetta ha registrato il marker e non ha
eseguito alcun comando.
Il PoC pubblico contiene deliberatamente solo un pattern B sovradimensionato.
Non include il valore privato di trasferimento del controllo né la costruzione
del confine di comando. Lo script RCE mantenuto separatamente in
poc/exploit-db/ è preparato per una sottomissione a Exploit-DB ed è destinato
esclusivamente a test autorizzati sui sistemi interessati.
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

Il frontend del fornitore costruisce srcHost e destHost come tre componenti
separati da virgola e li invia 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 richiesta CGI con forma HTTP validata non conteneva alcun header Cookie o Authorization. /tmp/boa_auth era assente, eppure l'esecuzione è proseguita nel percorso del parser ipFilterList=mod e destHost. Il difetto di corruzione della memoria è lo scanset senza larghezza in FUN_0040f7f4; il più ampio fallimento di autorizzazione CGI è la condizione che espone questo handler privilegiato in fase pre-autenticazione.

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
I metadati originali di libapmib.so mappano sia srcHost che destHost al tipo
0x0c, che seleziona questo caso del parser. La validazione dinamica è stata eseguita con
destHost; srcHost è incluso come copertura statica dello stesso parser anziché come
affermazione dinamica separata.

Pseudocodice normalizzato derivato da 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() non è intrinsecamente la vulnerabilità. Il difetto è che %[^,]
non ha una larghezza massima del campo, quindi sscanf non sa che ogni destinazione
è di soli 16 byte. Un formato consapevole della capacità userebbe una larghezza come
%15[^,], verificherebbe che esattamente tre conversioni siano riuscite, e poi validerebbe
i valori degli indirizzi analizzati. Questo è un esempio di mitigazione, non una patch del fornitore.
FUN_0040f7f4 inizia a 0x0040f7f4 e crea un frame di 0x1d0 byte:
0040f7f4 addiu sp,sp,-0x1d0
0040f7f8 sw ra,0x1cc(sp)
0040f7fc sw fp,0x1c8(sp)
0040f800 sw s0,0x1c4(sp)
Le destinazioni di tipo 0x0c si trovano a fp+0x14c e fp+0x15c. Il registro ra salvato è
a fp+0x1cc, il che rende la distanza esatta dal secondo buffer:
0x1cc - 0x15c = 0x70 = 112 bytes

La prova isolata del PC ha utilizzato 112 byte di padding seguiti dai tre byte bassi
little-endian di 0x0040f7f4; il terminatore di sscanf ha fornito il
quarto byte zero. QEMU ha osservato due normali voci del parser seguite da una
terza voce causata dall'indirizzo di ritorno sovrascritto: