CVE-2026-76071: Unauthenticated Pre-Auth Stack Buffer Overflow via sscanf %[^,] in skk_set.cgi ipFilterList Handler in Netis NC63
On this page
- Netis NC63 | CVE-2026-76071 | Unauthenticated Pre-Auth Stack Buffer Overflow via sscanf %[^,] in skk_set.cgi ipFilterList
- Vulnerability: Pre-Authentication Stack Buffer Overflow → Remote Code Execution Primitive
- CVE: CVE-2026-76071
- CVE Status: Assigned by VulnCheck; record population pending at the time of writing
- Researcher: Özcan Ersan (@ozcanpng)
- Introduction
- Affected Target
- Short Version
- Frontend and External Request Format
- Authentication Boundary
- Handler and Metadata Route
- Root Cause: Missing Scanset Width
- Stack Corruption Analysis
- Dynamic Verification
- Controlled Program Counter
- Observation-Only RCE Boundary
- Runtime Scope and Limitations
- Safe Public Proof of Concept
- Impact
- Classification
- Mitigation
- Disclosure Timeline
- Disclosure Note
- References
Netis NC63 | CVE-2026-76071 | Unauthenticated Pre-Auth Stack Buffer Overflow via sscanf %[^,] in skk_set.cgi ipFilterList
Vulnerability: Pre-Authentication Stack Buffer Overflow → Remote Code Execution Primitive
CVE: CVE-2026-76071
CVE Status: Assigned by VulnCheck; record population pending at the time of writing
Researcher: Özcan Ersan (@ozcanpng)
Introduction
CVE-2026-76071 is a pre-authentication stack-based buffer overflow in the generic MIB/value parser used by the Netis NC63 /cgi-bin/skk_set.cgi endpoint.
When the request selects ipFilterList=mod, attacker-controlled destHost is routed into FUN_0040f7f4. The type-0x0c parser splits the value with two %[^,] conversions. Both destination arrays are 16 bytes, but neither scanset has a maximum field width.
The defect is not that the firmware uses sscanf() at all. The root cause is a capacity-unaware scanset format: %[^,] continues copying non-comma bytes regardless of the destination size. A direct HTTP request can therefore overwrite saved stack state.
Dynamic testing against the original-hash production CGI confirmed saved return-address and PC control. A separate observation-only validation reached the original binary’s system() PLT path with an attacker-controlled a0 marker while a guarded /bin/sh logged the arguments and executed nothing.
Affected Target
| Field | Value |
|---|---|
| Vendor | Netis Systems Co., Ltd. |
| Product | Netis NC63 AC1200 Wireless Dual Band Gigabit MU-MIMO Router |
| Firmware | NC63_V3.0.0.3327 |
| Architecture | MIPS32r2 little-endian, o32 ABI, uClibc |
| CGI binary | /bin/netis.cgi |
| Endpoint | POST /cgi-bin/skk_set.cgi |
| Trigger | ipFilterList=mod |
| Dynamically tested parameter | destHost |
| Static same-parser coverage | srcHost |
| Authentication | None observed or required in the validated path |
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

Short Version
Unauthenticated HTTP client
|
| POST /cgi-bin/skk_set.cgi
| ipFilterList=mod
| destHost=1,0.0.0.0,<attacker-controlled comma-free bytes>
v
FUN_004138a0
v
FUN_004134c8
v
FUN_00410898(request, "ipFilterList")
v
FUN_0040f7f4(request, trigger, mib_table, pMib)
|
| get_request_param("destHost")
v
sscanf(value, "%d,%[^,],%[^,]", ...)
|
| two char[16] destinations
| no maximum scanset width
v
saved fp overwrite -> saved ra overwrite -> controlled MIPS PC
The dynamically tested second component is 112 bytes from saved ra.
Frontend and External Request Format
The extracted vendor frontend constructs srcHost and destHost as three comma-separated fields and sends them with ipFilterList:
param.srcHost = $("#src_host").val();
param.srcHost += "," + $("#src_ip_1").val();
param.srcHost += "," + $("#src_ip_2").val();
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
});

Browser-side IP validation does not constrain a direct client. The server must enforce the destination width independently, but this parser does not.
Authentication Boundary
The HTTP-shaped CGI validation request contained no Cookie or Authorization header. The disposable root filesystem deliberately had no /tmp/boa_auth, and QEMU recorded:
open("/tmp/boa_auth", O_RDONLY) = -1 ENOENT
Execution nevertheless continued through skk_set.cgi, ipFilterList=mod, and the vulnerable destHost conversion.

The broad CGI authorization failure is the enabling authentication condition. The independent memory-corruption root cause assigned CVE-2026-76071 is the widthless type-0x0c scanset parser in FUN_0040f7f4.
Handler and Metadata Route
Static analysis mapped the request as follows:
POST /cgi-bin/skk_set.cgi
-> top-level route row at 0x00434080
-> FUN_004138a0
-> FUN_004134c8 (trigger: ipFilterList)
-> FUN_00410898(request, "ipFilterList")
-> FUN_0040f7f4(request, trigger, mib_table, pMib)
The original libapmib.so metadata includes:
id=0x8f name='srcHost' type=0x0c dst=0x21
id=0x90 name='destHost' type=0x0c dst=0x2a
Both names select the same vulnerable parser case. Dynamic validation was performed with destHost; srcHost is a static same-root-cause observation rather than a separate exploit claim.

