TALOS-2026-2387
CVE-2026-35499
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.
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)
X2000R - https://www.totolink.net/home/menu/newstpl/menu_newstpl/products/id/242.html
9.1 - CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H
CWE-78 - Improper Neutralization of Special Elements used in an OS Command (‘OS Command Injection’)
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.
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
Lilith Wyatt of Cisco Talos