Sploitus

Exploit for CVE-2026-76070

githubexploit · 2026-08-19

Exploit Code

README301 lines
## https://sploitus.com/exploit?id=EEADFF0B-F699-5E06-83A6-E00725E3ACBA
# CVE-2026-76070: Unauthenticated Pre-Auth Stack Buffer Overflow via Base64-Decoded Password in Netis NC63 `login.cgi` Leading to RCE

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

## Disclosure status

- **CVE:** `CVE-2026-76070`
- **Vendor:** Netis Systems Co., Ltd.
- **Product:** Netis NC63 Wireless AC1200 Router
- **Tested firmware:** `NC63_V3.0.0.3327`
- **Affected component:** `/bin/netis.cgi`
- **Endpoint:** `POST /cgi-bin/login.cgi`
- **Parameter:** Base64-encoded `password`
- **Authentication:** none; the unsafe decode occurs before credential comparison
- **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 public login handler in Netis NC63 firmware `V3.0.0.3327` retrieves the
attacker-controlled `password` parameter and decodes it with the custom
Base64 routine `FUN_00402bd4`. The caller supplies a 64-byte local stack
buffer but does not pass its capacity to the decoder. The decoder derives its
work from the encoded input and writes decoded bytes without checking the
destination end.

The saved MIPS return address is 136 bytes from the beginning of the decoded
buffer. Dynamic tests against the original-hash production CGI confirmed:

1. a decoded 140-byte `B` pattern produces a fault at `0x42424242`;
2. replacing saved `ra` with `0x0041a2e0` causes a second observed entry at
   the login handler, proving program-counter control; and
3. an isolated observation-only test reaches the original binary's direct
   `system()` call with an attacker-selected MIPS `a0` value. The replacement
   `/bin/sh` logged `/bin/sh -c NC63_RCE_PROOF` and executed no command.

The public PoC in this repository deliberately stops at a crash pattern. It
contains no return chain, shellcode, command, reverse shell, or persistence.

## Affected artifact integrity

```text
193f6a5e2ce65972b1805bf076f8d3521379a8441c8aaeb5ad0ba174bbee0792  netis_NC63_V3.0.0.3327.bin
23faa747b7d2f067aa5431bcc227ceca97a7977cf3e7c372f715cbba57f9209b  squashfs-root/bin/boa
eb298774c27070dc595fefcabb4e8c12a46cb5f4fd08f91c3ca92282c3a289a2  squashfs-root/bin/netis.cgi
```

The dynamically tested `/bin/netis.cgi` copy has the same SHA-256 as the
vendor-extracted executable.

![Original and runtime hashes](evidence/screenshots/runtime/original-runtime-hashes.png)

## Attack surface and authentication status

The vendor frontend sends the password to the public endpoint as Base64:

```javascript
obj.password = base64encode(utf16to8(password));
request({
    url: "/cgi-bin/login.cgi",
    data: obj
});
```

The HTML field uses `maxlength="63"`, but that is only a browser-side
restriction. A direct HTTP client can submit a larger encoded value.

![Frontend request and client-only limit](evidence/screenshots/requests/frontend-login-request-and-limit.png)

`login.cgi` is necessarily reachable before authentication. The unsafe decode
happens before the decoded password is compared with the configured
administrator password. No valid session, Cookie header, Authorization header,
or correct password is required.

## Source-to-sink trace

```text
Unauthenticated HTTP client
  |
  | POST /cgi-bin/login.cgi
  | password=
  v
/bin/netis.cgi: FUN_0041a2e0
  |
  | get_request_param("password")
  v
FUN_00402bd4(decoded_stack_buffer, encoded_password)
  |
  | no destination-capacity argument
  | decoded output exceeds 64 bytes
  v
saved s8 at decoded offset 132
saved ra at decoded offset 136
  |
  v
attacker-selected MIPS PC
```

## Vulnerable code

Ghidra-derived pseudocode, with names normalized for readability:

```c
int login_cgi(void *request)
{
    char decoded[64];
    char stored[68];
    char *password;

    memset(decoded, 0, 64);
    memset(stored, 0, 64);
    password = get_request_param(request, "password");
    if (password != NULL)
        FUN_00402bd4(decoded, password); /* no capacity argument */

    apmib_get(0x15e, stored);
    if (strcmp(decoded, stored) == 0)
        printf("[\"SUCCESS\"]");
    else {
        system("echo 0 >/tmp/boa_auth");
        printf("[\"%d\"]", 0x15);
    }
    return 0;
}
```

![Vulnerable login handler](evidence/screenshots/decompiled/login-handler-pseudocode.png)

The decoder at `FUN_00402bd4` receives only destination and source pointers.
Its loop advances the destination pointer and stores up to three decoded bytes
for each four Base64 symbols. No comparison checks the destination against
`decoded + 64`.

