Talos Vulnerability Report

TALOS-2026-2439

Totolink X2000R Mcli stack-based buffer overflow vulnerability

October 20, 2026

CVE Number

CVE-2026-10139

Summary

A stack-based buffer overflow vulnerability exists in the Mcli 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/formUpload functionality:

0044c8a8    size_t formUpload(struct client* cli)

0044c8e4        char str[0xc8]
0044c8e4        memset(&str, c: 0, n: 0xc8)
0044c8f4        char var_2d0[0xc8]
0044c8f4        memset(str: &var_2d0, c: 0, n: 0xc8)
0044c904        int32_t out = 1
0044c908        char _deviceMac
0044c908        memset(str: &_deviceMac, c: 0, n: 0x99)
0044c910        char* uploadbuf = cli->uploadbuf
0044c914        int32_t read_offset = get_readstart_offset(uploadbuf)  // [1]
0044c914        
0044c924        if (read_offset != 0xffffffff)
0044c9e8            int32_t dmac_slot_1 = handleFirmwareFile(msgbuf: uploadbuf, max_size: cli->message_size_maybe, read_offset) // [2]
0044c9f4            int32_t dmac_slot = dmac_slot_1
0044c9f4            
0044c9f0            if (dmac_slot_1 == 0)
0044ca08                sprintf(dst: &str, fmt: "Upload successfully (size = %d bytes)!<br><br>Firmware update in progress.", 
0044ca08                    cli->message_size_maybe)
0044ca20                void* _AutoReset = sessionCheck(cli, varname: "AutoReset", &cgi_var_pref, nullptr, 1)
0044ca30                apmib_get(MIB_DO_Mcli, &out)
0044ca38                int32_t MeshEnabled = out
0044ca3c                char* redirect-url
0044ca3c                
0044ca3c                if (MeshEnabled != 1)                // [3]
// [...]
0044ca3c                else
0044ca54                    printf("===========================Start mesh upgrade function: %s\n", &upload_filename)
0044ca60                    get_filename_from_buf(buf: cli->uploadbuf, dst: &upload_filename) 

To start, the program finds the offset of the file that we are uploading at [1] via parsing HTTP POST data, and then at [2] treats this uploaded file as a firmware file at [2] with the handleFirmwareFile function. If we wish to sidestep actually flashing anything, we can use and the following commented python code to generate a buffer that’s considered “valid firmware” but doesn’t flash.

####################
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"

Continuing on, assuming that our device is currently in easy mesh mode [3], or we set that configuration ourselves via other HTTP requests, we can then continue on within the formUpload() function:

0044ca94                    void* _controller_upgrade = sessionCheck(cli, varname: "controller_upgrade", &cgi_var_pref, nullptr, &data_490000)
0044cab8                    int32_t _deviceNum = atoi(str: sessionCheck(cli, varname: "deviceNum", &cgi_var_pref, nullptr, &data_490000))   // [4]
0044cacc                    printf("devicenum :%d\n", _deviceNum)
0044cadc                    char* format = "enable%d"
0044cae8                    char* str2 = &ON
0044cae8                    
0044caf0                    while (_deviceNum s>= MeshEnabled)  
0044cc3c                        char varname[0x64]
0044cc3c                        snprintf(dst: &varname, 0x14, format, MeshEnabled)  // [5]
0044cc3c                        
0044cc64                        if (strcmp(str1: sessionCheck(cli, &varname, &cgi_var_pref, nullptr, &data_490000), str2) != 0)
0044cc68                            MeshEnabled += 1
0044cc64                        else
0044cc78                            snprintf(dst: &varname, 0x14, format: "devicemac%d", MeshEnabled)                          // [6]
0044cc8c                            char* _devicemac%d = sessionCheck(cli, &varname, &cgi_var_pref, nullptr, &data_490000)
0044cc8c                            
0044cc9c                            if (sx.d(*_devicemac%d) == 0)
0044cca0                                MeshEnabled += 1
0044cc9c                            else
0044ccb0                                printf("device mac:%s\n", _devicemac%d)
0044ccb0                                
0044ccbc                                if (dmac_slot s< 3)
0044ccd4                                    strcpy(dest: &(&_deviceMac)[dmac_slot * 0x33], src: _devicemac%d)  // [7]

