漏洞简介
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 MikroTik 和 Finding 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,还需要再重新挂载并修改,比较麻烦。可行的解决方式如下:
- 在修改/rw/DEFCONF文件后,创建一个虚拟机快照,下次直接恢复该快照即可;
- 在修改/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的数据仍可触发该漏洞。