![Custom Base64 decoder write loop](evidence/screenshots/decompiled/base64-decoder-loop.png)

Base64 is the input transformation, not the underlying defect. The root cause
is the mismatch between attacker-controlled decoded length and a fixed-size
destination whose capacity is never enforced. For ordinary padded input, four
encoded characters represent up to three decoded bytes; server-side checks
must therefore calculate and validate decoded size before writing.

## Stack corruption analysis

`FUN_0041a2e0` starts at `0x0041a2e0` and creates a `0xa8`-byte frame:

```asm
0041a2e0  addiu sp,sp,-168
0041a2e4  sw    ra,164(sp)
0041a2e8  sw    s8,160(sp)
0041a2ec  move  s8,sp
```

The decoded destination begins at `s8+0x1c`; saved `s8` and saved `ra` are at
`s8+0xa0` and `s8+0xa4`:

```text
decoded[64]  s8+0x1c   decoded offset 0
saved s8     s8+0xa0   decoded offset 132
saved ra     s8+0xa4   decoded offset 136
```

The exact return-address distance is `0xa4 - 0x1c = 0x88`, or 136 bytes.

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

## Dynamic verification

### Saved return-address overwrite

A 140-byte decoded `B` pattern replaced the four-byte saved return address:

```text
--- SIGSEGV {si_signo=SIGSEGV, si_code=1, si_addr=0x42424242} ---
qemu: uncaught target signal 11 (Segmentation fault)
```

![Fault at attacker-selected return address](evidence/screenshots/runtime/controlled-ra-crash.png)

### Program-counter control

A separate 140-byte input set saved `ra` to `0x0041a2e0`. QEMU CPU tracing
recorded an ordinary first handler entry followed by a second entry with
`s8=0x41414141` and `ra=0x0041a2e0`.

![Controlled second handler entry](evidence/screenshots/runtime/controlled-pc.png)

### Observation-only command boundary

The original binary contains a direct `jal system` at `0x0041a3cc`. In the
private isolated validation, existing fixed-base instructions loaded a marker
into `a0` and reached that call. A static observation program was mounted over
`/bin/sh`; it logged the command-interpreter arguments and executed nothing:

```text
argv[0]=
argv[1]=
argv[2]=
CONTROLLED_MARKER_REACHED
PASS: attacker-controlled a0 reached system() and /bin/sh argv.
PASS: the guard logged the request and executed no command.
```

This demonstrates an RCE primitive in the isolated production-code path. It
does not establish identical exploit reliability on a physical router under
its deployed kernel and stack-randomization configuration.

## Privilege and binary-hardening context

The original Boa configuration specifies `User root`, `Group root`, and a CGI
path containing `/bin` and `/web/cgi-bin`. The production executable is fixed
base (`0x00400000`), has no stack canary or RELRO, and declares an executable
GNU stack with RWX segments.

![Production Boa privilege configuration](evidence/screenshots/runtime/production-boa-root-config.png)

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

## Safe public PoC

The included script defaults to dry-run mode and only generates a Base64 form
body containing 140 `B` bytes after decoding:

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

Sending requires an explicit authorized target and `--send`:

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

Sending the pattern may crash the CGI process. Use it only in an authorized,
disposable environment. The PoC does not implement the private RCE validation
chain.

## Impact

Successful exploitation can execute attacker-selected code or commands in the
router-management context. Under the original Boa configuration that context
runs as root. Potential consequences include configuration and secret
disclosure, DNS/firewall/routing manipulation, traffic redirection, service
disruption, and full device compromise.

## Severity and classification

The CVE record was assigned but had not yet been populated when this package
was prepared. The following scores are researcher assessments, not a published
VulnCheck score:

- **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. Replace the custom decoder with an API that accepts destination capacity.
2. Reject input whose calculated decoded length exceeds 63 bytes, reserving
   space for a terminator.
3. Validate length and Base64 syntax server-side before decoding.
4. Audit every caller of `FUN_00402bd4`.
5. Rebuild with stack canaries, PIE, NX, and RELRO.
6. Run CGI processes with least privilege.

## Evidence index

See [evidence/README.md](evidence/README.md) for screenshots and trace mapping.
Normalized Ghidra excerpts 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
  `78efb47e-2c9e-4f8c-ae98-083128ff9e0c`.
- **2026-08-20:** VulnCheck assigned `CVE-2026-76070` and authorized public
  disclosure.
- **2026-08-20:** local public-disclosure package prepared; remote publication
  remains pending an explicit push.

## References

- [CVE-2026-76070](https://vulners.com/cve/CVE-2026-76070)
- [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 shell command, reverse shell,
persistence, external connection, credential theft, or destructive firmware
operation was used.