0x00 再续前言

天才少年“Diveing to the deep water”的言论让我打了个机灵,于是想看看大型一点的IoT项目上的安全问题,就这样我和Bug Hunting in RouterOS相遇了。议题中介绍了RouterOS的版本架构、历史研究和开发者后门,作者通过对通信解析的逆向介绍了客户端命令传递至后端的处理过程,最后组合漏洞链完成RCE的利用。不久之后,作者还写了一篇文章蜻蜓点水地补充了一点小细节和提供了利用开发的架构工具,也有同学对整个议题的内容深入分析了一番。

但我向来是一个喜欢知道为什么的人,而且如果要深入挖掘的话,作者没有多讲的逆向过程还是需要我们自己上下求索的,本文结合开发者后门提供的便利调试环境,主要对后端命令的分发过程和议题中触发漏洞message的字段处理及漏洞修复进行了逆向分析,而通信流量的编码和加解密过程以及Web端调用后台程序的过程需花另一篇幅逆向介绍,还请各位大拿海涵。

0x01 探索后门

议题PPT中上来就介绍如何开启开发者“后门”,这一步其实为了方便后续的调试与逆向,因为stable的不同版本使开启方法略有不同,本小节以6.38.4和6.42.4为例简要介绍下方法,环境的搭建可自行搜索

6.38.4

下载routeros-x86-6.38.4.npk,binwalk解包即可看到squashfs-root的文件系统。需要开启后门的话,按照PPT中的说法,需要在原始的文件系统here/flash/nova/etc/下新建一个devel-login文件。做法是使用centos的镜像安全启动原RouterOS系统,自动或手动挂载文件系统,新建至根目录的软链接:

顺便简要看一下在/nova/bin/login处理用户登录过程中存在开发者后门的原因,在程序初始化的过程中有一点判断用户名是否为devel的逻辑,如果是且通过sub_804E052函数的判断则把用户名更换为admin:

继续跟进函数中,有目录的拼接并判断特殊文件是否存在:

nv::getAllDirs对搜索的目录有简单的包装,如此可知如果使用ftp上传,使/flash/nova/etc/devel-login文件存在就能通过校验:

所以当我们使用devel用户名和admin的密码登录成功后,即可获取到返回的shell:

自带的shell还是功能受限的busybox,这时候可以利用同样的思路,ftp上传一个功能齐全的busybox(不能太新不然会段错误),加权限后即可大展身手了:

6.42.4

对于6.42.4这样的高版本系统,作者在Make It Rain with MikroTik文章中提到了RouterOS会在系统的启动脚本S12defconf中执行/rw/DEFCONF的文件内容,控制该文件就能开启后门shell了,原文中专门录屏展示整个开telnetd的过程,此处不赘述。

0x02 流量解密

首先查看监听的端口来寻找可能存在的攻击面,80端口自然是第一考察对象:

Web口登录过程中使用Content-Type: text/plain;charset=UTF-8 Header,认证成功之后使用Content-Type: msg,并且从抓包上看都是向/jsproxy POST加密过后的数据包。正如PPT所讲整个过程可在客户端的/webfig/master-min-xxxxxxxxxxxx.js中知晓,认证和会话密钥的生成用的是MS-CHAP-2协议,但在ChallengeResponse时对PasswordHash的padding做了些更改:

在认证过程中使用的编码是UTF-8,可以在前端调试或者后端找到相应的处理逻辑:

登录成功之后,前端至后端的message以buffer或json的形式传递,其字段值的类型还会对字段名的类型和编码有影响,同样也可以在前端代码中略见:

对于WinBox通信的binary形式的message的格式也是类似的。作者提供了jsproxy_pcap_parserwinbox_pcap_parser工具分别解析这两者的流量,就让我们先站在巨人的肩膀上,以后有机会再结合前后端对流量的包装进行详解。

0x03 消息处理

消息流量的传递处理对于后端程序来说有些RPC的意味,0xff0001数组中的system num决定调用哪个二进制文件,数组中的handler指定了处理函数,还有0xff0007字段的command则代表具体要执行的命令,下面分别对/nova/bin/www/nova/bin/mproxy程序的消息处理过程进行逆向。

/nova/bin/www

PPT中提到/nova/bin下面的二进制文件都可以通过HTTP或Winbox来触发到,CVE-2018-1156的PoC也是如此,向/jsproxy POST json信息的sysyetm num为55,对应地起后端程序/nova/bin/licupgr处理。

/nova/bin/www程序中对于有/jsproxy的请求,其会先将/nova/lib/www/jsproxy.p作为Servlet进行动态加载:

JSProxyServlet::JSProxyServlet的初始化过程中,找到虚表中的JSProxyServlet::doPost为处理POST请求函数,在具体判断Content-Type Header的内容后传递至JSSession::processMessage,通过调试可知解密完消息后传递至threadExchangeMessage 函数,在线程间通过信号量传递消息:

而在www程序Looper::Looper初始化过程中,有对/ram/novasock的socket添加:

所以推测/nova/bin/www/jsproxy传递来的消息,会通过/ram/novasock传递给/nova/bin/loader,由后者根据对/nova/etc/loader/system.x3的解析来调用后端程序处理:

