CVE-2019-12103 使用Ghidra分析TP-Link M7350上的预认证RCE
2019-08-20 10:02:00 Author: xz.aliyun.com(查看原文) 阅读量:166 收藏

本文是翻译文章,原文链接:https://www.pentestpartners.com/security-blog/cve-2019-12103-analysis-of-a-pre-auth-rce-on-the-tp-link-m7350-with-ghidra/

译者注:文章的前半篇讲的比较基础...可以适当跳过,后半篇主要讲漏挖

TP-Link M7350 (V3)受到预认证(Pre-Auth)后认证(Post-Auth)CVE-2019-12104命令注入漏洞的影响

如果攻击者位于同一LAN上或者能够访问路由器Web界面,则可以远程利用这些注入。CVE-2019-12103也可以通过跨站点请求伪造(CSRF)在任何浏览器中利用,因为在用户登录之前没有CSRF保护

如果您正在运行其中一个设备,请立即更新到新固件(版本190531)。

无论如何,这篇文章是技术性的,关于使用Ghidra找到这样的问题,通过逆向工程找到命令注入漏洞很有趣!

大多数消费级网络硬件都带有嵌入式网络服务器。Web服务器是用户使用GUI访问设备配置的一种非常简单的方法,无需安装专有软件。很多时候,Web服务器会暴露某种API端点 - 有时候是JSONXML。这个Web服务器API通常只是shell命令的一个简陋包装器(wrapper)。传递给webserver API的变量只是传递给shell命令,因为大多数消费级网络硬件只是运行Linux。

当开发人员不是非常非常小心的时候,你会发现某种任意的命令执行是可能的。我很少看到路由器在暴露的接口上没有命令注入或内存相关的漏洞

所以,这是在TP-Link M7350中找到一个非常方便的命令注入的故事。

正如他们声称的“闪电”一样,M7350只是另一个基于Qualcomm的蜂窝热点——在这种情况下它正在运行(此时相对古老的)MDM9225。

从我们的角度来看,我们希望看到运行的是什么,以便尝试发现漏洞。幸运的是,固件可以从这里被找到

我正在使用硬件版本3.0的M7350,因此我的固件文件名为M7350(EU)_V3_160330_1472438334613t.zip

这个文件本身只是一个ZIP,其中包含PDF安装说明,存在问题的是另一个名为M7350(EU) 3.0_1.1.1 Build 160330 Rel.1002n_User.zip.zip的ZIP。在其中,我们发现:

这看起来非常像 没有尝试混淆或者加密(zero-attempt-at-obfuscation-or encryption) 的固件更新文件

我们甚至可以在META-INF/com/google/android/updater-script看到固件更新的脚本

我们已经在这里使用Android更新包编写了关于攻击设备的文章。但这有点超出了这个特定的搜索范围——我们想要网络界面中的错误!

那么,首先我们如何确定哪些二进制文件对我们有意义?

使用grep

我们可以用grep来找出一些我们可能控制的关键变量,可能是因为我在WSL中使用的90%的命令都是grep

无论如何,利用burpsuite来获得M7350的主要web界面信息,我们可以看到一些变量名称,这可能有助于我们找到处理它们的二进制文件。

通用配置请求将发送到/cgi-bin/qcmap_web_cgi,POST主体是JSON编码的,验证后请求需要token值。module参数很有意思,因为它表明会有一个很大的switch case语句在某处运行,它基于请求的模块,来用不同的方式处理输入数据。

那么,让我们来留意webServer,看看它出现在哪里。

为了避免无关的垃圾输出,我传递了-o标志,这只显示我们在文本文件中找到的实际字符串。无论如何,我们只对二进制文件感兴趣,-r则表示递归grep

webServer出现在二进制文件QCMAP_Web_CLIENTqcmap_web_cgi

我们先来看看qcmap_web_cgi,如果您记得上面的POST请求,qcmap_web_cgi是所有正在POST的端点。因此,它可能负责管理每个请求的处理方式

Ghidra非常擅长这一点

