
Pesquisa original e PoC não destrutiva para um estouro de buffer de pilha na senha decodificada em Base64 antes da autenticação no Netis NC63 login.cgi.
login.cgi levando a RCEPesquisador: Özcan Ersan (@ozcanpng)
CVE-2026-76070NC63_V3.0.0.3327/bin/netis.cgiPOST /cgi-bin/login.cgipassword codificada em Base64O manipulador de login público no firmware Netis NC63 V3.0.0.3327 obtém 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 da entrada codificada e grava 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 produz uma falha em 0x42424242;ra salvo por 0x0041a2e0 causa uma segunda entrada observada no manipulador de login, provando o controle do contador de programa; esystem() do binário original com um valor a0 MIPS selecionado 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 para deliberadamente em um padrão de falha. Ele não contém cadeia de retorno, shellcode, comando, shell reversa ou persistência.
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 do executável extraído pelo fabricante.

O frontend do fabricante envia a senha ao 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.

login.cgi é necessariamente acessível antes da autenticação. A decodificação insegura ocorre antes que a senha decodificada seja comparada com a senha de administrador configurada. Nenhuma sessão válida, cabeçalho Cookie, cabeçalho Authorization ou senha correta é necessária.
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 legibilidade:
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 de 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; portanto, verificações no lado do servidor devem calcular e validar o tamanho decodificado antes de gravar.
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; s8 salvo e 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 decodificado de 140 bytes B 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)

Uma entrada separada de 140 bytes definiu o ra salvo para 0x0041a2e0. O rastreamento de CPU do QEMU registrou uma primeira entrada comum do manipulador seguida por uma segunda entrada com s8=0x41414141 e ra=0x0041a2e0.

O binário original contém um jal system direto em 0x0041a3cc. Na validação isolada privada, instruções existentes de base fixa carregaram um marcador em a0 e alcançaram essa chamada. Um programa estático de observação foi montado sobre /bin/sh; ele registrou os argumentos do interpretador de comandos e não executou nada:
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.
Isso demonstra uma primitiva de RCE no caminho isolado do código de produção. Não estabelece confiabilidade idêntica de exploração em um roteador físico sob seu kernel implantado e configuração de randomização de pilha.
A configuração original do Boa especifica User root, Group root e um caminho CGI contendo /bin e /web/cgi-bin. O executável de produção tem base fixa (0x00400000), não tem canário de pilha nem RELRO e declara uma pilha GNU executável com segmentos RWX.


O script incluído usa como padrão o modo dry-run e apenas gera um corpo de formulário Base64 contendo 140 bytes B após a decodificação:
python3 poc/poc.py
O envio requer um alvo autorizado explícito e --send:
python3 poc/poc.py --target http://192.168.1.1 --send
O envio do padrão pode derrubar o processo CGI. Use-o apenas em um ambiente autorizado e descartável. O PoC não implementa a cadeia privada de validação de RCE.
A exploração bem-sucedida pode executar código ou comandos selecionados pelo atacante no contexto de gerenciamento do roteador. Sob a configuração original do Boa, esse contexto é executado como root. As consequências potenciais incluem divulgação de configuração e segredos, manipulação de DNS/firewall/roteamento, redirecionamento de tráfego, interrupção de serviço e comprometimento total do dispositivo.
FUN_00402bd4.Consulte evidence/README.md para capturas de tela e mapeamento de rastreamentos. Trechos normalizados do Ghidra estão em attachments/decompiled-functions/.
CVE-2026-76070 e autorizou a divulgação pública.Nenhum roteador físico foi flashado. Nenhum comando shell real, shell reversa, persistência, conexão externa, roubo de credenciais ou operação destrutiva de firmware foi utilizado.