TALOS-2026-2430
CVE-2026-9827
A arbitrary file write vulnerability exists in the boa formUploadFile 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-22 - Improper Limitation of a Pathname to a Restricted Directory (‘Path Traversal’)
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/formUploadFile page, which leads us into the formUploadFile() function:
0044ce38 size_t formUploadFile(struct client* cli)
0044ce50 int32_t upload_offset = get_readstart_offset(cli->uploadbuf) // [1]
0044ce60 char var_d8[0xc8]
0044ce60
0044ce60 if (upload_offset != 0xffffffff)
0044cf20 // okay, we reach here...
0044cf20 uint32_t $v0_2 = isFileExist("/tmp/bzupload")
0044cf2c char* uploadbuf = cli->uploadbuf
0044cf2c
0044cf28 if ($v0_2 != 0)
0044cf34 get_filename_from_buf(buf: uploadbuf, dst: &upload_filename)
0044cf48 sprintf(dst: &var_d8, fmt: "<b>Upload Successful, file name: %s</b>", &upload_filename)
0044cf5c write_to_firware_ini(readbuf: cli->uploadbuf, msgsize: cli->message_size_maybe, readoffset: upload_offset,
0044cf5c filename: &upload_filename)
0044cf68 system(string: "killall batchupg >/dev/null 2>&1")
0044cf74 system(string: "batchupg & >/dev/null 2>&1")
0044cf84 return send_302_redirect(cli, redirect-url: "/bzupload.htm")
0044cf84
0044cfb0 if (handleFirmwareFile(msgbuf: uploadbuf, max_size: cli->message_size_maybe, read_offset: upload_offset) == 0) // [2]
0044cfbc get_filename_from_buf(buf: cli->uploadbuf, dst: &upload_filename) // [3]
0044cfd0 sprintf(dst: &var_d8, fmt: "<b>Upload Successful, file name: %s</b>", &upload_filename) // [4]
0044cfe4 write_to_firware_ini(readbuf: cli->uploadbuf, msgsize: cli->message_size_maybe, readoffset: upload_offset,
0044cfe4 filename: &upload_filename)
0044cf84 return send_302_redirect(cli, redirect-url: "/bupload.htm")
0044cf84
0044d00c sprintf(dst: &var_d8, fmt: "<b>%s</b>", &invalid_file_format_err)
0044ce60 else
0044ce70 strcpy(dest: &var_d8, src: "<b><script>dw(Js_InvalidFormat)</script></b>")
Since we are uploading a file, we suitably start by looking to see if our http request actually contains multipart form data at [1]. Assuming there actually exists a file to upload, we check to see the /tmp/bzupload file exists. Unless we send a /form/formBuploadSetting request, this file will not exist. Regardless, since we control this, we hit the branch at [2], where by our uploaded firmware file is parsed and validated. Assuming we pass this check, we read in the filename from our multi-part form data at [3] and copy it to the stack at [4] before then hitting the write_to_firmware_ini function at [4]:
0044c68c int32_t write_to_firware_ini(char* readbuf, int32_t msgsize, int32_t readoffset, char* filename)
0044c6b4 if (zx.d(*filename) == 0)
0044c6c4 strcpy(dest: filename, src: version_str)
0044c6e4 strcpy(dest: strstr(haystack: filename, needle: "-B"), src: "-B99999999.9999")
0044c6e4
0044c700 char string[0x100]
0044c700 sprintf(dst: &string, fmt: "mkdir -p /var/web/fw/%s >/dev/null 2>&1", &fw_model_string)
0044c708 system(&string)
0044c724 sprintf(dst: &string, fmt: "rm -f /var/web/fw/%s/* >/dev/null 2>&1", &fw_model_string)
0044c72c system(&string)
0044c748 sprintf(dst: &string, fmt: "touch /var/web/fw/%s/version >/dev/null 2>&1", &fw_model_string)
0044c750 system(&string)
0044c770 char fpath[0x104]
0044c770 snprintf(dst: &fpath, 0x100, format: "/var/web/fw/%s/version", &fw_model_string)
0044c790 void* const var_228 = "echo"
0044c798 p3_execve(pathname: "/bin/echo", fd: &fpath, argc: 2, argv_1: 1)
0044c7bc snprintf(dst: &fpath, 0x100, format: "/var/web/fw/%s/%s", &fw_model_string, filename, filename) // [5]
0044c7cc printf("WirteToFile: %s\n", &fpath)
0044c7dc create_buf_with_contents(file: &fpath, buf: &readbuf[readoffset], len: msgsize - readoffset) // [6]
0044c7dc
0044c7f0 if (isFileExist("/tmp/bzupload") == 0)
0044c830 return sub_44c610()
0044c830
0044c80c return inifile_set("/tmp/firware.ini", "firware", "filename", &fpath)
After a series of setup calls to system, our filename parameter is finally utilized at [5] when the program appends the filename to a full path [5] and then writes the contents of our uploaded file to the path at [6]. Unfortunately there does not seem to be any validation of our filename, so we can easily utilize ../ to conduct a path traversal and arbitrarily write to any file we want that is writable by root.
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