CVE-2018-1158 MikroTik RouterOS漏洞分析之发现CVE-2019-13955

阅读量    67008 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

漏洞简介

CVE-2018-1158是MikroTik路由器中存在的一个stack exhaustion漏洞。认证的用户通过构造并发送一个特殊的json消息,处理程序在解析该json消息时会出现递归调用,造成stack exhaustion,导致对应的服务崩溃重启。

该漏洞由Tenable的Jacob Baines发现,同时提供了对应的PoC脚本。另外,他的关于RouterOS漏洞挖掘的议题《Bug Hunting in RouterOS》非常不错,对MikroTik路由器中使用的一些自定义消息格式进行了细致介绍,同时还提供了很多工具来辅助分析。相关工具、议题以及PoC脚本可在git库routeros获取,强烈推荐给对MikroTik设备感兴趣的人。

下面利用已有的PoC脚本和搭建的MikroTik RouterOS仿真环境,对该漏洞的形成原因进行分析。

 

环境准备

MikroTik官方提供多种格式的镜像,其中可以利用.iso或者.vmdk格式的镜像,结合VMware虚拟机来搭建仿真环境。具体的搭建步骤可参考文章 Make It Rain with MikroTikFinding and exploiting CVE-2018–7445,这里不再赘述。

根据Tenable的漏洞公告可知,该漏洞在6.40.9、6.42.7及6.43等版本中修复。为了便于对漏洞进行分析和补丁分析,选取的相关镜像版本如下。

  • 6.40.5,x86架构,用于进行漏洞分析
  • 6.42.11,x86架构,用于进行补丁分析

搭建起仿真环境后,由于RouterOS自带的命令行界面比较受限,只能执行特定的命令,不便于后续进一步的分析和调试,因此还需要想办法获取设备的root shell。同样,Jacob Baines在他的议题《Bug Hunting in RouterOS》中给出了相应的方法,这里采用修改/rw/DEFCONF的方式。对于该文件的修改,可以通过给Ubuntu虚拟机添加一块硬盘并选择对应的vmdk文件,然后进行mount并修改。

需要说明的是,采用这种方式进行修改后,每次设备启动后/rw/DEFCONF文件会被删除,如下。

# /etc/rc.d/run.d/S12defconf
elif [ -f /rw/DEFCONF ]; then
    # ...
    defcf=$(cat /rw/DEFCONF)
    echo > /ram/defconf-params
    if [ -f /nova/bin/flash ]; then
    /nova/bin/flash --fetch-defconf-params /ram/defconf-params
    fi
    (eval $(cat /ram/defconf-params) action=apply /bin/gosh $defcf;
     cp $defcf $confirm; rm /rw/DEFCONF /ram/defconf-params) &  # /rw/DEFCONF 被删除
fi

这样下次如果需要获取root shell,还需要再重新挂载并修改,比较麻烦。可行的解决方式如下:

  1. 在修改/rw/DEFCONF文件后,创建一个虚拟机快照,下次直接恢复该快照即可;
  2. 在修改/rw/DEFCONF文件后,将其拷贝一份保存到其他路径,获取到设备root shell后再拷贝一份到/rw路径下。

 

漏洞分析

根据漏洞公告可知,与该漏洞相关的程序为www。在设备上利用gdbserver附加到该进程进行远程调试,然后运行对应的PoC脚本,在本地的gdb中捕获到如下异常。

(gdb)
Thread 2 received signal SIGSEGV, Segmentation fault.
[Switching to Thread 267.373]
=> 0x777563f5 <pthread_mutex_lock+9>:   call   0x7775a948
   0x777563fa <pthread_mutex_lock+14>:  add    ebx,0x7c06
   0x77756400 <pthread_mutex_lock+20>:  mov    esi,DWORD PTR [ebp+0x8]
   0x77756403 <pthread_mutex_lock+23>:  mov    edi,DWORD PTR [esi+0xc]
// ...

// stacktrace
(gdb) bt
#0  0x777563f5 in pthread_mutex_lock () from <path>/mikrotik-6.40.5/_system-6.40.5.npk.extracted/squashfs-root/lib/libpthread.so.0
#1  0x77573cc3 in malloc () from <path>/mikrotik-6.40.5/_system-6.40.5.npk.extracted/squashfs-root/lib/libc.so.0
#2  0x775a5c3e in string::reserve(unsigned int) () from <path>/mikrotik-6.40.5/_system-6.40.5.npk.extracted/squashfs-root/lib/libuc++.so
#3  0x775a5ecd in string::assign(char const*, char const*) () from <path>/mikrotik-6.40.5/_system-6.40.5.npk.extracted/squashfs-root/lib/libuc++.so
#4  0x775a5f1d in string::assign(string const&) () from <path>/mikrotik-6.40.5/_system-6.40.5.npk.extracted/squashfs-root/lib/libuc++.so
#5  0x77788942 in void nv::message::insert<nv::string_id>(nv::string_id, nv::IdTraits<nv::string_id>::set_type) () from <path>/mikrotik-6.40.5/_system-6.40.5.npk.extracted/squashfs-root/lib/libumsg.so
#6  0x77504b63 in ?? () from <path>/mikrotik-6.40.5/_system-6.40.5.npk.extracted/squashfs-root/nova/lib/www/jsproxy.p
# ...
#1102 0x77504bd3 in ?? () from <path>/mikrotik-6.40.5/_system-6.40.5.npk.extracted/squashfs-root/nova/lib/www/jsproxy.p
# ...

