Talos Vulnerability Report

TALOS-2026-2388

Totolink X2000R boa formWsc targetAPSsid OS command injection vulnerability

October 20, 2026

CVE Number

CVE-2026-40627

Summary

A OS command injection vulnerability exists in the boa formWsc peerRptPin 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-78 - Improper Neutralization of Special Elements used in an OS Command (‘OS Command Injection’)

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/formWsc page, which leads us into the formWsc() function:

00448054    size_t formWsc(int32_t* arg1)
// [...]
00448168        char* targetAPMac = get_from_postbody(cli: arg1, "targetAPMac", &cgi_var_pref)
00448180        char* targetAPSsid = get_from_postbody(cli: arg1, "targetAPSsid", &cgi_var_pref)
00448198        char* redirect-url = get_from_postbody(cli: arg1, "submit-url", &cgi_var_pref)
004481c0        void* const var_34 = &data_460000
004481c4        int32_t var_48

We can skip looking at most of the code, it’s only really worth noting that the get_from_postbody() function lets the program read parameters that we pass in via HTTP post parameters. Also for this bug in particular, we read in the targetAPSsid HTTP POST parameter in at [1]. Continuing on to the portion of the function where the targetAPSsid is used:

// [...]

004488e4                        if (setnewpin == 0)                   // [2]
00448910                            char* targetAPMac_1 = targetAPMac
0044890c                            char* targetAPSsid_1
0044890c                            
0044890c                            if (sx.d(*targetAPMac) != 0)  // [...]
0044890c                            else
00448914                                targetAPSsid_1 = targetAPSsid
00448914                            
00448920                            int32_t out_10 = out_5
00448920                            
0044891c                            if (sx.d(*targetAPSsid_1) != 0)
00448930                                if (strlen(str: targetAPSsid) u>= 0x21)  // [3]
00448aa0                                    puts(str: "invaild SSID Len")
00448930                                else
00448944                                    memset(str: &pin, c: 0, n: 0x80)
00448950                                    strcpy(dest: &pin, src: targetAPSsid)
00448958                                    escape_quote_tick_slash(&pin)        // [4]
00448970                                    var_2e8 = &pin
00448974                                    snprintf(dst: &var_210, 0xc8, format: "iwpriv wlan%d set_mib wsc_specssid="%s" ", wlan_idx, var_2e8)  // [5]
0044897c                                    system(string: &var_210)             // [6]

Assuming we set the correct HTTP POST parameters to avoid setting a new pin, we enter the branch at [2] and our targetAPSsid variable is looked at. Assuming it’s non-zero, there’s a length check at [3] to verify it’s within the normal 0x20 bounds of a typical SSID. If this check passes, a further validation check occurs within the escape_quote_tick_slash() function at [4], and then the value is inserted into a larger command at [5] which is then run with the call to system at [6]. The crux of this issue lies exactly in how the function at [4] works, so let us take a quick peek:

00429224    char* escape_quote_tick_slash(char* arg1)
0042922c        char var_d0[0xcc]
0042922c        char (* wptr)[0xcc] = &var_d0
0042923c        char* $a3 = arg1
0042923c        
00429244        while (true)
00429244            char* rptr = sx.d(*$a3)
00429244            
00429248            if (rptr == 0)
00429248                break
00429248            
00429264            if (rptr == '\\')
0042926c                *wptr = '\\'
00429270                (*wptr)[1] = '\\'
00429274                wptr = &(*wptr)[2]
00429264            else if (rptr == '\"')
00429288                *wptr = '\\'
00429290                (*wptr)[1] = '\"'
00429274                wptr = &(*wptr)[2]
00429280            else if (rptr != '`')
00429298                *wptr = rptr.b
004292ac                wptr = &(*wptr)[1]
00429294            else
0042929c                *wptr = '\\'
004292a4                (*wptr)[1] = '`'
00429274                wptr = &(*wptr)[2]
00429274            
0042927c            $a3 = &$a3[1]
0042927c        
00429254        *wptr = 0
0042925c        return strcpy(dest: arg1, src: &var_d0)

Without giving a step-by-step, it suffices to say that a copy of our input is copied to the stack, except any backslashes, double quotes, or backticks are escaped via backslashes. Critically, for unknown reasons, this function overlooks the fact that busybox also supports subshell commands via the $(...) syntax. As such, we can input any set of commands to be run as long as the length is less than 0x1d (0x20 - 0x3 for $()). Since there’s not really any reason we cannot run this command injection multiple times with multiple requests, this quickly results in arbitrary code execution.

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