← back to blog
EN TR

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 OverflowRemote 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

FieldValue
VendorNetis Systems Co., Ltd.
ProductNetis NC63 AC1200 Wireless Dual Band Gigabit MU-MIMO Router
FirmwareNC63_V3.0.0.3327
ArchitectureMIPS32r2 little-endian, o32 ABI, uClibc
CGI binary/bin/netis.cgi
EndpointPOST /cgi-bin/skk_set.cgi
TriggeripFilterList=mod
Dynamically tested parameterdestHost
Static same-parser coveragesrcHost
AuthenticationNone 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

Original production hashes


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
});

Vendor ipFilterList request construction

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.

Unauthenticated path and production privilege

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.

Handler route and MIB metadata


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;

Widthless scansets in the generic parser

%[^,] 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

Exact stack layout and saved-ra offset


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.

Controlled third parser entry

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.

Binary hardening state

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

  1. Bound both scansets to the 16-byte destinations, for example with %15[^,].
  2. Require exactly three successful conversions.
  3. Reject oversized serialized values before parsing.
  4. Validate both addresses server-side before storing them.
  5. Enforce administrator authorization before privileged CGI dispatch.
  6. Audit every metadata parser case for widthless %s and %[...].
  7. 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-76071 and 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

No physical router was flashed. No real command, reverse shell, persistence, external connection, credential theft, or destructive operation was used.