The program goes back to reading our HTTP POST parameters, most importantly the deviceNum parameter at [4], which determines how many times we enter the loop further down. For each loop iteration, we look first for generated enable%d parameter, where %d is the loop iteration. Assuming this HTTP POST parameter is found and is non-zero, then we hit the line at [6] and try to look for a devicemac%d parameter as well. If this is found, then we copy the value to an array slot with strcpy at [7]. Continuing on to where these dmac_slot array values are used:

0044cafc                    if (zx.d(_deviceMac) != 0)
0044cb0c                        int32_t $v0_6 = sx.d(*_AutoReset)
0044cb08                        char dmac_slot_2
0044cb08                        char dmac_slot_3
0044cb08                        
0044cb08                        if (zx.d(dmac_slot_2) == 0)
0044cd54                            char* __dmac
0044cd54                            int32_t mclinum
0044cd54                            
0044cd54                            if ($v0_6 == 0)
0044cd84                                mclinum = 2
0044cd8c                                __dmac = &_deviceMac
0044cd54                            else
0044cd60                                mclinum = 3
0044cd64                                __dmac = &_deviceMac
0044cd64                            
0044cd70                            sprintf(dst: &var_2d0, fmt: "%s %d %s &", "Mcli", mclinum, __dmac)   // [8]
0044cb08                        else if (zx.d(dmac_slot_3) == 0)
0044cd08                            char* _dmac
0044cd08                            char* var_2e4_2
0044cd08                            int32_t $a3_5
0044cd08                            
0044cd08                            if ($v0_6 == 0)
0044cd40                                $a3_5 = 2
0044cd44                                var_2e4_2 = &dmac_slot_2
0044cd50                                _dmac = &_deviceMac
0044cd08                            else
0044cd14                                $a3_5 = 3
0044cd18                                var_2e4_2 = &dmac_slot_2
0044cd20                                _dmac = &_deviceMac
0044cd20                            
0044cd2c                            sprintf(dst: &var_2d0, fmt: "%s %d %s %s &", "Mcli", $a3_5, _dmac, var_2e4_2)  // [9]
0044cb14                        else
0044cb1c                            char* dmac
0044cb1c                            char* var_2e4_1
0044cb1c                            char* var_2e0_1
0044cb1c                            int32_t $a3_2
0044cb1c                            
0044cb1c                            if ($v0_6 == 0)
0044ccec                                $a3_2 = 2
0044ccf0                                var_2e0_1 = &dmac_slot_3
0044ccf8                                var_2e4_1 = &dmac_slot_2
0044cd04                                dmac = &_deviceMac
0044cb1c                            else
0044cb28                                $a3_2 = 3
0044cb2c                                var_2e0_1 = &dmac_slot_3
0044cb34                                var_2e4_1 = &dmac_slot_2
0044cb3c                                dmac = &_deviceMac
0044cb3c                            
0044cb48                            sprintf(dst: &var_2d0, fmt: "%s %d %s %s %s &", "Mcli", $a3_2, dmac, var_2e4_1, var_2e0_1)  // [10]
0044cb50                        system(string: &var_2d0)              // [11]

To summarize most of this code, depending on how many device macs there are, we generate a longer or shorter Mcli command string at [8], [9], or [10] and then run that command with a system call at [11]. Let us now look at how the Mcli binary behaves:

