Talos Vulnerability Report

TALOS-2026-2425

Totolink X2000R boa formFilter stack-based buffer overflow vulnerabilities

October 20, 2026

CVE Number

CVE-2026-9659,CVE-2026-9660

Summary

Multiple exploitable stack-based buffer overflow vulnerabilities exist in the boa formFilter 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 these vulnerabilities.

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. For the current vulnerability we examine the /boafrm/formFilter page, which leads us into the formFilter() function: 004175f0 size_t formFilter(struct client* arg1, enum mib arg2 @ $s3) 0041760c enum mib var_1c = arg2 00417630 struct client* cli = arg1 0041762c char* addFilterIp_ = get_from_postbody(cli: arg1, “addFilterIp”, &cgi_var_pref) 00417634 char* addFilterIp = addFilterIp_ 00417634 0041763c if (sx.d(addFilterIp_) == 0) 00417658 addFilterIp = get_from_postbody(cli, “addFilterIpFlag”, &cgi_var_pref) 00417658 00417668 char deleteSelFilterIp_ = get_from_postbody(cli, “deleteSelFilterIp”, &cgi_var_pref) 00417680 char* deleteAllFilterIp = get_from_postbody(cli, “deleteAllFilterIp”, &cgi_var_pref) 00417698 char* applyFilterIps = get_from_postbody(cli, “applyFilterIps”, &cgi_var_pref) 004176b0 char* addFilterIps = get_from_postbody(cli, “addFilterIps”, &cgi_var_pref) 004176c8 char* deleteSelFilterIps = get_from_postbody(cli, “deleteSelFilterIps”, &cgi_var_pref) 004176e0 char* deleteAllSelFilterIps = get_from_postbody(cli, “deleteAllFilterIps”, &cgi_var_pref) 004176f8 char* addFilterPort = get_from_postbody(cli, “addFilterPort”, &cgi_var_pref) 00417700 char* addFilterPort_1 = addFilterPort 00417700 00417708 if (sx.d(addFilterPort) == 0) 00417724 addFilterPort_1 = get_from_postbody(cli, “addFilterPortFlag”, &cgi_var_pref) 00417724 00417734 char deleteSelFilterPort = get_from_postbody(cli, “deleteSelFilterPort”, &cgi_var_pref) 0041774c char* deleteAllSelFilterPort = get_from_postbody(cli, “deleteAllFilterPort”, &cgi_var_pref) 00417764 char* addFilterMac = get_from_postbody(cli, “addFilterMac”, &cgi_var_pref) 0041776c char* addFilterMac_1 = addFilterMac 0041776c 00417774 if (sx.d(addFilterMac) == 0) 00417790 addFilterMac_1 = get_from_postbody(cli, “addFilterMacFlag”, &cgi_var_pref) 00417790 004177a0 char deleteSelFilterMac = get_from_postbody(cli, “deleteSelFilterMac”, &cgi_var_pref) 004177b8 char* deleteAllFilterMaqc = get_from_postbody(cli, “deleteAllFilterMac”, &cgi_var_pref) 004177d0 char* addFilterUrl = get_from_postbody(cli, “addFilterUrl”, &cgi_var_pref) 004177d8 char* addFilterUrl_1 = addFilterUrl 004177d8 004177e0 if (sx.d(addFilterUrl) == 0) 004177fc addFilterUrl_1 = get_from_postbody(cli, “addFilterUrlFlag”, &cgi_var_pref) 004177fc 0041780c char deleteSelFilterUrl = get_from_postbody(cli, “deleteSelFilterUrl”, &cgi_var_pref) 00417824 char* deleteAllFilterUrl = get_from_postbody(cli, “deleteAllFilterUrl”, &cgi_var_pref)

As shown above, the formFilter() function is capable of many different types of actions, all summarized as adding, modification, and removal of assorted firewall rules and VPN configuration. For the purposes of our vulnerabilities in this form, we just need the addFilterIP parameter and enabled parameter to be set before we hit our desired code paths shown below:

    00417fb8                        if (sx.d(*addFilterIps_2) == 0) // [1]
    00417fbc                            add_filter_mac = sx.d(*addFilterMac_1)
    00417fb8                        else
    00417fcc                            char* protocol = get_from_postbody(cli, "protocol", &cgi_var_pref)
    00417fe4                            char* vpnUser = get_from_postbody(cli, "vpnUser", &cgi_var_pref)   // [2]
    00417ff0                            char var_2c8[0x33]
    00417ff0
    00417ff0                            if (sx.d(*vpnUser) != 0)
    00417ffc                                strcpy(dest: &var_2c8, src: vpnUser)    // [3]
    00418010                            char* vpnPassword = get_from_postbody(cli, "vpnPassword", &cgi_var_pref)  // [4]
    0041801c                            int32_t $v0_79
    0041801c
    0041801c                            if (sx.d(*vpnPassword) == 0)
    00418020                                $v0_79 = sx.d(*protocol)
    0041801c                            else
    00418028                                char var_295[0x1f]
    00418028                                strcpy(dest: &var_295, src: vpnPassword) // [5]
    00418030                                $v0_79 = sx.d(*protocol)

