
Ricerca originale e PoC per un buffer overflow nello stack della password decodificata in Base64, pre-autenticazione, in Netis NC63 login.cgi
login.cgi di Netis NC63 che porta a RCERicercatore: Özcan Ersan (@ozcanpng)
CVE-2026-76070NC63_V3.0.0.3327/bin/netis.cgiPOST /cgi-bin/login.cgipassword codificata in Base64Il gestore pubblico del login nel firmware Netis NC63 V3.0.0.3327 recupera il
parametro password controllato dall'attaccante e lo decodifica con la routine
Base64 personalizzata FUN_00402bd4. Il chiamante fornisce un buffer locale
sullo stack di 64 byte ma non ne passa la capacità al decodificatore. Il
decodificatore deriva il proprio lavoro dall'input codificato e scrive i byte
decodificati senza verificare la fine della destinazione.
L'indirizzo di ritorno MIPS salvato si trova a 136 byte dall'inizio del buffer decodificato. I test dinamici contro il CGI di produzione con hash originale hanno confermato:
B decodificato di 140 byte produce un fault a 0x42424242;ra salvato con 0x0041a2e0 si causa una seconda entrata
osservata nel gestore del login, dimostrando il controllo del program counter; esystem() del binario originale con un valore a0 MIPS scelto
dall'attaccante. Il /bin/sh sostitutivo ha registrato /bin/sh -c NC63_RCE_PROOF e non ha eseguito alcun comando.Il PoC pubblico in questo repository si ferma deliberatamente a un pattern di
crash. Non contiene catene di ritorno, shellcode, comandi, reverse shell o
persistenza. Lo script RCE mantenuto separatamente in poc/exploit-db/ è
preparato per una sottomissione a Exploit-DB ed è destinato esclusivamente al
test autorizzato dei sistemi interessati.
193f6a5e2ce65972b1805bf076f8d3521379a8441c8aaeb5ad0ba174bbee0792 netis_NC63_V3.0.0.3327.bin
23faa747b7d2f067aa5431bcc227ceca97a7977cf3e7c372f715cbba57f9209b squashfs-root/bin/boa
eb298774c27070dc595fefcabb4e8c12a46cb5f4fd08f91c3ca92282c3a289a2 squashfs-root/bin/netis.cgi
La copia di /bin/netis.cgi testata dinamicamente ha lo stesso SHA-256 dell'eseguibile estratto dal vendor.

Il frontend del vendor invia la password all'endpoint pubblico come Base64:
obj.password = base64encode(utf16to8(password));
request({
url: "/cgi-bin/login.cgi",
data: obj
});
Il campo HTML usa maxlength="63", ma si tratta solo di una restrizione lato browser. Un client HTTP diretto può inviare un valore codificato più grande.

login.cgi è necessariamente raggiungibile prima dell'autenticazione. La decodifica non sicura avviene prima che la password decodificata venga confrontata con la password dell'amministratore configurata. Non sono richiesti una sessione valida, un header Cookie, un header Authorization o la password corretta.
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
Pseudo-codice derivato da Ghidra, con nomi normalizzati per leggibilità:
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;
}

Il decoder in FUN_00402bd4 riceve solo i puntatori di destinazione e sorgente.
Il suo ciclo avanza il puntatore di destinazione e memorizza fino a tre byte decodificati
per ogni quattro simboli Base64. Nessun confronto verifica la destinazione rispetto a
decoded + 64.

Base64 è la trasformazione dell'input, non il difetto sottostante. La causa radice è la discrepanza tra la lunghezza decodificata controllata dall'attaccante e una destinazione di dimensione fissa la cui capacità non viene mai applicata. Per input ordinari con padding, quattro caratteri codificati rappresentano fino a tre byte decodificati; i controlli lato server devono quindi calcolare e validare la dimensione decodificata prima della scrittura.
FUN_0041a2e0 inizia a 0x0041a2e0 e crea un frame di 0xa8 byte:
0041a2e0 addiu sp,sp,-168
0041a2e4 sw ra,164(sp)
0041a2e8 sw s8,160(sp)
0041a2ec move s8,sp
La destinazione decodificata inizia a s8+0x1c; i valori salvati s8 e ra si trovano rispettivamente a
s8+0xa0 e s8+0xa4:
decoded[64] s8+0x1c decoded offset 0
saved s8 s8+0xa0 decoded offset 132
saved ra s8+0xa4 decoded offset 136
La distanza esatta del return-address è 0xa4 - 0x1c = 0x88, ovvero 136 byte.

Un pattern B decodificato di 140 byte ha sostituito il return address salvato di quattro byte:
--- SIGSEGV {si_signo=SIGSEGV, si_code=1, si_addr=0x42424242} ---
qemu: uncaught target signal 11 (Segmentation fault)
