
Ursprüngliche Forschung und PoC für einen Pre-Auth-Stack-Buffer-Overflow über ein unbegrenztes sscanf-Scanset im Netis NC63 ipFilterList-Handler
sscanf %[^,] im ipFilterList-Handler von skk_set.cgi im Netis NC63Forscher: Özcan Ersan (@ozcanpng)
CVE-2026-76071NC63_V3.0.0.3327/bin/netis.cgiPOST /cgi-bin/skk_set.cgiipFilterList=moddestHostsrcHostDer generische MIB-/Wert-Parser in der Netis-NC63-Firmware V3.0.0.3327 parst das
Feld destHost von ipFilterList mit zwei %[^,]-Scansets, jedoch ohne maximale
Feldbreiten. Jede Konvertierung schreibt in einen 16-Byte großen lokalen Stack-Buffer. Ein direkter
HTTP-Client kann daher eine lange, kommafreie Komponente übermitteln und die
gespeicherten Kontrolldaten der Funktion überschreiben.
Für die dynamisch getestete zweite destHost-Komponente liegt die gespeicherte ra exakt
112 Bytes vom lokalen Buffer entfernt. QEMU-Tracing gegen das CGI mit Original-Hash
bestätigte einen vom Angreifer gewählten dritten Eintrag bei 0x0040f7f4. Ein separater,
rein beobachtender Test leitete die Rückkehr auf den ursprünglichen system()-PLT-
Pfad bei 0x00423ab0 um und bewahrte dabei die vom Angreifer kontrollierten Request-Daten als
exaktes MIPS-a0-Argument. Eine abgesicherte /bin/sh protokollierte den Marker und führte keinen
Befehl aus.
Der öffentliche PoC enthält bewusst nur ein überlanges B-Muster. Er enthält nicht
den privaten Wert zur Kontrollflussübernahme oder die Konstruktion der Befehlsgrenze.
Das separat gepflegte RCE-Skript in poc/exploit-db/ ist für eine
Exploit-DB-Einreichung vorbereitet und ausschließlich für autorisierte Tests betroffener
Systeme vorgesehen.
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

Das Frontend des Herstellers erstellt srcHost und destHost als drei durch Kommas getrennte
Komponenten und sendet sie mit ipFilterList an /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
});

Die validierte, HTTP-förmige CGI-Anfrage enthielt keinen Cookie- oder Authorization-Header. /tmp/boa_auth fehlte, dennoch wurde die Ausführung im Parser-Pfad für ipFilterList=mod und destHost fortgesetzt. Der Speicherkorruptionsfehler ist das breitenlose Scanset in FUN_0040f7f4; der umfassendere CGI-Autorisierungsfehler ist die Bedingung, die diesen privilegierten Handler vor der Authentifizierung exponiert.

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
Die ursprünglichen libapmib.so-Metadaten ordnen sowohl srcHost als auch destHost dem Typ 0x0c zu, wodurch dieser Parser-Fall ausgewählt wird. Die dynamische Validierung wurde mit destHost durchgeführt; srcHost ist als statische Abdeckung desselben Parsers enthalten und nicht als separate dynamische Behauptung.

Normalisierter, aus Ghidra abgeleiteter Pseudocode:
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() ist nicht an sich die Schwachstelle. Der Defekt besteht darin, dass %[^,] keine maximale Feldbreite hat, sodass sscanf nicht weiß, dass jedes Ziel nur 16 Bytes groß ist. Ein kapazitätsbewusstes Format würde eine Breite wie %15[^,] verwenden, überprüfen, dass genau drei Konvertierungen erfolgreich waren, und anschließend die geparsten Adresswerte validieren. Dies ist ein Beispiel für eine Gegenmaßnahme, kein Hersteller-Patch.
FUN_0040f7f4 beginnt bei 0x0040f7f4 und erzeugt einen 0x1d0-Byte großen Frame:
0040f7f4 addiu sp,sp,-0x1d0
0040f7f8 sw ra,0x1cc(sp)
0040f7fc sw fp,0x1c8(sp)
0040f800 sw s0,0x1c4(sp)
Die Ziele vom Typ 0x0c befinden sich bei fp+0x14c und fp+0x15c. Das gespeicherte ra liegt bei fp+0x1cc, woraus sich der exakte Abstand zum zweiten Puffer ergibt:
0x1cc - 0x15c = 0x70 = 112 bytes

Der isolierte PC-Nachweis verwendete 112 Füllbytes, gefolgt von den drei niedrigen Little-Endian-Bytes von 0x0040f7f4; der sscanf-Terminator lieferte das vierte Nullbyte. QEMU beobachtete zwei gewöhnliche Parser-Einträge, gefolgt von einem dritten Eintrag, der durch die überschriebene Rücksprungadresse verursacht wurde: