译者注:文章的前半篇讲的比较基础...可以适当跳过,后半篇主要讲漏挖
TP-Link M7350 (V3)受到预认证(Pre-Auth)和后认证(Post-Auth)CVE-2019-12104命令注入漏洞的影响
如果攻击者位于同一LAN上或者能够访问路由器Web界面,则可以远程利用这些注入。CVE-2019-12103
也可以通过跨站点请求伪造(CSRF)在任何浏览器中利用,因为在用户登录之前没有CSRF保护
如果您正在运行其中一个设备,请立即更新到新固件(版本190531)。
无论如何,这篇文章是技术性的,关于使用Ghidra
找到这样的问题,通过逆向工程找到命令注入漏洞很有趣!
大多数消费级网络硬件都带有嵌入式网络服务器。Web服务器是用户使用GUI访问设备配置的一种非常简单的方法,无需安装专有软件。很多时候,Web服务器会暴露某种API端点 - 有时候是JSON
或XML
。这个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来找出一些我们可能控制的关键变量,可能是因为我在WSL
中使用的90%的命令都是grep
无论如何,利用burpsuite来获得M7350的主要web界面信息,我们可以看到一些变量名称,这可能有助于我们找到处理它们的二进制文件。
通用配置请求将发送到/cgi-bin/qcmap_web_cgi
,POST主体是JSON编码的,验证后请求需要token
值。module
参数很有意思,因为它表明会有一个很大的switch case语句
在某处运行,它基于请求的模块,来用不同的方式处理输入数据。
那么,让我们来留意webServer
,看看它出现在哪里。
为了避免无关的垃圾输出,我传递了-o
标志,这只显示我们在文本文件中找到的实际字符串。无论如何,我们只对二进制文件感兴趣,-r
则表示递归grep
webServer
出现在二进制文件QCMAP_Web_CLIENT
和qcmap_web_cgi
中
我们先来看看qcmap_web_cgi
,如果您记得上面的POST请求,qcmap_web_cgi
是所有正在POST的端点。因此,它可能负责管理每个请求的处理方式
打开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
实际上非常有趣。即使单从调用树看,你也可以看到它调用其他打开套接字的函数,并执行sendto
和recv
调用,还至少有一个系统调用。
您可能期望从与Web服务器相邻的二进制文件中出现类似的内容——但请记住,此二进制文件根本不处理HTTP服务器套接字活动。这个二进制文件本身不是Web服务器——它只是实际Web服务器传递HTTP请求的端点。任何套接字活动都完全在做其他事情。让我们的RE旅程更长一些,但也许会发现有趣的东西。
好的,回到二进制文件。正如您在调用树中看到的那样,FUN_000092ec
正在调用FUN_00008f3c
,它正在执行socket
,system
和sendto syscalls
!让我们看一下(用一点手动变量名称清理):
二进制文件bind
到套接字文件/www/qcmap_cgi_webclient_file
,然后再把请求的数据sendto
到套接字文件/www/qcmap_webclient_cgi_file
对我们来说,所有这些意味着我们现在必须扩大我们的搜索范围。由于数据被推出qcmap_web_cgi
,我们需要弄清楚它的去向以及发生了什么。
让我们来看看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
参数,字符串位于查找表中每个条目的开头,直到匹配为止。然后它调用相关的函数。我怀疑它看起来像开发人员实际编写的查找函数——但这就是它在反编译的伪代码中的样子。
刚才有一些逆向的内容让人分析,让我们回到API_webServer_function
,Ghidra
为我们准备了一个非常好的切换语句供我们仔细阅读
提取用户提供的来自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
调用本身是突出显示的
popen
字面上运行系统级命令。它很像system
或exec
。将不受信任的用户输入直接传递给它存在问题,但这正是二进制文件所做的。
如果操作为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
中的函数调用类似于x86_64
,因为参数存储在寄存器中
R0应包含第一个参数,R1表示第二个参数,R2表示第三个参数...等等
我们正在查看一个snprintf
调用,如果要填充一些格式字符串,则应至少使用4个参数。而且uci set ...
命令字符串中的%s
绝对是格式字符串。
应以下列格式调用snprintf
int snprintf(char* s,size_t n,const 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之后,并且不包括在伪代码中。那好吧。至少我们现在肯定知道它在那里。
现在它应该是不言而喻的,但是将language
设置为shell命令将导致我们的shell命令被snprintf
字面地包含在uci set ...
字符串中。当该字符串传递给popen时,该命令将被执行。
我们现在知道伪代码应该是这样的:
因此,我们提供给language
参数的值将替换uci set ...
字符串中的%s
格式字符串,该值存储在acStack224
中,最后popen
就被调用
因此,以下请求将生成telnetd
。Pre-authentication
接着,我们可以登录设备,并准备接下来的“掠夺”
好吧,不完全是。蜂窝调制解调器连接到APN
,APN
就像电信公司提供的大规模局域网。配置APN
不一定要求严格——例如,不实施客户端隔离。在这些情况下,任何非常顽皮的人都可以连接到相同的APN,因为您可以访问蜂窝调制解调器的Web配置界面。任何非常顽皮的访问电信GGSN
的人也可能能够连接到路由器的Web界面——假设路由器不阻止通过蜂窝接口访问。
还有可能存在drive-by
JavaScript跨站点请求伪造攻击。在JavaScript中,很容易枚举路由器所在的位置,查看它是否存在漏洞,并伪造可能执行代码的请求。您可以在我们的旧帖子中看到此类攻击的示例。所以,一个讨厌的页面可以在您的路由器上执行任意代码。除了访问完全不相关但恶意的页面之外,您无需执行任何操作。
这是注入命令的JavaScript,等待500毫秒,并将语言设置为正常:
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不会。