CVE-2018-7034复现(TrendNet路由器登录信息泄露)
前言
该漏洞的利用是通过发送 POST
报文在正常字段 SERVICES=DEVICE.ACCOUNT
后紧跟了一个 AUTHORIZED_GROUP=1
(二者用 %0a
连接),导致 cgibin
文件解析时先识别到了第一个 =
,认为 SERVICES
是键,DEVICE.ACCOUNT%0aAUTHORIZED_GROUP=1
是值。随后又对字符串进行解码,%0a
处理为 \n
,此时内存中字符串为 _POST_SERVICES=DEVICE.ACCOUNT\nAUTHORIZED_GROUP=1
,后续在 cgibin
中认证失败,在此基础上添加了 AUTHORIZED_GROUP=-1
。
之后把整个数据发送给 php
文件进行处理, getcfg.php
文件中if($AUTHORIZED_GROUP < 0)
获取字段值时,首先解析到的是 AUTHORIZED_GROUP=1
从而通过了验证。又因为可以加载 htdocs/webinc/getcfg
目录下的任意文件,最终造成敏感文件的信息泄露
固件下载
由于平常复现漏洞的时候,师傅们一般都把固件上传到了文章中,因此每次复现的时候我都没有考虑过固件下载的问题。如果现在要我进行漏洞挖掘或者复现很新的漏洞,我确实没有把握一定能找到对应厂商发布的固件…😶🌫️,之后再复现漏洞我打算都把找固件的过程也记录一下(萌新确实是菜成这样的…)
本篇文章要复现 TrendNet 的漏洞,根据几次找固件的经验来看,应该在搜索引擎里搜 TrendNet support
(因为也没人告诉过我这一点,全靠为数不多的经验猜的,不保证正确和严谨😥,至少这次是没问题的🥰)
根据 CVE-2018-7034 披露信息
可知 TRENDnet TEW-751DR v1.03B03
、TEW-752DRU v1.03B01
和 TEW733GR v1.03B01
设备存在认证绕过漏洞,通过使用 AUTHORIZED_GROUP=1
值,攻击者可以绕过认证,如通过请求 getcfg.php
进行信息泄露
我这里搜索的型号为 TEW-751DR
,尽管英语不太好,但是点进去看到 Firmware
和 Download
也知道是下载固件了
后续解压固件就不再提了,binwalk
直接解
PHP代码
htdocs/web/getcfg.php
HTTP/1.1 200 OK |
因为 php
我基本也不太懂,不过也能大概猜个意思, $_POST["SERVICES"]
是用户通过 POST
传参可控的,将这个可控的字段拼接到固定的路径里,然后检查这个文件是否存在,如果存在的话就进行加载
$GETCFG_SVC = cut($_POST["SERVICES"], $SERVICE_INDEX, ","); |
/htdocs/webinc/getcfg/
路径下面正好有一个敏感文件是以 .xml.php
结尾,DEVICE.ACCOUNT.xml.php
该文件可以泄露用户名和密码等信息
foreach("/device/account/entry") |
所以能通过验证的话,将 SERVICES
设置为 DEVICE.ACCOUNT
就能泄露路由器的信息。if($AUTHORIZED_GROUP < 0)
这个可以通过伪造一个全局变量 $AUTHORIZED_GROUP
绕过验证。因为传入的数据都是先通过登录验证文件 htdocs/cgibin
的解析后,发送给 php
等文件进行处理,下面通过分析 cgibin
是如何设置 $AUTHORIZED_GROUP
字段,从而寻找一下利用的机会
二进制程序逆向
phpcgi_main函数分析
因为 phpcgi
软链接到了 cgibin
上,这里 *argv
取的就是 phpcgi
,所以调用了 phpcgi_main
函数
sobj_new
创建了一个结构体,用于存放之后解析出来的各个字段,sobj_add_string
函数是将 *(a2+4)
的字符串给添加到刚刚的结构体中,因为 a2
就是 main
函数的 argv
,所以 +4
就取到了第一个命令行参数(必须要给一个命令行参数,否则会进入 if(a1<2)
退出 )
最初的 qemu
用户级仿真的脚本是这样的(后续我也再根据报错和问题不断的调整)
qemu-mipsel-static -E CONTENT_TYPE="application/" -E REQUEST_METHOD="POST" -g 1234 ./phpcgi 123 |
下面是动态调试结构体的情况,可以看到 sobj_add_string
函数执行后确实把第一个命令行参数 123
给添加进去了。执行了 sobj_add_char
后又在 123
的后面添加了一个 \n
接下来是一个循环,a3
是 main
函数的 envp
,那么结合上面的分析不难猜测出这里是把所有的环境变量按照 _SERVER_+环境变量+\n
的形式添加到结构体里
调试界面可以看到确实把环境变量都添加到了这个结构体里…🤔,因为我没有用 chroot
来做调试的隔离环境,貌似把所有环境变量都遍历出来了?(emm,这里是我有意为之,我就是想看一下如果不做隔离环境,后面的分析和调试会不会出现问题)
于是乎,我重新来一次在隔离环境中的调试。我执行了命令 sudo chroot . ./qemu-mipsel-static -E REQUEST_METHOD="POST" -E CONTENT_TYPE="application/" -g 1234 ./phpcgi 123
,但是报错 Error while loading ./phpcgi: No such file or directory
,有点奇怪,于是我直接回到了文件系统的根目录,执行命令 sudo chroot . ./qemu-mipsel-static -E REQUEST_METHOD="POST" -E CONTENT_TYPE="application/" -g 1234 ./usr/sbin/phpcgi 123
。这次一切正常,重新调试
……还是这么多环境变量,那看起来这个地方用不用 chroot
都是一样的。
总之呢,这里就确实是把我设置的环境变量给导到结构体里了
下面这部分代码先判断了 REQUEST_METHOD
的值,HEAD
或 GET
将 sub_405CF8
函数指针赋值给 v11
,POST
则将 sub_405AC0
函数指针赋值给 v11
cgibin_parse_request函数分析
目光直接聚焦到 cgibin_parse_request
函数,它的第一个参数是放的函数指针,第二个参数是结构体。
这个 parse_uri
函数进去简单看了一下,貌似是把 REQUEST_URI
中的 ?
后面部分给存入了结构体里,本来我是没传这个环境变量的,但是我发现不传的话,就会返回 -1
(代码如下)
因此加上这个环境变量,继续重调,此时的启动脚本如下
sudo chroot . ./qemu-mipsel-static -E REQUEST_METHOD="POST" -E CONTENT_TYPE="application/" -E REQUEST_URI="wtf?COOL" -g 1234 ./usr/sbin/phpcgi 123 |
结果走了没几步发现又退出了,发现 CONTENT_TYPE
和 CONTENT_LENGTH
的值也必须存在
继续改改改,现在的脚本是这样
sudo chroot . ./qemu-mipsel-static -E REQUEST_METHOD="POST" -E CONTENT_TYPE="application/" -E CONTENT_LENGTH="26214" -E REQUEST_URI="wtf?COOL" -g 1234 ./usr/sbin/phpcgi 123 |
现在终于能来到这个位置了,想执行到第 38
行的指针,需要通过 strncasecmp
函数的检测
把目光看到 0x433014
的地址处(下图),这里很明显能看出来是三个内存单元组成了一个结构体,第一个成员是字符串常量,也就是 strncasecmp
比较时的 v14
,第二个成员变量是 strncasecmp
函数的第三个参数 v12
,如果通过了检查就执行第三个成员变量(一个函数指针),所以这部分就是在匹配 CONTENT_TYPE
中的类型来执行不同的函数
sub_40445C函数分析
当我进入 sub_40445C
函数后发现有个 strncasecmp
进行判断,通过动态调试我发现这个参数 a4
貌似没控制到(如果你好奇为什么我只进入字符串 application/
对应的函数,其实只需要看一下另外几个函数就知道了,他们的返回值都是 -1
,后续都直接从程序退出了)
感觉这里有一点奇怪,我就执行了 grep -r "application" ./
命令,搜到了下面的内容,这让我猜测 qemu
启动时设置的环境变量 CONTENT_TYPE
是 application/x-www-form-urlencoded
而不是 application/
(WEB
常识没学好……)
继续重调…
sudo chroot . ./qemu-mipsel-static -E REQUEST_METHOD="POST" -E CONTENT_TYPE="application/x-www-form-urlencoded" -E CONTENT_LENGTH="26214" -E REQUEST_URI="wtf?COOL" -g 1234 ./usr/sbin/phpcgi 123 |
先分析一下这里的 select
函数,它的作用是监视多个文件描述符的状态,当有任意一个文件描述符准备就绪(可读或者可写),select
函数就会通知程序,从而避免轮询文件描述符。下面代码的 select
函数中的第一个参数 1
指的是要检查的最大文件描述符加一,因此这里只监视了文件描述符 0
(标准输入流)。第二个参数放的是要监视的可读文件描述符集合, v21.__fds_bits[0] |= 1u
这里把第一个元素(文件描述符为 0
)设置为 1
,这表示监视标准输入流中是否有数据。第三个参数和第四个参数都是 0
,这表示没有要监视可写和异常的文件。第五个参数 timeout
则是该函数最长的等待时间。
v21.__fds_bits[0] |= 1u; |
我最初调试到这个 select
函数的时候,它会卡住五秒,然后返回一个 0
(代表超时了),因为一旦这里返回 0
的话,后面就没办法走到 read
函数了(此处 read
函数读入的数据就是实际环境中 POST
报文的数据)。为了模拟真实的场景,我直接选择用 set $v0=1
逆天改命了一手,这样也确实能走到 read
函数。不过我后来重新思考了一波🤔, select
不就是没检测到标准输入流的数据么,可我压根还没输入数据怎么会检测到呢(我原先都是想着等 read
函数执行时,阻塞住等待输入再写入数据)
于是乎,我在 select
函数执行前就把数据写到了 qemu
这边(下图右部分),而后 select
函数执行后立刻返回了 1
(下图左部分)
这样就可以顺理执行到 read
函数,如下
注意上图中,我在启动 qemu
的时候,给的 CONTENT_LENGTH
值是 100
,在下图中我标注了这段代码的执行流程,可见要想让 sub_40445C
函数返回 1
就得保证读入的数据长度和 CONTENT_LENGTH
一致,否则多跑一轮没有读到数据的话,就会返回 -1
(这里也卡住我了一段时间,好险…差点又放弃了😅)
如果 sub_40445C
返回 -1
的话, cgibin_parse_request
函数也返回 -1
,这样会触发 cgibin_print_http_status(400, "unsupported HTTP request", "unsupported HTTP request")
函数,这明显是报错了🙃
因此 CONTENT_LENGTH
必须要严格和输入的数据长度一样(实际 POST
报文的发送,这个 CONTENT_LENGTH
也不需要我们去刻意控制,它自己就能取到正常的值,这里是本地搭建的环境,要刻意设置一下),cgibin_parse_request
函数返回 1
的话,就可以执行下面的内容
sess_validate
函数在验证用户的身份,如果没有通过验证返回 -1
,这里本身就是未授权获取路由器的管理员用户名和密码,自然就没验证成功。于是乎 AUTHORIZED_GROUP=-1
就被添加到了结构体里
sprintf(v16, "AUTHORIZED_GROUP=%d", v13); |
在此之前,read
读入的数据也被解析到了结构体里,现在的结构体里存放的字符串是这样
后续调用了 xmldbc_ephp
函数,这是把上图中的字符串通过套接字传输,写到了 /var/run/xmldb_sock
文件中,然后其他程序再从该文件中读取数据,而 getcfg.php
文件在读取 $AUTHORIZED_GROUP
值的时候,先读取到了我们伪造的 AUTHORIZED_GROUP=1
继而绕过验证
为了不影响主线,上面并没有提到 _POST_SERVICES=DEVICE.ACCOUNT\nAUTHORIZED_GROUP=1
是何时被写入内存的,它具体靠 sub_403864
函数来实现
sub_403864函数分析
直接上图解,应该不难看懂
在看 sub_40445C
函数的时候,IDA
中 72
行的 sub_403864(&v16, (int)v22, v9);
调用时,我一直纳闷,sub_403864
函数明明直接就是个死循环 while
,只能 return
,如何执行到后面对字符串的解码和写入操作的。完整调试完才发现,不是在 72
行进行的解码和写入操作,而是在 80
行再次调用了 sub_403864
函数时进行的操作…🥲
sobj_unescape_uri函数分析
sobj_unescape_uri
函数看名字就是对 uri
进行解码的,这部分就是在扫描字符串中是否有 %
,如果有 %
的话,取它的下一位和下下一位,将其转换为一个十六进制数字,最后用 sobj_add_char
将这个字符添加回字符串中。比如传入的字符串是 abc%0a123
,那么解码后则为 abc\n123
sub_405AC0函数分析
此处的 v9
就是在 phpcgi_main
函数中的那个函数指针 0x405AC0
,
再看 sub_405AC0
函数,开始的 if(*a2)
是 0
,直接看 else
部分(如下)
首先是往 a1
里面添加了字符串 _POST_
(这个 a1
是之前一直存放解析各种数据的结构体)
接着 sobj_add_string((_DWORD *)a1, v5)
向 a1
里面添加了 POST
内容(也就是 read
读入的数据)中 =
前面的部分(如下)
随后又向 a1
里写入了 =
和 =
后半部分以及 \n
,就不再赘述了。这个函数执行完毕后,就把 POST
报文的内容给按照一定格式解析写到了内存里
攻击效果
获取了用户名和密码,成功登录管理员后台
尾声
做这个漏洞复现的时候,感慨了好几次。好险…差一点又放弃了😅 因为网上的文章大多都千篇一律(除了 winmt 师傅😋他写的还是比较详细的),感觉就是拿 poc
打了一遍,然后过程写的省略就算了,说实话我还感觉有些驴唇不对马嘴…😶🌫️ 所以做的时候基本自己就遇见一个坑就分析一个坑,每个坑都是差点没爬出来(当时的想法是已经写的比其他人详细了,放弃这里的细节研究貌似也没啥)。好在没有得过且过,在每次想放弃的时候又坚持了一下,最终也是把自己有疑问的地方都分析清楚了