Sploitus

Exploit for CVE-2026-76071

githubexploit · 2026-08-19

Exploit Code

README292 lines
## https://sploitus.com/exploit?id=E6239A64-7F8A-5C59-AACC-41AE38C32A88
# CVE-2026-76071: Unauthenticated Pre-Auth Stack Buffer Overflow via `sscanf %[^,]` in `skk_set.cgi` `ipFilterList` Handler in Netis NC63

**Researcher:** Özcan Ersan ([@ozcanpng](https://github.com/ozcanpng))  
**Prepared by:** Özcan Ersan (@ozcanpng)

## Disclosure status

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

## Executive summary

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.

## Affected artifact integrity

```text
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](evidence/screenshots/runtime/original-production-hashes.png)

## Attack surface and authentication status

The vendor frontend builds `srcHost` and `destHost` as three comma-separated
components and posts them with `ipFilterList` to `/cgi-bin/skk_set.cgi`:

```javascript
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 frontend request construction](evidence/screenshots/requests/frontend-ipfilter-request-construction.png)

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 root CGI boundary](evidence/screenshots/runtime/unauthenticated-root-cgi-boundary.png)

## Source-to-sink trace

```text
Unauthenticated HTTP client
  |
  | POST /cgi-bin/skk_set.cgi
  | ipFilterList=mod
  | destHost=1,0.0.0.0,
  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.

![Handler route and original MIB metadata](evidence/screenshots/decompiled/handler-route-and-mib-metadata.png)

## Vulnerable code

Normalized Ghidra-derived pseudocode:

```c
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;
```

![Unbounded scanset conversions](evidence/screenshots/decompiled/unbounded-sscanf-pseudocode.png)

`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.

## Stack corruption analysis

`FUN_0040f7f4` begins at `0x0040f7f4` and creates a `0x1d0`-byte frame:

```asm
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:

```text
0x1cc - 0x15c = 0x70 = 112 bytes
```

![Stack layout and saved-ra offset](evidence/screenshots/decompiled/stack-layout-and-ra-offset.png)

## Dynamic verification

### Program-counter control

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:

```text
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](evidence/screenshots/runtime/controlled-pc.png)

### Observation-only command boundary

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:

```text
argv[0]=
argv[1]=
argv[2]=
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.

## Runtime accommodation and evidentiary boundary

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.

![Binary hardening state](evidence/screenshots/runtime/binary-protections.png)

## Safe public PoC

Dry-run generation of the URL-encoded body:

```bash
python3 poc/poc.py
```

Explicit transmission to an authorized disposable target:

```bash
python3 poc/poc.py --target http://192.168.1.1 --send
```

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.

## Impact

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.

## Severity and classification

The assigned CVE record had not yet been populated when this package was
prepared. These are researcher assessments rather than published VulnCheck
scores:

- **Researcher-assessed CVSS v3.1 (typical adjacent management network):**
  `8.8 — AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H`
- **Conditional routable-management score:**
  `9.8 — 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

## Remediation

1. Use `%15[^,]` for each 16-byte destination and require three successful
   conversions.
2. Reject oversized serialized host values before parsing.
3. Validate both IP values server-side before storing them.
4. Enforce administrator authorization before privileged CGI dispatch.
5. Audit every metadata parser case for widthless `%s` and `%[...]`.
6. Rebuild with stack canaries, PIE, NX, and RELRO.

## Evidence index

See [evidence/README.md](evidence/README.md). Normalized pseudocode and
instruction evidence are under
[`attachments/decompiled-functions/`](attachments/decompiled-functions/).

## Disclosure timeline

- **2026-08-16:** discovery and isolated production-binary validation completed.
- **August 2026:** reported to VulnCheck under tracking ID
  `384e4715-7972-4044-99b1-7002fd38bc42`.
- **2026-08-20:** VulnCheck assigned `CVE-2026-76071` and authorized public
  disclosure.
- **2026-08-20:** local public-disclosure package prepared; remote publication
  remains pending an explicit push.

## References

- [CVE-2026-76071](https://vulners.com/cve/CVE-2026-76071)
- [VulnCheck](https://www.vulncheck.com/)
- [Netis NC63 support page](https://www.netis-systems.com/support/downinfo.html?id=35)
- [CVE-2026-73673](https://vulners.com/cve/CVE-2026-73673)

## Researcher credit

Discovered and reported by **Özcan Ersan ([@ozcanpng](https://github.com/ozcanpng))**.

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