Talos Vulnerability Report

TALOS-2026-2436

Totolink X2000R boa formUpload() heap-based buffer overflow vulnerability

October 20, 2026

CVE Number

CVE-2026-10136

Summary

A heap-based buffer overflow vulnerability exists in the boa formUpload() 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-122 - Heap-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) // [4]

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 hit the function named get_filename_from_buf() at [4]:

0044c538    char* get_filename_from_buf(char* buf, char* dst)

0044c55c        strcpy(dest: dst, src: &cgi_var_pref)
0044c55c        
0044c564        if (buf != 0)
0044c570            char* str = strstr(haystack: buf, needle: "filename=")
0044c570            
0044c578            if (str != 0)
0044c580                char* $v0_1 = strchr(str, c: '\"')
0044c580                
0044c588                if ($v0_1 != 0)
0044c598                    char* $v0_2 = strchr(str: &$v0_1[1], c: '\"')
0044c5a4                    void* $s0_2 = $v0_2 - $v0_1
0044c5a4                    
0044c5a0                    if ($v0_2 != 0)
0044c5b4                        strncpy(dest: dst, src: &$v0_1[1], n: $s0_2 - 1) // [5]
0044c5bc                        *(dst + $s0_2 - 1) = 0
0044c5bc        
0044c5d4        return dst

This function quickly finds the filename of the input file in the multi-part HTTP POST data and then copies that string to the dst parameter with a strncpy at [5]. Unfortunately the length passed to the strncpy is determined by the input string itself, so we’re dealing with essentially an unrestricted copy to the global static upload_filename variable which lies at 0x4935c0. If we try overflowing into any of the other global static variables located in higher memory, nothing actually ends up crashing, but if we take a quick look at the memory layout we can see a very interesting fact:

[>_>]> !mm
0x400000 0x47c000 0x7c000 0x0 r-xp /bin/boa
0x48c000 0x48f000 0x3000 0x7c000 rw-p /bin/boa
0x48f000 0x4c1000 0x32000 0x0 rwxp [heap]       // [7]
0x4c1000 0x504000 0x43000 0x0 rwxp [heap]       // [8]
0x7705a000 0x7707c000 0x22000 0x0 rw-s /SYSV3e0d02a6 (deleted)
0x7707c000 0x7709e000 0x22000 0x0 rw-s /SYSV3d0d02a6 (deleted)

After our upload_filename variable lies the entire heap memory segment at [7] and [8], and most importantly, without any gaps in memory pages or non-writeable pages. As such we can completely overwrite the entire process heap and use this to gain arbitrary code execution.

It’s worth nothing that this get_filename_from_buf fuinction can be hit from both the formUpload and formUploadFile functions, but the vulnerability lies within the get_filename_from_buf function.

Crash Information

Program received signal SIGSEGV, Segmentation fault.
0x7769f454 in malloc () from lib/libc.so.0

[^_^] SIGSEGV

***********************************************************************************
***********************************************************************************
v0         : 0x41414140                         | s3         : 0x00000001
v1         : 0x776ce7a0                         | s4         : 0x00000001
a0[H]      : 0x004ad490                         | s5         : 0x00000010
a1         : 0x41414141                         | s6[X]      : 0x004780c0
a2         : 0x414140c8                         | s7         : 0x00000000
a3         : 0x80808080                         | t8[X]      : 0x0048c0dc
t0         : 0x776ce628                         | t9[L]      : 0x7769eef0
t1         : 0x776ce628                         | k0         : 0x00000010
t2[H]      : 0x004afd88                         | k1         : 0x00000000
t3         : 0xfffffffc                         | gp[L]      : 0x776d0400
t4         : 0x00000088                         | sp[S]      : 0x7ff844f0
t5         : 0x00000001                         | ra[L]      : 0x7769efe0
t6         : 0x00000036                         | status     : 0x0000f413
t7[H]      : 0x00490000                         | badvaddr   : 0x41414149
s0         : 0x776ce5f4                         | pc[L]      : 0x7769f454
s1         : 0x00000000                         | fcsr       : 0x00000000
s2         : 0x00000078                         | restart    : 0x00000000
***********************************************************************************
   0x7769f444 <malloc+1236>:    and     v0,v0,a1
   0x7769f448 <malloc+1240>:    lw      a1,12(a0)
   0x7769f44c <malloc+1244>:    subu    a2,v0,s2
   0x7769f450 <malloc+1248>:    sw      a1,12(v1)
=> 0x7769f454 <malloc+1252>:    sw      v1,8(a1)
   0x7769f458 <malloc+1256>:    sltiu   a1,a2,16
   0x7769f45c <malloc+1260>:    addu    v1,a0,v0
   0x7769f460 <malloc+1264>:    beqz    a1,0x7769f478 <malloc+1288>
   0x7769f464 <malloc+1268>:    addiu   v0,a0,8
***********************************************************************************
#0  0x7769f454 in malloc () from /lib/libc.so.0
#1  0x0040c248 in (0x400000 0x47c000 0x7c000 0x0 r-xp /bin/boa)(+0xc248)
***********************************************************************************

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