为了便于分析,临时关闭了系统的ASLR机制.

查看栈回溯信息,可以看到存在大量与0x77504bd3 in ?? () from …/jsproxy.p相关的栈帧信息,与漏洞描述中的”递归解析”一致。根据PoC中数据内容格式”{m01: {m01: … }}”,结合单步调试,定位漏洞触发的地方在sub_77504904()函数中,其被json2message()函数调用,核心代码片段如下。

sub_77504904()
{
    // ...
    switch ( (_BYTE)a3 )
    {
        case 'b':
            v9 = v6;
            v10 = strtoul(nptr, &nptr, 0);
            nv::message::insert<nv::bool_id>(v58, v59, v10 != 0, v9);
            break;
        case 'm':
            if ( v55 != '{' )
                return v3;
            ++nptr;
            nv::message::message((nv::message *)&v63);
            v17 = sub_77D61904(nptr, (int)&v63, v16);  // !!!递归调用
            v3 = v17;
            nptr = v17;
            if ( *v17 != '}' )
            {
                nv::message::~message((nv::message *)&v63);
                return v3;
            }
            // ...
            break;
        case 'a':
            if ( v55 != '[' )
            // ...
    // ...
}

以”{m01: {m01:{m01: ” “}}}”为例,其主要处理逻辑为:先解析前面的”{m01: “,执行到switch语句时,匹配”case ‘m'”分支,然后再次调用sub_77504904()函数,此时数据变为”{m01: {m01: “” }}”,处理逻辑和之前相同。因此,只需要发送的数据包中包含足够多的重复模式,在解析该数据时会造成函数的递归调用,从而不断开辟栈帧,,最终导致”stack exhaustion”。

 

补丁分析

版本6.42.11中修复了该漏洞,基于前面对漏洞形成原因的分析,在程序jsproxy.p中定位漏洞触发的代码片段,如下。可以看到,该代码片段的处理逻辑与之前类似,但在调用函数sub_7750DCFC()时多了一个参数,用来限制递归的深度。

sub_7750DCFC()
{
    // ...
    switch ( (_BYTE)a3 )
    {
        case 'b':
            v9 = v6;
            v10 = strtoul(nptr, &nptr, 0);
            nv::message::insert<nv::bool_id>(v62, v63, v10 != 0, v9);
            break;
        case 'm':
            if ( a4 > 0xA || v59 != '{' )       // a4:限制递归深度
                return v4;
            ++nptr;
            nv::message::message((nv::message *)&v67);
            v18 = sub_7750DCFC(nptr, (int)&v67, v17, a4 + 1); // !!!递归调用
            v4 = v18;
            nptr = v18;
            if ( *v18 != 125 )
            {
                nv::message::~message((nv::message *)&v67);
                return v4;
            }
            // ...
            break;
        case 'a':
            // ...
    // ...
}

 

未知漏洞发现

在对补丁进行分析时,通过IDA的交叉引用功能,发现该函数还存在另一处递归调用,如下。

调用处的部分代码片段如下。可以看到,在处理对应的消息类型M时,也会调用sub_7750DCFC()函数自身,但是却没有对递归调用深度的限制,因此猜测这个地方很可能存在问题。

sub_7750DCFC()
{
    // ...
    if ( (_BYTE)a3 == 'M' )    // 消息类型M
    {
      if ( v59 != '[]' )
        return v4;
      vector_base::vector_base((vector_base *)&v69);
      ++nptr;
      while ( 1 )
      {
        v4 = nptr;
        v52 = *nptr;
        if ( *nptr == ']' )
          break;
        if ( !v52 )
          goto LABEL_151;
        if ( v52 == ' ' || v52 == ',' )
        {
          ++nptr;
        }
        else
        {
          nv::message::message((nv::message *)&v65);
          v54 = sub_7750DCFC(nptr, (int)&v65, v53, a4 + 1);  // !!!递归调用,没有对a4进行判断
          v4 = v54;
          nptr = v54;
          if ( *v54 != '}' )
          {
              // ...
       // ....
}

根据Jacob Baines议题《Bug Hunting in RouterOS》中对json消息格式的介绍,消息类型M与消息类型m对应,m表示单个Message,而M表示”Message array”。

图片来源:Jacob Baines议题《Bug Hunting in RouterOS》

通过构造一个简短的payload: “{M01:[M01:[M01:[]]]}”,然后利用gdb进行调试,发现确实可以到达对应的函数调用点,该函数会递归调用自身来对数据进行解析,与之前对消息类型m的处理逻辑相似。接着,利用一个简单的脚本来产生大量包含这种模式的数据,然后修改CVE-2018-1158 PoC中对应的数据,在版本为6.42.11的设备上进行验证,可以看到进程www确实崩溃了。

msg = "{M01:[M01:[]]}"
for _ in xrange(2000):
    msg = msg.replace('[]', "[M01:[]]")

通过代码静态分析,该未知漏洞在”Long-term”最新版本6.43.16上仍然存在。

该漏洞(CVE-2019-13955)目前已被修复,建议及时升级到最新版本。

 

小结

  • 该漏洞触发的原因为:程序在对某些特殊构造的数据进行解析时存在递归调用,从而造成”stack exhaustion”;
  • 对该漏洞的修复主要是在递归调用函数时增加了一个参数,用来限制递归调用的深度;
  • 对该漏洞进行修复时未考虑全面,仅对消息类型为m的数据增加了递归调用深度的判断,而通过构造消息类型为M的数据仍可触发该漏洞。

 

相关链接

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多