CVE-2026-9659 - vpnUser

As mentioned, assuming our addFilterIP parameter is non-zero [1], then our vpnUser HTTP POST parameter is read in at [2] and then copied onto the stack at [3] with an unlimited strcpy resulting in a classic buffer overflow and subsequent command execution.

Crash Information

0x41414141 in ?? ()

[^_^] SIGSEGV

***********************************************************************************
***********************************************************************************
v0         : 0x00000083                         | s3         : 0x41414141
v1[H]      : 0x00490000                         | s4         : 0x41414141
a0[H]      : 0x004b8b87                         | s5         : 0x41414141
a1[S]      : 0x7fce8983                         | s6         : 0x41414141
a2         : 0x00000003                         | s7         : 0x41414141
a3[S]      : 0x7fce8983                         | t8[X]      : 0x0048c074
t0         : 0x0000003e                         | t9[L]      : 0x776ce6d0
t1         : 0x3e6d726f                         | k0         : 0x00000000
t2         : 0x6c622f3c                         | k1         : 0x00000000
t3         : 0x716b636f                         | gp[L]      : 0x7771e400
t4         : 0x65746f75                         | sp[S]      : 0x7fce9430
t5         : 0x622f3c3e                         | ra         : 0x41414141
t6         : 0x3e79646f                         | status     : 0x0000f413
t7         : 0x74682f3c                         | 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?)
***********************************************************************************

CVE-2026-9660 - vpnPassword

As mentioned, assuming our addFilterIP parameter is non-zero [1], then our vpnPassword HTTP POST parameter is read in at [4] and then copied onto the stack at [5] with an unlimited strcpy resulting in a classic buffer overflow and subsequent command execution.

Crash Information

\Program received signal SIGSEGV, Segmentation fault.
0x00418268 in ?? ()

[^_^] SIGSEGV
***********************************************************************************
***********************************************************************************
v0         : 0x44444444                         | s3[X]      : 0x004780c0
v1         : 0x00000030                         | s4         : 0x00000000
a0         : 0x00000000                         | s5[X]      : 0x004780c0
a1[H]      : 0x004ad6a0                         | s6[X]      : 0x004780c0
a2[L]      : 0x776f82d0                         | s7[H]      : 0x004ad410
a3         : 0x00000001                         | t8[X]      : 0x0048c35c
t0         : 0x776fe628                         | t9[L]      : 0x776b14d0
t1         : 0x776fe628                         | k0         : 0x00000000
t2[H]      : 0x004ad448                         | k1         : 0x00000000
t3         : 0xfffffffc                         | gp[L]      : 0x77be5020
t4         : 0x00000020                         | sp[S]      : 0x7fd40a70
t5         : 0x00000001                         | ra[X]      : 0x00418030
t6         : 0x00000df0                         | status     : 0x0000f413
t7[H]      : 0x00490000                         | badvaddr   : 0x44444444
s0[H]      : 0x004d6578                         | pc[X]      : 0x00418268
s1         : 0x00480000                         | fcsr       : 0x00000000
s2[S]      : 0x7fd40a90                         | restart    : 0x00000000
***********************************************************************************
   0x418258:    lb      s4,0(s4)
   0x41825c:    bnezl   s4,0x4182a0
   0x418260:    lb      v0,0(s3)
   0x418264:    lw      v0,664(sp)
=> 0x418268:    lb      v0,0(v0)
   0x41826c:    bnezl   v0,0x4182a0
   0x418270:    lb      v0,0(s3)
   0x418274:    lw      v0,668(sp)
   0x418278:    lb      v0,0(v0)
***********************************************************************************
#0  0x00418268 in (0x400000 0x47c000 0x7c000 0x0 r-xp /bin/boa)(+0x18268)
***********************************************************************************

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