
Ursprüngliche Forschung und PoC für einen Pre-Auth-Stack-Buffer-Overflow bei der Base64-dekodierten Passwortverarbeitung 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
vom Angreifer kontrollierten Parameter password ab und dekodiert ihn mit der
benutzerdefinierten Base64-Routine FUN_00402bd4. Der Aufrufer stellt einen
lokalen Stack-Puffer von 64 Byte bereit, übergibt dessen Kapazität jedoch nicht
an den Decoder. Der Decoder leitet seinen Arbeitsumfang 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 Byte vom Anfang des dekodierten Puffers entfernt. Dynamische Tests gegen das Produktions-CGI mit Original-Hash bestätigten:
B erzeugt einen Fehler bei 0x42424242;ra durch 0x0041a2e0 bewirkt einen zweiten
beobachteten Eintritt in den Login-Handler und belegt damit die Kontrolle
über den Programmzähler; undsystem()-Aufruf des Original-Binaries mit einem vom Angreifer gewählten
MIPS-Wert in a0. Das ersetzte /bin/sh protokollierte
/bin/sh -c NC63_RCE_PROOF und führte keinen Befehl aus.Der öffentliche PoC in diesem Repository endet bewusst bei einem Absturzmuster.
Er enthält keine Rücksprungkette, keinen Shellcode, keinen Befehl, keine
Reverse Shell und keine Persistenz. 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
23faa747b7d2f067aa5431bcc227ceca97a7977cf3e7c372f715cbba57f9209b squashfs-root/bin/boa
eb298774c27070dc595fefcabb4e8c12a46cb5f4fd08f91c3ca92282c3a289a2 squashfs-root/bin/netis.cgi
Die dynamisch getestete Kopie von /bin/netis.cgi hat denselben SHA-256 wie
die aus dem Hersteller-Image extrahierte ausführbare Datei.

Das Hersteller-Frontend 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
browserseitige Einschränkung. 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 Administratorkennwort verglichen wird. Es ist keine gültige
Sitzung, kein Cookie-Header, kein Authorization-Header und kein korrektes
Passwort erforderlich.
Unauthenticated HTTP client
|
| POST /cgi-bin/login.cgi
| password=<attacker-controlled Base64>
v
/bin/netis.cgi: FUN_0041a2e0
|
| get_request_param("password")
v
FUN_00402bd4(decoded_stack_buffer, encoded_password)
|
| no destination-capacity argument
| decoded output exceeds 64 bytes
v
saved s8 at decoded offset 132
saved ra at decoded offset 136
|
v
attacker-selected MIPS PC
Aus Ghidra abgeleiteter Pseudocode, mit zur besseren 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); /* no capacity argument */
apmib_get(0x15e, stored);
if (strcmp(decoded, stored) == 0)
printf("[\"SUCCESS\"]");
else {
system("echo 0 >/tmp/boa_auth");
printf("[\"%d\"]", 0x15);
}
return 0;
}

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

Base64 ist die Eingabetransformation, nicht der zugrunde liegende Defekt. Die Ursache ist die Diskrepanz zwischen der vom Angreifer kontrollierten dekodierten Länge und einem Ziel mit fester Größe, dessen Kapazität nie durchgesetzt wird. Bei gewöhnlicher gepolsterter 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 beginnt 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 und s8+0xa4:
decoded[64] s8+0x1c decoded offset 0
saved s8 s8+0xa0 decoded offset 132
saved ra s8+0xa4 decoded offset 136
Der exakte Return-Address-Abstand ist 0xa4 - 0x1c = 0x88, also 136 Bytes.

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