TLDR
has released an advisory
- RV34X /upload Authorization Bypass Vulnerability (CVE-2021-1472)
- RV34X OS Command injection in Cookie string (CVE-2021-1473)

In early 2021, we reported a few security issues to Cisco related to their RV34X series of routers, two of which have been recently patched. The issues in question were an authentication bypass and system command injection, both in the web management interface. These can be chained together to achieve unauthenticated command execution.
Cisco , and assigned CVE IDs as follows:
The issues have been fixed in firmware version 1.0.03.21 in the RV34X series. Cisco has noted that the RV26X and RV16X series are also affected by the authentication bypass issue, and has released firmware version 1.0.01.03 to address this.
This post contains a root cause analysis for these bugs. Enjoy!
[caption id="attachment_4060" align="aligncenter" width="719"] © Cisco[/caption]
RV34X/RV26X/RV16X /upload Authorization Bypass Vulnerability (CVE-2021-1472)
nginx
nginx
nginx
nginx
/etc/nginx/
/etc/nginx/
/upload
/upload
/form-file-upload
/form-file-upload
/api/operations/ciscosb-file:form-file-upload
/api/operations/ciscosb-file:form-file-upload
upload.cgi
upload.cgi
upload.cgi
upload.cgi
upload.cgi
upload.cgi
ZDI-20-1100ZDI-20-1101Cisco advisory herenginx
nginx

