
Pesquisa original e PoC para um estouro de buffer de pilha na senha decodificada em Base64, pré-autenticação, no login.cgi do Netis NC63
login.cgi do Netis NC63 levando a RCEPesquisador: Özcan Ersan (@ozcanpng)
CVE-2026-76070NC63_V3.0.0.3327/bin/netis.cgiPOST /cgi-bin/login.cgipassword codificado em Base64O manipulador de login público no firmware V3.0.0.3327 do Netis NC63 recupera o
parâmetro password controlado pelo atacante e o decodifica com a rotina
Base64 personalizada FUN_00402bd4. O chamador fornece um buffer local de pilha
de 64 bytes, mas não passa sua capacidade ao decodificador. O decodificador deriva seu
trabalho a partir da entrada codificada e grava os bytes decodificados sem verificar o
fim do destino.
O endereço de retorno MIPS salvo está a 136 bytes do início do buffer decodificado. Testes dinâmicos contra o CGI de produção com hash original confirmaram:
B decodificado de 140 bytes produz uma falha em 0x42424242;ra salvo por 0x0041a2e0 causa uma segunda entrada observada no
manipulador de login, comprovando o controle do contador de programa; esystem() do binário original com um valor a0 MIPS escolhido pelo atacante. O
/bin/sh substituto registrou /bin/sh -c NC63_RCE_PROOF e não executou nenhum comando.O PoC público neste repositório deliberadamente para em um padrão de crash. Ele
não contém cadeia de retorno, shellcode, comando, reverse shell ou persistência.
O script de RCE mantido separadamente em poc/exploit-db/ está preparado para uma
submissão ao Exploit-DB e destina-se apenas a testes autorizados de sistemas
afetados.
193f6a5e2ce65972b1805bf076f8d3521379a8441c8aaeb5ad0ba174bbee0792 netis_NC63_V3.0.0.3327.bin
23faa747b7d2f067aa5431bcc227ceca97a7977cf3e7c372f715cbba57f9209b squashfs-root/bin/boa
eb298774c27070dc595fefcabb4e8c12a46cb5f4fd08f91c3ca92282c3a289a2 squashfs-root/bin/netis.cgi
A cópia de /bin/netis.cgi testada dinamicamente tem o mesmo SHA-256 que o
executável extraído do fornecedor.

O frontend do fornecedor envia a palavra-passe para o endpoint público como Base64:
obj.password = base64encode(utf16to8(password));
request({
url: "/cgi-bin/login.cgi",
data: obj
});
O campo HTML usa maxlength="63", mas isso é apenas uma restrição do lado do navegador. Um cliente HTTP direto pode enviar um valor codificado maior.

O login.cgi é necessariamente acessível antes da autenticação. A decodificação insegura ocorre antes de a senha decodificada ser comparada com a senha de administrador configurada. Não é necessária nenhuma sessão válida, cabeçalho Cookie, cabeçalho Authorization ou senha correta.
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
Pseudocódigo derivado do Ghidra, com nomes normalizados para facilitar a leitura:
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;
}

O decodificador em FUN_00402bd4 recebe apenas ponteiros de destino e origem.
Seu loop avança o ponteiro de destino e armazena até três bytes decodificados
para cada quatro símbolos Base64. Nenhuma comparação verifica o destino em
relação a decoded + 64.

Base64 é a transformação de entrada, não o defeito subjacente. A causa raiz é a incompatibilidade entre o comprimento decodificado controlado pelo atacante e um destino de tamanho fixo cuja capacidade nunca é imposta. Para entrada comum com preenchimento, quatro caracteres codificados representam até três bytes decodificados; verificações do lado do servidor devem, portanto, calcular e validar o tamanho decodificado antes da escrita.
FUN_0041a2e0 começa em 0x0041a2e0 e cria um frame de 0xa8 bytes:
0041a2e0 addiu sp,sp,-168
0041a2e4 sw ra,164(sp)
0041a2e8 sw s8,160(sp)
0041a2ec move s8,sp
O destino decodificado começa em s8+0x1c; o s8 salvo e o ra salvo estão em
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
A distância exata do endereço de retorno é 0xa4 - 0x1c = 0x88, ou 136 bytes.

Um padrão B decodificado de 140 bytes substituiu o endereço de retorno salvo de quatro bytes:
--- SIGSEGV {si_signo=SIGSEGV, si_code=1, si_addr=0x42424242} ---
qemu: uncaught target signal 11 (Segmentation fault)

Um conjunto separado de entrada de 140 bytes salvou ra em 0x0041a2e0. O rastreamento de CPU do QEMU registrou uma entrada comum no primeiro handler seguida por uma segunda entrada com s8=0x41414141 e ra=0x0041a2e0.