Advisory: Cisco RV34X Series – Privilege Escalation in vpnTimer

TL;DR

A few weeks ago, we published an advisory on the Cisco RV series routers, where we outlined the root cause for authentication bypass and remote command execution issues.

This week, Cisco has released an advisory for another bug we reported around the same time: A privilege escalation issue, which could be used in combination with the other two issues to run arbitrary code with root privileges on affected RV34X devices. As embedded devices often run everything with root privileges, it’s relatively uncommon that we have the opportunity to find privilege escalation bugs, so this is a particularly interesting case. In this post, we’ll do a quick root-cause analysis of this bug we found.

Cisco RV340
© Cisco

 

Affected vendor & product Cisco Small Business RV Series Router (www.cisco.com)
Vulnerable version RV34X 1.0.3.20 & below,
Fixed version RV34X series: 1.0.03.21.
CVE IDs CVE-2021-1520
Impact 6.7 (medium) CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H
Credit T. Shiomitsu, IoT Inspector Research Lab

RV34X Privilege Escalation in vpnTimer (CVE-2021-1520)

It’s not often that one gets to report a privilege escalation in the embedded security world. So often, everything is running as root, that opportunities for privilege escalation are few and far between. However, on the RV34X series, the nginx server runs as the www-data user. This isn’t particularly useful for an attacker and, as such, a privilege escalation exploit is required in order to run code as root.

Luckily, we identified a vulnerable service called vpnTimer, which runs as root, and exposes a socket on the local loopback interface (on UDP/9999). This service receives data on this socket and passes it insecurely to a system command string. As such, if an attacker can run lower-privileged code on an RV34X device, they would be able to then send a crafted UDP packet to 127.0.0.1:9999, which will run an arbitrary command with root privileges.

vpnTimer is simply a Perl script, which waits for connections on UDP/9999. process_timer() is the main function of this script, and runs in a loop as follows (with some extraneous code snipped):

 

sub process_timer() { 
  while(1) { 
    @sockets_ready = $select->can_read(1); 
    if (! scalar(@sockets_ready)) { 
      [...snip...] 
    } else { 
      #print("$cur_min : $cur_sec\n"); 
      foreach $socket_new (@sockets_ready) { 
        if (! recv($socket_new, $message, 1024, 0)) { 
          print "Error reading from socket: $!\n"; 
        } else { 
          my $temp=substr($message,1,); 
          [...snip...]

          if (index(substr($message,0,1),"+") == 0){ 
            my $isTVPNC=`uci get strongswan.$temp`; 
           chomp $isTVPNC; 
           if ($isTVPNC eq "client"){ 
             system("tvpnc_timer $temp &"); 
           } else { 
             my $interval=`uci get strongswan.$temp.keep_alive_interval`; 
             chomp $interval; 
             $conn_time{$temp}=$interval; 
              addtimer($temp,$interval,1); 
           }

Highlighted here are the key components. The $message is read from the socket. $temp is initialized to contain the contents of $message without the first character (removed with this call to substr).

The first character of $message is then checked, to see if it is a “+” character. If it is, the value of $temp is passed to a statement within two separate backtick strings. In Perl, statements within backticks are executed as a system shell command. Since the vpnTimer service is running as root, the command will be run as root.

As such, sending a packet to UDP/127.0.0.1:9999 with the content “+;touch /tmp/test;“ will result in command uci get strongwan.;touch /tmp/test; being run, and the file /tmp/test being written to the filesystem by the root user.

How Long Was This Bug There?

We thought it might be interesting to see if we could quickly figure out how long this bug has been affecting the Cisco RV34X devices. Often, our assumption might be that a script with a 2015-dated copyright string at the top has not been thought about or actively changed since 2015. However, assumptions are always good to challenge.

With our API, it’s relatively simple to script a set of GraphQL queries, and spit out a list of file hashes for all version of vpnTimer for all available RV34X firmware images:

vpnTimer hashes over time.

We can very easily see that between firmware versions 1.0.01.20 (released on the 19th October 2018) and 1.0.02.16 (released 1st January 2019), the vpnTimer script was actually slightly edited.

The changes were not significant, mainly adding a signal_handler() function, and fixing a couple of typos:

vpnTimer diff

New function and logging lines added.

vpnTimer diff

A typo being fixed.

But it does illustrate that this script was actively developed in the past couple of years, and that a bunch of small extra functionalities were added. This script may not have necessarily been thought about deeply, but it was certainly not a forgotten relic. This info does also show that this bug in the vpnTimer script has been present since at least the first firmware update package of the RV34X series (February 2017).

Key Takeaways

When implementing different privilege levels, it’s always important to follow the principle of least privilege. Does the component need root privileges, or is it possible to run it with a more limited set? In a well-hardened system, an attacker would be hard-pressed to come across a component running as root, which does not absolutely require root privileges. In more complex systems, you may even consider implementing SELinux (well) to provide an even more granular set of permissions to each process.

Despite all this, since so many embedded devices run everything with root privileges, it’s still heartening to see when privilege separation is at least attempted in a device. Thanks for the challenge, Cisco!