Authorization
Authorization
/tmp/websession/token/
/tmp/websession/token/
sessionid
sessionid
any non-null Authorization
Authorization
header$deny
$deny
anyAuthorization
Authorization
/upload
/upload
While Cisco has noted that this issue affects other devices, I'll only go over the specifics of how it affects the RV34X series here. On RV34X devices, the web management interface is served by on port 443. is configured (by files in ) so that requests made to the URIs , and are all passed to a CGI binary called . Depending on which URI is requested, the behavior of is slightly different.
In firmware revisions earlier than 1.0.3.20, there was no real attempt to restrict access to these -related endpoints. In fact, a set of command injection issues from late 2020 affecting the RV34X series were initially disclosed as post-authentication issues but later revised to reflect the fact that these could be exploited pre-authentication (after a very charitable and publicly-uncredited researcher - cough cough - tipped off the Cisco PSIRT). These were tracked by the ZDI as and , and you can see the . While the ZDI advisories have not been updated and still show the initial lower CVSS rating – the Cisco advisory and CVSS scores have been updated to reflect the pre-authentication nature of the bugs.
In 1.0.03.20, an authentication check was implemented. This was written into configuration, which you can see here:
The attempt here appears to be to check that some header is set, and/or that a file exists in the folder with the same name as the request cookie. Then a user is assumed to be authorized.
Unfortunately, there’s a fatal flaw in this fix. The logic is such that would set to “0”. So, sending literally valid-looking header as part of a request to will bypass the authorization check.
RV34X OS Command injection in Cookie string (CVE-2021-1473)
/upload
/upload
upload.cgi
upload.cgi
nginx
nginx
uwsgi
uwsgi
main()
main()
upload.cgi
upload.cgi
HTTP_COOKIE
HTTP_COOKIE
sessionid
sessionid
strtok_r
strtok_r
strstr
strstr
sessionid
sessionid
strtok_r
strtok_r
if (HTTP_COOKIE != (char *)0x0) {
StrBufSetStr(cookie,HTTP_COOKIE);
cookie = StrBufToStr(cookie);
cookie = strtok_r(cookie, ";", &saveptr);
cookie = strstr(cookie, "sessionid=");
sessionid_cookie_value = pathparam_ + 10;
if (HTTP_COOKIE != (char *)0x0) {
StrBufSetStr(cookie,HTTP_COOKIE);
cookie = StrBufToStr(cookie);
cookie = strtok_r(cookie, ";", &saveptr);
while (cookie != 0x0) {
cookie = strstr(cookie, "sessionid=");
if (cookie != 0x0) {
sessionid_cookie_value = pathparam_ + 10;
}
}
}
if (HTTP_COOKIE != (char *)0x0) {
StrBufSetStr(cookie,HTTP_COOKIE);
cookie = StrBufToStr(cookie);
cookie = strtok_r(cookie, ";", &saveptr);
while (cookie != 0x0) {
cookie = strstr(cookie, "sessionid=");
if (cookie != 0x0) {
sessionid_cookie_value = pathparam_ + 10;
}
}
}
/upload
/upload
main()
main()
upload.cgi
upload.cgi
000124a4
000124a4
handle_upload()
handle_upload()
sessionid
sessionid
void handle_upload(char *sessionId, char *destination, char *option, char *pathparam, char *fileparam, char *cert_name, char *cert_type, char *password)
void handle_upload(char *sessionId, char *destination, char *option, char *pathparam, char *fileparam, char *cert_name, char *cert_type, char *password)
main()
main()
Depending on what string is passed as the pathparam
pathparam
parameter, slightly different code paths will be taken, which means that slightly different checks must be bypassed to be able to reach the vulnerable code. In this example, I am using a request with the pathparam
pathparam
set to “Configuration”, so the pseudocode I'm showing reflects this.
handle_upload()
handle_upload()
curl
curl
sprintf
sprintf
popen
popen
ret = strcmp(pathparam, "Configuration");
config_json = upload_Configuration_json(destination,fileparam);
post_data = json_object_to_json_string(config_json);
sprintf(command_buf, "curl %s --cookie \'sessionid=%s\' -X POST -H \'Content-Type: application/json\' -d\'%s\' ", jsonrpc_cgi, sessionId , post_data);
debug("curl_cmd=%s",command_buf);
__stream = popen(command_buf, "r");
if (__stream != (FILE *)0x0) {
ret = strcmp(pathparam, "Configuration");
if (ret == 0) {
config_json = upload_Configuration_json(destination,fileparam);
if (config_json != 0) {
post_data = json_object_to_json_string(config_json);
sprintf(command_buf, "curl %s --cookie \'sessionid=%s\' -X POST -H \'Content-Type: application/json\' -d\'%s\' ", jsonrpc_cgi, sessionId , post_data);
debug("curl_cmd=%s",command_buf);
__stream = popen(command_buf, "r");
if (__stream != (FILE *)0x0) {
[...snip...]
}
ret = strcmp(pathparam, "Configuration");
if (ret == 0) {
config_json = upload_Configuration_json(destination,fileparam);
if (config_json != 0) {
post_data = json_object_to_json_string(config_json);
sprintf(command_buf, "curl %s --cookie \'sessionid=%s\' -X POST -H \'Content-Type: application/json\' -d\'%s\' ", jsonrpc_cgi, sessionId , post_data);
debug("curl_cmd=%s",command_buf);
__stream = popen(command_buf, "r");
if (__stream != (FILE *)0x0) {
[...snip...]
}
sessionid
sessionid
sprintf()
sprintf()
sessionid
sessionid
upload.cgi
upload.cgi
www-data
www-data
Once we have bypassed authentication, it’s then possible to interact directly with the endpoint. Requests made to this endpoint are passed directly to the binary by the CGI configuration.
Within the function in , the environmental variable is read, and the value from the cookie is extracted using a simple series of and . This specific -reading logic is notable because, due to the call, it’s not possible to use “;” characters in any injection, as it will prematurely terminate the injection string. In pseudocode, it looks like this:
Because our HTTP request is made to the URI, the function in calls a function at , which I’ve named . This function takes a pointer to the cookie value as its first argument.
It also takes several other arguments, each of which are populated by the multipart request parsing that takes place in the function. The names I’ve given these arguments roughly align with the names of the parameters that this multipart ingesting logic looks for.
Within , a command is constructed with a call to , the resulting buffer of which is then passed directly to :
The cookie value that we have passed in our request is passed directly into this call. With a crafted value, we would therefore be able to inject arbitrary commands into this command buffer. This will run the command with the privileges of the binary which, in this case, is .
Key Takeaways

Logic bugs can be quite easy to introduce, and sometimes tricky to identify. Authentication can be difficult to implement well, especially when multiple authorization methods might be accepted. As higher-end embedded devices start to use more common server software components (for purposes they were not necessarily intended for), there are often more layers of complexity introduced - thicker web servers requiring more precise configuration, CGI binaries, middleware gluing things together. Each layer introduces opportunities for mis-configuration, which could lead to security issues.
Timeline
2021-01-02: Initial disclosure made to Cisco PSIRT.
2021-01-07: Confirmation of receipt of disclosure from Cisco PSIRT.
2021-01-27: Confirmation that issue is valid from Cisco PSIRT.
2021-02-12: Update from Cisco PSIRT.
2021-03-23: We contact Cisco PSIRT for timeline update and CVE IDs.
2021-03-23: Cisco PSIRT respond giving us timeline and CVE IDs.
2021-04-07: Cisco release advisory.