Talos Vulnerability Report

TALOS-2026-2430

Totolink X2000R boa formUploadFile arbitrary file write vulnerability

October 20, 2026

CVE Number

CVE-2026-9827

Summary

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.

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-22 - Improper Limitation of a Pathname to a Restricted Directory (‘Path Traversal’)

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/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.

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