综上只是结合调试信息,对由system num调用后端程序的过程进行了大致推理,作者还提供了parse_x3工具来解析系统号对应的程序,感兴趣的同学可以深入跟踪分析下。而消息中的handler和command会在新起的程序中处理,下小节以/nova/bin/mproxy为例进行分析。

/nova/bin/mproxy

mproxy直接监听的是8291端口,虽然没有loader分发命令的过程,但其处理消息的流程和其他后端程序类似。这里依旧使用CVE-2018-14847的PoC构建调用命令,查看解密后传递的message:

然后在PPT中提到的漏洞触发点下断点,进行栈回溯即可快速看到整个的调用链:

跟踪可知在函数sub_8055048处判断system number是否为2:

nv::Looper::addHandler的逻辑相似,在nv::Looper::dispatchMessage中处理正确的handler后调用nv::Handler::handle,其中涉及到的bff0005字段代表nv::isReplyExpected

nv::Handler::handle函数中间接调用了nv::policies::is_allowed来判断command是否具有相应的policy:

set_policy对应command为0时确实不需要任何认证即可调用:

真正去处理command的函数nv::Handler::handleCmd实则是个大的switch-case,其把未知的命令传递给nv::Handler::cmdUnknown处理:

对于handler 2其偏移76对应函数sub_8052934,也是个小型的switch-case处理未知的命令,由此可探索更多的攻击面,下一小节会分析未知命令中存在的漏洞原理。

0x04 漏洞分析

本小节以6.38.4为例,结合PPT中两个提及的漏洞,主要逆向分析message所需字段的原因。

CVE-2018-1156

CVE-2018-1156是一个需要认证的/nova/bin/licupgr文件的溢出,对应的System Number为55,其初始化过程中没有nv::Looper::addHandler的操作,直接在对应偏移处找到覆盖的nv::Handler::cmdUnknown来处理不同的command:

跟进可知command 1和4均能到达目标的溢出函数sub_804AC9E,同时还需要传递bool_id_7参数:

剩下string_id_1 usernamestring_id_2 password参数导致sprintf的溢出就显而易见了:

CVE-2018-14847

CVE-2018-14847是一个无需认证利用目录穿越实现任意文件读取的漏洞,在拿到用户凭据user.dat文件后,使用写文件操作开启系统后门。对应的/nova/bin/mproxy文件在初始化过程中有和前文照应的nv::Looper::addHandlernv::policies::set_policy操作:

System number和handler均为2,在处理command 7中将文件路径和/home/web/webfig/拼接传递至nv::findFile函数,就算是没有找到文件,其还会返回原始拼接的文件路径:

随后其打开文件路径,并设置u32_id_2为文件的大小:

函数返回前还有对0xFE0001 sessionid的设置:

在处理完command返回消息调用nv::Handler::replyMessage的过程中,也有对0xFF0006 RequestId的处理:

在处理command 4时首先会验证sessionid的合法性:

最终根据u32_id_2的大小从session打开的fd中读取文件数据:

0x05 对比修复

CVE-2018-1156

根据官方的公告可知这个溢出漏洞在6.42.7版本中完成修复,其使用snprintf函数来限制过长的用户输入:

CVE-2018-14847

我们还是聚焦于CVE-2018-14847这个利用目录穿越读取任意文件的漏洞,因为读取得到账号密码才能进行后一步的BTW攻击。从官方的公告可知6.29 至 6.42的current版本在6.42.1中完成修复,我这里就使用6.42.4和6.38.4进行对比。

使用PoC直接去打6.42.4得到File size is 0的返回,初步推断在第一步获取文件大小时可能被直接返回了。通过对/nova/bin/mproxy的调试和bindiff分析,发现6.42.4在抵达handler 2 command 7的结构虽有改动但逻辑没有变化:

调试可知string_id_1在经过tokenize函数的处理后,没能通过sub_8051B80的校验最终报错返回了。tokenize函数主要是把字符串//./.././.././../etc/passwd分解为{".", "..", ".", "..", ".", "..", "etc", "passwd"} 这样的vector string。对于6.42.4patch的关键就在于sub_8051B80函数了:

大致的逻辑就是遇到"."就删除,遇到"larry", ".."就一起删除这两项,但在删除后的遍历操作中如果遇到".."打头,则认为是存在目录穿越的利用,无法通过校验。

反观6.38.4中的对应逻辑,也是不允许".."打头,但没有照顾到".",就产生了作者使用./../的绕过方式:

由于command 4的读取任意文件需要command 7打开文件的fd和返回的session id,一旦读取的文件路径无法通过校验,后续操作自然无法成功利用了。

0x06 总结哈子

  • 全文看下来虽说是逆向分析但难免有些钻牛角尖,局部的逆向是为了快速分析理解漏洞,全局的逆向分析则为了理解程序后端逻辑和寻找攻击面,也不可能锱铢必较。
  • 大体可以看出两点攻击面,一是自定义消息的处理解析,而是后端程序未知命令的处理逻辑。
  • 最近也有一篇RouterOS SMB服务溢出利用的Write Up,其将注意力放在了不是默认开启的SMB服务上,并快速使用dumb fuzz出crash也是个不错的尝试思路。