CVE-2023-34354
A stored cross-site scripting (XSS) vulnerability exists in the upload_brand.cgi functionality of peplink Surf SOHO HW1 v6.3.5 (in QEMU). A specially crafted HTTP request can lead to execution of arbitrary javascript in another user’s browser. 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.
Peplink Surf SOHO HW1 v6.3.5 (in QEMU)
Surf SOHO HW1 - https://www.peplink.com/products/soho-series-surf/
3.4 - CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:N/A:N
CWE-80 - Improper Neutralization of Script-Related HTML Tags in a Web Page (Basic XSS)
The Surf series of SOHO routers is marketed as an entry-level router for use at home. It provides networking via USB cellular modems, ethernet and Wi-Fi. The device can host a VPN and supports Wi-Fi meshing.
The device hosts a web interface for administrative configuration. A stored XSS vulnerability exists in the handling of requests destined for the /cgi-bin/MANGA/upload_brand.cgi
endpoint which are intended to interact with the logo and branding management feature. This endpoint is accessible only after successfully authenticating as a user with write privileges on the device. The HTTP POST request must have a parameter, mode
, whose value is set to api
in order to reach the vulnerable code. The OEM of the device must be VISLINK, or the vulnerable brand upload feature will be disabled and inaccessible.
The vulnerable function is located in the file upload_brand.cgi
at offset 0x40d390
in firmware version 6.3.5, and we refer to it as handle_brand_upload
. An annotated decompilation of the function is included for reference.
#define NUM_BLOCKLIST 7
#define LEN_BLOCKLIST 100
char global_blocklist[NUM_BLOCKLIST][LEN_BLOCKLIST] = {
"javascript",
"<script",
"<object",
"<applet",
"<embed",
"<form",
"\0"
};
int handle_brand_upload()
{
// [1] Extract the attacker-controlled parameters
char* brand_web_name = cgi_safe_param("brand_web_name");
char* brand_product_name = cgi_safe_param("brand_product_name");
char* brand_web_name = cgi_safe_param("brand_web_title");
char* brand_product_name = cgi_safe_param("brand_web_link_text");
char* brand_web_name = cgi_safe_param("brand_web_link_url");
char brand_tmp_filepath[] = "/tmp/__upload_brand_conf.XXXXXX";
int fd;
char cmd[0x400] = {0};
// [2] Check only the supplied `brand_web_name` against a short list of 'dangerous' inputs
for (int i = 0; i < NUM_BLOCKLIST; i++)
{
if strcasestr(brand_web_name, global_blocklist[i])
{
xml_error("Request rejected due to prohibited value in brand_web_name.");
return 0;
}
}
// [3] Store the attacker-controlled parameters into a new taglist
taglist_t* brand_tags = create_taglist();
update_taglist_ex(brand_tags, scrub_newlines(brand_product_name), "PRODUCT_NAME");
update_taglist_ex(brand_tags, scrub_newlines(brand_web_title), "WEB_TITLE");
update_taglist_ex(brand_tags, scrub_newlines(brand_web_name), "WEB_NAME");
update_taglist_ex(brand_tags, scrub_newlines(brand_web_link_text), "WEB_LINK_STRING");
update_taglist_ex(brand_tags, scrub_newlines(brand_web_link_url), "WEB_LINK_URL");
fd = mkstemp(&brand_tmp_filepath);
if (fd == -1) {
xml_error("Internal error, please try again later.");
return 0;
}
close(fd);
// [4] Store the tags to a temporary file
save_taglist_ex(brand_tags, &brand_tmp_filepath, 1);
// [5] Persist the data into /etc/brand.conf
snprintf(&cmd, 0x400, "/usr/local/ilink/bin/persistent_data_utils brand -a save -f %s >/dev/null 2>&1", &brand_tmp_filepath);
int result = system(&cmd);
unlink(&brand_tmp_filepath);
if (result != 0) {
xml_error("Unable to save, please try again later.");
return 0;
}
return 1;
}
Upon receiving a properly formed HTTP request, and validating that the OEM of the device is VISLINK, handle_brand_upload
is called. Within this function, the attacker-controlled POST parameters of the request are extracted ([1]
), and brand_web_name
specifically is checked against a blocklist to ensure that none of six prohibited values are contained within the submitted value at [2]
. If brand_web_name
passes these checks, then the values are persisted to the device’s configuration files.
It is not necessary to know the full implementation details for the “taglist” structure except to say that values put into a taglist via update_taglist_ex
([3]
), saved via save_taglist_ex
([4]
) and persisted into the brand configuration file through persistent_data_utils
([5]
), will be made available to any software on the system.
These particular values are used to populate the branding for the web interface, and they’re placed into the HTML within the index.cgi
binary in a function named load_leftmenu
. This function is located at offset 0x40a6a4
and the relevant portion of a decompilation of the function is included below.
void load_leftmenu(int menu_type)
{
char* html_data;
switch(menu_type)
{
case MAIN:
// [6] Recover the attacker-controlled branding from /etc/brand.
taglist_t brand_tags = load_taglist_ex("/etc/brand.conf", 1, 0);
char* web_name = get_taglist_default_ex(brand_tags, "", "WEB_NAME");
char* web_link_string = get_taglist_default_ex(brand_tags, "", "WEB_LINK_STRING");
char* web_link_url = get_taglist_default_ex(brand_tags, "", "WEB_LINK_URL");
// [7] Read the template file and replace the templated data with the brand data
html_data = readfile("html/leftmenu_main.html");
html_data = replace(html_data, "{PROGRAM_NAME}", __javascript_escape(web_name));
html_data = replace(html_data, "{OEL_NAME}" , __javascript_escape(web_link_string));
html_data = replace(html_data, "{OEL_URL}", __javascript_escape(web_link_url));
free_taglist(brand_tags);
break;
...
}
puts(html_data);
free(html_data);
return;
}
At [6]
, the branding taglist is loaded from /etc/brand.conf
and three of the branding tags are loaded. Then, at [7]
, the HTML template for the menu is loaded from disk, and the branding data is used to populate the templated values. While the branding data is passed through __javascript_escape
prior to being injected into the template, the escaping implemented in this function simply prefixes dangerous characters with a backslash with no HTML-specific escaping. However, this escaping does limit this vulnerability to just brand_web_name
and web_link_url
, due to the way in which the template is written. The template file, leftmenu_main.html
, almost entirely consists of jQuery-heavy javascript, and this script is responsible for updating the DOM with the provided branding. Some of these parameters are injected into the template within locations in jQuery code that will not treat the input as an htmlString
, and the __javascript_escape
prevents an attacker from escaping into the <script>
itself. In this instance, only brand_web_name
and web_link_url
will be transformed into HTML elements that can be manipulated into executing javascript.
2023-06-26 - Initial Vendor Contact
2023-06-27 - Vendor Disclosure
2023-10-11 - Public Release
Discovered by Matt Wiseman of Cisco Talos.