打开qcmap_web_cgi二进制文件,我们可以先从搜索字符串开始进行分析(Search->For Strings)

单击搜索对话框,保留默认设置后,我们可以开始看到很多字符串——包括我们的webServer字符串

双击该条目将我们带到webServer位于内存中的位置。Ghidra指出这个地址在二进制文件的其他地方被交叉引用(使用XREF注释)

我们可以双击那个交叉引用,它将把我们带到引用webServer的函数。我已经重命名了它——它通常被称为FUN_xxx,如FUN_00008ce0。我认为FUN代表“功能”而不仅仅是“有趣”。虽然逆向工程是“有趣的”,但大多数时候我不称之为“有趣”。

Ghidra的反编译器非常好(我们稍后会介绍),所以我们可以很容易地看到这个函数的逻辑是什么。

字符串传递给函数,如果字符串是webServer,则返回1,简单。

然后,我们可以向后跟踪此函数,以弄清楚它是如何被调用的以及为什么。右键单击反汇编视图中的函数名称(在反编译视图中不起作用,我不明白...),然后单击参考 -> 显示调用树

这将给出一个非常简单的可扩展项目符号列表,列出函数的调用位置——以及它调用的内容。

在左侧,您可以看到传入的引用 - 由FUN_00008d78调用webServer_or_status,它本身由main(以及之前的ELF条目)调用。在右侧,显示webserver_or_Status仅调用strcmp

然后我们可以开始讨论函数,只关注可能影响输入值的函数。

FUN_00008d78对我们来说很无聊,它主要是从环境变量中提取数据,从JSON中提取内容并在适当的地方执行身份验证检查。

那么,让我们来看看主要功能。

因此,这是调用FUN_00008d78的main的一部分——从webServer_or_status升级一级。所有其他if-else块都是错误处理——如果请求格式错误或不完整,则抛出错误。这个突出显示的代码块可以完成所有有效请求的工作。

您会注意到FUN_00008d78没有返回任何内容,然后调用FUN_000092ec

FUN_000092ec实际上非常有趣。即使单从调用树看,你也可以看到它调用其他打开套接字的函数,并执行sendtorecv调用,还至少有一个系统调用。

您可能期望从与Web服务器相邻的二进制文件中出现类似的内容——但请记住,此二进制文件根本不处理HTTP服务器套接字活动。这个二进制文件本身不是Web服务器——它只是实际Web服务器传递HTTP请求的端点。任何套接字活动都完全在做其他事情。让我们的RE旅程更长一些,但也许会发现有趣的东西。

好的,回到二进制文件。正如您在调用树中看到的那样,FUN_000092ec正在调用FUN_00008f3c,它正在执行socketsystemsendto syscalls!让我们看一下(用一点手动变量名称清理):

二进制文件bind到套接字文件/www/qcmap_cgi_webclient_file,然后再把请求的数据sendto到套接字文件/www/qcmap_webclient_cgi_file

对我们来说,所有这些意味着我们现在必须扩大我们的搜索范围。由于数据被推出qcmap_web_cgi,我们需要弄清楚它的去向以及发生了什么。

我没有选择grep,是grep选择了我

让我们来看看qcmap_webclient_cgi_file文件,另一个过程可能就是监听此文件。上帝,我爱grep

$ grep -r qcmap_webclient_cgi_file
Binary file data/bin/QCMAP_Web_CLIENT matches
Binary file system/WEBSERVER/www/cgi-bin/qcmap_web_cgi matches

只有2个结果,我们已经分析完了无聊的qcmap_web_cgi,现在只有有趣的QCMAP_Web_CLIENT,您可能还记得我们之前的webServer grep

现在我们把这个有趣的文件装入Ghidra,我们再次检查字符串,这次webServer出现了几次

但是,这一次,点击它的地址只显示webServer漂浮在null的海洋中。

没有直接的交叉引用,然而,向下滚动一点,并且有一些非空字节。你可以通过右键单击第一个->数据 - >dword将它们转换为双字

