Talos Vulnerability Report

TALOS-2026-2387

Totolink X2000R boa formWsc peerRptPin OS command injection vulnerability

October 20, 2026

CVE Number

CVE-2026-35499

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* str_2 = get_from_postbody(arg1, "targetAPMac", &data_4780c0)
00448180        char* str_3 = get_from_postbody(arg1, "targetAPSsid", &data_4780c0)
00448198        void* $v0 = get_from_postbody(arg1, "submit-url", &data_4780c0)
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. Continuing on to the portion of the function we care about:

// [...]
00448c20                    if (sx.d(*get_from_postbody(arg1, "setPIN", &data_4780c0)) == 0)  // [1]
00448ed8                    label_448ed8:
00448ed8                        int32_t setRptPin = sx.d(*get_from_postbody(arg1, "setRptPIN", &data_4780c0))  // [2]
00448edc                        void* peerRptPin
00448edc                        
00448edc                        if (setRptPin != 0)
00448ef0                            peerRptPin = get_from_postbody(arg1, "peerRptPin", &data_4780c0) // [3]
00448ef0                        

Assuming that our setPIN HTTP post parameter is 0x0 and the setRptPIN parameter is not equal to 0x0 [2], then the program reads our peerRptPin variable as well at [3]. Continuing down to where the peerRptPin is actually used:

00448f0c                        apmib_get(0x10e, &var_48)  // [4]
00448f0c                        
00448f18                        if (var_48 == 0)
00448f68                            snprintf(&var_210, 0xc8, "iwpriv wlan%d-vxd set_mib pin=%s", wlan_idx, peerRptPin)
00448e0c                            $a2_12 = system(string: &var_210)  // [5]
00448f18                        else
00448f24                            var_48 = 0
00448f28                            apmib_set(0x10e, &var_48)
00448f30                            sub_43c398(4)
00448f48                            snprintf(&var_210, 0xc8, "echo %s > /var/wps_peer_pin", peerRptPin)
00448d48                        label_448d48:
00448d48                            system(string: &var_210) // [6]

At [4], the device reads a configuration value from flash memory, but we don’t really care about what the value is in this case as both branches of the following if statement contain a direct and unescaped call to system with the value of our peerRptPin HTTP post variable, which immediately lets us run arbitrary OS commands on the device.

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