004009f0    int32_t main(int32_t argc, char** argv, char** envp)
00400a00        if (argc s<= 0)
00400a0c            puts(str: "use: Mcli flag....")
00400a14            return 1
00400a14        
00400a2c        int32_t _argc = atoi(str: argv[1])
00400a40        printf("flag:%d\n", _argc)
00400a50        int16_t str
00400a50        memset(&str, c: 0, n: 0x60)
00400a5c        char _argc_1 = _argc.b
00400a60        str = 0x6161
00400a6c        char devmac0[0x12]
00400a6c        
00400a6c        if (_argc - 2 u< 3)
00400a74            char* arg2 = argv[2]
00400a74            
00400a84            if (arg2 != 0 && sx.d(*arg2) != 0)
00400a8c                strcpy(dest: &devmac0, src: arg2)     // [12]
00400a9c                printf("devicemac0:%s\n", &devmac0)
00400aa4                char* arg3 = argv[3]
00400aa4                
00400ab4                if (arg3 != 0 && sx.d(*arg3) != 0)
00400abc                    char devmac1[0x12]
00400abc                    strcpy(dest: &devmac1, src: arg3)  // [13]
00400acc                    printf("devicemac1:%s\n", &devmac1)
00400ad4                    char* arg4 = argv[4]
00400ad4                    
00400ae4                    if (arg4 != 0 && sx.d(*arg4) != 0)
00400aec                        char devmac2[0x12]
00400aec                        strcpy(dest: &devmac2, src: arg4)  // [14]
00400b00                        printf("devicemac2:%s\n", &devmac2)
// [...]
00400b38        void* $v0_7 = malloc(size: 0x60)
00400b38        
00400b44        if ($v0_7 == 0)
00400a24            return 0
00400a24        
00400b54        memcpy(dest: $v0_7, src: &str, n: 0x60)
00400b60        sub_400dd0($v0_7, 0x60)
00400b68        free(ptr: $v0_7)
00400b74        return 0

We don’t need to particularly delve indepth as its quickly apparent that all of our input devicemac%d parameters are just immediatlely copied to the stack of this Mcli binary at [12], [13], or [14], via strcpy, resulting in stack-based buffer overflows that quickly lead to code execution.

Crash Information

Breakpoint 1, 0x0044cb50 in ?? ()

[o.o]> x/1s $sp+0x28
0x7f8b2a70:     "Mcli 2 slot1 slot2 ", 'A' <repeats 72 times>, "BBBB &"

[>_>]> set follow-fork-mode child

[<_<]> catch exec
Catchpoint 2 (exec)

[x.x]> c
Continuing.
[Attaching after Thread 24991.24991 fork to child Thread 27452.27452]
[New inferior 2 (process 27452)]
[Detaching after fork from parent process 24991]
**************************************************
       [^.^] T-t-thanks for Stopping by! [^.^]
**************************************************
[Inferior 1 (process 24991) detached]
process 27452 is executing new program: /bin/busybox
[Switching to Thread 27452.27452]

Thread 2.1 "sh" hit Catchpoint 2 (exec'd /bin/busybox), 0x7765cfa0 in ?? ()

[^~^]> disable 1

[~.~]> c
Continuing.
[Attaching after Thread 27452.27452 fork to child Thread 27467.27467]
[New inferior 3 (process 27467)]
[Detaching after fork from parent process 27452]
**************************************************
       [^.^] T-t-thanks for Stopping by! [^.^]
**************************************************
[Inferior 2 (process 27452) detached]
process 27467 is executing new program: /bin/Mcli
[Switching to Thread 27467.27467]

Thread 3.1 "Mcli" hit Catchpoint 2 (exec'd /bin/Mcli), 0x77e84fa0 in ?? ()

[^.^]> c
Continuing.


Thread 3.1 "Mcli" received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()

[^_^] SIGSEGV

***********************************************************************************
***********************************************************************************
v0         : 0x00000000                         | s3         : 0x7766f464
v1         : 0x00000000                         | s4         : 0x00450000
a0         : 0x00000001                         | s5         : 0x00470524
a1         : 0x00000001                         | s6         : 0x7766f464
a2[L]      : 0x77abe2d0                         | s7         : 0x00000002
a3         : 0x00000001                         | t8         : 0xffffffff
t0         : 0x00000000                         | t9[L]      : 0x77a218bc
t1         : 0x0006d8b5                         | k0         : 0x00000000
t2         : 0x00000000                         | k1         : 0x00000000
t3         : 0x00000000                         | gp[L]      : 0x77a3d020
t4         : 0x77e97014                         | sp[S]      : 0x7fa2db28
t5         : 0x00000005                         | ra         : 0x41414141
t6         : 0x004007cd                         | status     : 0x0000f413
t7         : 0x00000001                         | badvaddr   : 0x41414140
s0         : 0x41414141                         | pc         : 0x41414141
s1         : 0x41414141                         | fcsr       : 0x00000000
s2         : 0x004704fc                         | 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