这看起来非常像查找表。如果你双击dword 15384h,你会发现自己在binary中的那个偏移量的位置。它看起来非常像函数的开头,目测像是将请求解析到webServer API模块的功能。

你能发现漏洞吗?我们将在一秒钟内回到这一点。

您可以通过右键单击FUN_00015384->重命名功能重命名此功能。我选择了名称API_webServer_function

如果像我一样,你想确保查找表中指向0x00015384的指针实际显示你的新函数名,你可以回到指针,右键单击它->引用->创建内存引用。为了更方便,一旦您知道查找表的结构,只需在单击dword的第一个字节时按p键即可立即将其转换为指向函数的指针

然后,反汇编视图将显示函数名称,而不仅仅是原始双字

如果我们想要彻底,我们可以向上滚动到看起来像查找表的开头,查看它是否在函数中引用,并分析该函数的作用。

向上滚动到查找表的开头是字符串lan,这是由我重命名为parse_json的函数引用的

parse_json函数非常大,但它引用lan字符串表明它是如何使用此查找表的

这个do-while循环从请求JSON中获取模块名称,并且从lan的地址开始 以0x44的增量循环每个相对偏移。每个循环,strcmp是用户提供的字符串,传递给module参数,字符串位于查找表中每个条目的开头,直到匹配为止。然后它调用相关的函数。我怀疑它看起来像开发人员实际编写的查找函数——但这就是它在反编译的伪代码中的样子。

回到bug

刚才有一些逆向的内容让人分析,让我们回到API_webServer_functionGhidra为我们准备了一个非常好的切换语句供我们仔细阅读

提取用户提供的来自JSON请求的action值(来自iVar1 + 0x14),并且switch case根据其值运行。

因此,如果我们发送包含{"module":"webServer","action":0}之类的请求,则QCMAP_Web_CLIENT进程将使用参数uci get webserver.user_config.language调用函数call_popen

然后它创建一个JSON对象,并将从call_popen获得的值作为language值返回。

call_popen是我自己给这个函数的名字。它只是popen系统调用的一个简单的wrapper,带有一些错误检查和返回值处理。这是完整的:

popen调用本身是突出显示的

盛大的popening

popen字面上运行系统级命令。它很像systemexec。将不受信任的用户输入直接传递给它存在问题,但这正是二进制文件所做的。

如果操作为1,则language参数的值将传递给由snprintf函数构造的shell命令字符串,然后将其传递给call_popen

“但”——我听到你们齐声说 ——“SNPRINTF还有什么额外的参数?”

这真的是精明和敏锐的你,聪明的你。好吧,答案是,反编译器并不完美。我们期待看到:

snprintf(char_array_204,200,"uci set webserver.user_config.language=%s;uci commit webserver", *(iVar1 + 0x10));

但我们没有。这就是我们所全部看到的。

ARM中的函数调用

ARM中的函数调用类似于x86_64,因为参数存储在寄存器中

R0应包含第一个参数,R1表示第二个参数,R2表示第三个参数...等等

我们正在查看一个snprintf调用,如果要填充一些格式字符串,则应至少使用4个参数。而且uci set ...命令字符串中的%s绝对是格式字符串。

应以下列格式调用snprintf

int snprintf(char* ssize_t nconst char* format...)

我们分析...,它会将任意多个字符串格式化地填入,由于已经注意到了明确的格式化字符串,我们希望R0,R1,R2,R3包含这个函数调用的参数

事实上我们可以在反汇编中看到R3,我们希望指向用于控制的language参数的寄存器,而它正在被设置,让我们看看是怎么回事

首先,来自cJSON_GetObjectItem的返回值被赋予R6(返回值存储在R0中,但在此处标记为language_val,因为我在反编译器视图中将其重命名)

是的,我知道这是一个SUBtract指令,但有时在ARM反汇编中你会看到各种ADD或SUB的值为0x0,而不是直接MOV赋值,两者有一个关键的差异

