MikroTik-RouterOS 相关漏洞 CVE-2019-13954 分析
2019-08-21 05:45:53 Author: mp.weixin.qq.com(查看原文) 阅读量:55 收藏

本文作者:cq674350529(信安之路 2019 年度优秀作者)

CVE-2019-13954MikroTik RouterOS中存在的一个memory exhaustion漏洞。认证的用户通过构造并发送一个特殊的POST请求,服务程序在处理POST请求时会陷入"死"循环,造成memory exhaustion,导致对应的服务程序崩溃或者系统重启。

该漏洞与CVE-2018-1157类似,是由于对漏洞CVE-2018-1157的修复不完善造成。下面通过搭建MikroTik RouterOS仿真环境,结合漏洞CVE-2018-1157PoC脚本及补丁,对漏洞CVE-2019-13954进行分析。

CVE-2018-1157漏洞分析

MikroTik RouterOS环境的搭建、root shell的获取及相关资料可参考文章《CVE-2018-1158 MikroTik RouterOS 漏洞分析之发现 CVE-2019-13955 》:

https://cq674350529.github.io/2019/08/15/CVE-2018-1158-MikroTik-RouterOS%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E4%B9%8B%E5%8F%91%E7%8E%B0CVE-2019-13955/

根据Tenable的漏洞公告:

https://www.tenable.com/security/research/tra-2018-21

漏洞CVE-2018-11576.40.96.42.76.43等版本中修复。为了便于对漏洞CVE-2018-1157进行分析,选取的相关镜像版本如下。

  • 6.40.5x86架构,用于进行漏洞分析

  • 6.42.11x86架构,用于进行补丁分析

为了便于分析,临时关闭了系统的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:

https://www.tenable.com/security/research/tra-2018-21

Two vulnerabilities found in MikroTik's RouterOS:

https://seclists.org/fulldisclosure/2019/Jul/20

Mikrotik RouterOS Changelogs:

https://mikrotik.com/download/changelogs/long-term-release-tree


文章来源: https://mp.weixin.qq.com/s?__biz=MzI5MDQ2NjExOQ==&amp;mid=2247491171&amp;idx=1&amp;sn=d5dc8cd69b69c423a99b2af18ffeb9bc&amp;chksm=ec1e2e4bdb69a75d0b496be9b9952cb348d80032801515454e5f1d9e3fc73cd7a30c54fe51ce#rd
如有侵权请联系:admin#unsafe.sh