Root Cause: Missing Scanset Width
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;

%[^,] consumes every non-comma byte until a comma or terminator. Because no width is present, sscanf cannot know that both arrays hold only 16 bytes.
A capacity-aware format would be similar to:
"%d,%15[^,],%15[^,]"
That change must also be paired with an exact return-count check and semantic validation. It is shown to explain the root cause, not as a vendor-supplied patch.
Stack Corruption Analysis
FUN_0040f7f4 allocates a 0x1d0-byte frame and saves control state near its end:
0040f7f4 addiu sp,sp,-0x1d0
0040f7f8 sw ra,0x1cc(sp)
0040f7fc sw fp,0x1c8(sp)
0040f800 sw s0,0x1c4(sp)
The two scanset destinations are:
00410550 addiu t8,fp,0x14c # first char[16]
00410554 addiu v1,fp,0x15c # second char[16]
00410564 addiu a1,...,0x19d4 # "%d,%[^,],%[^,]"
00410570 jal sscanf
Saved ra is at fp+0x1cc, so the distance from the second destination is:
0x1cc - 0x15c = 0x70 = 112 bytes

Dynamic Verification
Controlled Program Counter
The isolated proof used 112 padding bytes followed by the three low little-endian bytes of 0x0040f7f4. The NUL appended by sscanf completed the fourth byte, forming f4 f7 40 00.
QEMU CPU tracing observed two ordinary parser calls and a third entry caused by restored attacker-controlled ra:
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.

Observation-Only RCE Boundary
A second private validation redirected saved ra to 0x00423ab0, the original production system() PLT path. Request-controlled data remained as the exact MIPS a0 argument. The disposable runtime mounted an observation-only program over /bin/sh:
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.
The guard changes only the final execution boundary. The overflow, saved ra, PC, a0, and transfer to the original system() PLT all occur in the production CGI.
Runtime Scope and Limitations
The tested /bin/netis.cgi is byte-identical to the vendor-extracted production binary. The physical flash-backed MIB is unavailable in qemu-user, so the disposable rootfs used a disclosed lab-only libapmib.so accommodation to allocate zeroed MIB state and adjust one packed-field alignment.
That accommodation did not modify netis.cgi, its request parser, the vulnerable sscanf, the local buffers, frame layout, epilogue, saved-return offset, or system() path. Static MIB metadata was taken from the original vendor library.
The production CGI is fixed base, has no stack canary or RELRO, and declares an executable stack and RWX segment.

Physical-router exploit reliability, deployed-kernel randomization, and default WAN management exposure were not independently tested.
Safe Public Proof of Concept
The public repository contains a dry-run-by-default generator for the malformed destHost value:
python3 poc/poc.py
Sending requires an explicit authorized target:
python3 poc/poc.py --target http://192.168.1.1 --send
The public payload uses a 115-byte B component and may crash the CGI. It contains no return-to-system value, command, shellcode, reverse shell, or persistence. The RCE boundary is documented through the non-destructive runtime evidence rather than published as a ready-to-use exploitation chain.
Impact
An unauthenticated attacker who can reach the management interface may be able to exploit the unbounded destHost scanset and execute attacker-selected commands in the privileged router-management context. The original Boa configuration specifies User root and Group root.
Potential impact includes:
- full router compromise
- disclosure or modification of router configuration and stored secrets
- replacement or modification of trusted configuration and services
- DNS, firewall, and routing manipulation
- traffic interception, modification, or redirection
- credential theft through post-compromise access
- persistent compromise as a possible follow-on action
- management-service or device disruption
The default realistic exposure is usually adjacent-network access to the router management interface. If remote management is enabled or the interface is otherwise exposed, the attack surface can become network-reachable. Persistence and physical-device exploit reliability were not dynamically tested.
Classification
The CVE record was assigned but had not yet been populated when this post was prepared. The following are researcher assessments:
Researcher-assessed CVSS v3.1 (typical adjacent management network): 8.8
CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Conditional routable-management score: 9.8
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
- CWE-121: Stack-based Buffer Overflow
- Related: CWE-120 — Buffer Copy without Checking Size of Input
Mitigation
- Bound both scansets to the 16-byte destinations, for example with
%15[^,]. - Require exactly three successful conversions.
- Reject oversized serialized values before parsing.
- Validate both addresses 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.
Disclosure Timeline
- 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 prepared; remote publication remained pending explicit push.
Disclosure Note
VulnCheck assigned CVE-2026-76071 on 2026-08-20 and authorized public disclosure.
I intentionally avoided destructive validation. The public proof of concept uses only a non-executable overlong-field pattern. The RCE validation stopped at an observation-only /bin/sh -c boundary and executed no command.
References
- CVE-2026-76071
- CVE-2026-76071 PoC and Evidence Repository
- VulnCheck
- Netis NC63 Firmware Download Page
- CVE-2026-73673
No physical router was flashed. No real command, reverse shell, persistence, external connection, credential theft, or destructive operation was used.