它是SUBS而不仅仅是SUB的事实意味着根据操作的结果更新标志位flag。因此,如果SUBS指令导致R6等于零,则零标志(ZF)将被设置为1,并且将遵循下一个BEQ分支命令。

所以只需要几条指令就可以了。

我们也可以在伪代码中看到:

iVar1 != 0检查空返回值

回到汇编,从cJSON_GetObjectItem调用返回的对象的指针已移至R6,然后,*(ptr+0x10)的存储器中的值移动到R3。然后CMP指令检查它是否为空。

我们可以通过读取伪代码的其他部分进行有根据的猜测,即从cJSON_GetObjectItem返回的对象的偏移量0x10包含指向用户提供的字符串值的指针。然后有一个快速CMP,以确保指针不为空。再次,我们可以看到在伪代码中反映出来:

但是,由于某种原因,Ghidra反编译器没有考虑到R3仍然填充的事实,即使在CMP之后,并且不包括在伪代码中。那好吧。至少我们现在肯定知道它在那里。

sry,回到刚刚的bug

现在它应该是不言而喻的,但是将language设置为shell命令将导致我们的shell命令被snprintf字面地包含在uci set ...字符串中。当该字符串传递给popen时,该命令将被执行。

我们现在知道伪代码应该是这样的:

因此,我们提供给language参数的值将替换uci set ...字符串中的%s格式字符串,该值存储在acStack224中,最后popen就被调用

因此,以下请求将生成telnetd。Pre-authentication

接着,我们可以登录设备,并准备接下来的“掠夺”

所以只是一个仅限局域网的RCE?

好吧,不完全是。蜂窝调制解调器连接到APNAPN就像电信公司提供的大规模局域网。配置APN不一定要求严格——例如,不实施客户端隔离。在这些情况下,任何非常顽皮的人都可以连接到相同的APN,因为您可以访问蜂窝调制解调器的Web配置界面。任何非常顽皮的访问电信GGSN的人也可能能够连接到路由器的Web界面——假设路由器不阻止通过蜂窝接口访问。

还有可能存在drive-by JavaScript跨站点请求伪造攻击。在JavaScript中,很容易枚举路由器所在的位置,查看它是否存在漏洞,并伪造可能执行代码的请求。您可以在我们的旧帖子中看到此类攻击的示例。所以,一个讨厌的页面可以在您的路由器上执行任意代码。除了访问完全不相关但恶意的页面之外,您无需执行任何操作。

这是注入命令的JavaScript,等待500毫秒,并将语言设置为正常:

修复bug

TP-Link在固件更新190531中解决了这个问题。修复了什么?

使用了单引号转义格式化字符串,聪明。

蜂窝调制解调器中的错误仍然很常见。这只是我们在M7350中发现的一个漏洞的例子。公平地说,我只花了一天左右的时间。因此,可能会有更不明显的问题。其他TP-Link设备可能会有更多。快乐狩猎!

TP-Link的回应:

26/02/2019 - 首次接触尝试。
02/03/2019 - 第二次接触尝试。
12/03/2019 - 第三次接触尝试。
18/03/2019 - TP-Link终于回复了。
18/03/2019 - 发送一个命令注入问题的详细信息。
02/04/2019 - TP-Link确认收到电子邮件。
18/04/2019 - TP-Link确认存在问题,表示他们正在努力解决问题。
18/04/2019 - TP-Link提供用于测试的beta固件。
25/04/2019 - 我有时间查看这个固件,找到另一个bug。
29/04/2019 - TP-Link提供另一个更新的固件,修复了这个第二个错误。
14/04/2019 - 我发现有更多时间再次查看此固件,确认修复。
03/06/2019 - TP-Link发布固件版本190531

TP-Link曾表示此问题仅影响M7350硬件版本3,我不完全确定这是否属实。我一直希望,在收到命令注入漏洞报告后,他们会为其他非常类似的问题提供整个代码库的审计,但我猜TP-Link不会。


文章来源: http://xz.aliyun.com/t/6017
如有侵权请联系:admin#unsafe.sh