接下来的文章都会实战复现一些关于路由器的 web /二进制漏洞,可能会写的比较细,希望能给大家带来启发。
本文在复现 DIR-805L 任意文件读取漏洞时,将会比较详细的分析一下用于 cgi 处理的 phpcgi_main
函数其中的一些功能。在逆向 cgibin
二进制文件时,常常会遇到一些用于解析 http 请求的函数,在分析时经常对这些函数的用法不太清楚。总是云里雾里的,所以这里对这些函数做一个比较细致的总结,希望对同样在学这块的朋友们一点启发吧,也算是达到抛砖引玉的目的吧。
去 dlink 的官网下载固件:
ftp://ftp2.dlink.com/PRODUCTS/DIR-850L/REVA/DIR-850L_REVA_FIRMWARE_1.14.B07_WW.ZIP
使用 firmadyne 来模拟固件:
./sources/extractor/extractor.py -b DIR-850L -sql 127.0.0.1 -np -nk "DIR-850L_REVA_FIRMWARE_1.14.B07_WW.ZIP" images
./scripts/getArch.sh ./images/8.tar.gz
./scripts/tar2db.py -i 8 -f ./images/8.tar.gz
sudo ./scripts/makeImage.sh 8
./scripts/inferNetwork.sh 8
./scratch/8/run.sh
192.168.0.1
运行固件之后在本地连上远程的 ssh 隧道,代理到本地的 9000 端口:
ssh [email protected] -D 9000
sockets 代理 127.0.0.1:9000
,这里我们就能在本地访问到 vps 上模拟的固件
访问 192.168.0.1
即可访问到路由器的登录界面。
先给相应的 payload
:
post_uri: http://192.168.0.1/getcfg.php
data: SERVICES=DEVICE.ACCOUNT&attack=ture%0aAUTHORIZED_GROUP=1
在 /htdocs/web/getcfg.php
文件中,有一处文件读取的操作,通过读取 POST 传入的参数 SERVICES
的值,最后拼接到 $file
变量中,调用 dophp("load", $file)
函数进行文件读取。
显然这里的 SERVICES
变量的值可控,为了达到读取文件的目的,这里还需要绕过前面的一个 if 判断
if(is_power_user() == 1)
......
function is_power_user(){
if($_GLOBALS["AUTHORIZED_GROUP"] == "")
{
return 0;
}
if($_GLOBALS["AUTHORIZED_GROUP"] < 0)
{
return 0;
}
return 1;
}
这个函数在前面就定义了,通过全局变量 AUTHORIZED_GROUP
来判断返回值。这里需要满足 AUTHORIZED_GROUP
的值大于 0 才表示认证成功。那么这里全局变量 AUTHORIZED_GROUP
值是从哪里获取的呢?这里我们就需要在 cgibin
这个二进制文件中找到答案。
首先要明确 cgibin
这个文件是起到解析处理脚本语言的作用,当前 webserver 运行的是 php 脚本,那么这个二进制文件中重点就是处理 php 语言的部分,也就是 phpcgi
。
CGI的英文是(COMMON GATEWAY INTERFACE)公共网关接口,它的作用就是帮助服务器与语言通信。
所以在 IDA 中我们需要找到处理 phpcgi
的相关函数。在 0x00406528 处找到这个函数
下面开始详细分析这个函数其中的一些细节。
首先,第一步函数调用了 sobj_new、sobj_add_string、sobj_add_char
函数来初始化字符串,为后面对解析 POST 变量的处理做一个准备。
int *malloc_addr = malloc(0x18)
heap + 0 = malloc_addr
heap + 4 = malloc_addr
heap + 8 = 0
heap + C = 0
heap + 10 = 0
heap + 14 = 0
接下来通过 getenv
函数来获取请求的方法和请求的 uri。
这里是 POST 请求,所以会走到 0x40668C
这个分支
getenv
是个很重要的函数,这个函数用来获取客户端通过 http 请求的一些请求参数,例如 uri、content-length、请求方法等。char *getenv(const char *name)
当前函数返回值为指向查找的 name
的值的指针。
getenv 函数的返回值存储在一个全局二维数组里,当你再次使用 getenv 函数时不用担心会覆盖上次的调用结果。
接着调用 cgibin_parse_request
这个函数,函数主要功能是进一步处理 http 请求。
cgibin_parse_request(sub_406220,malloc_addr,8)
这个函数的第一个参数 sub_406220 函数意思是拼接 POST 请求的参数名和参数值。
接着 cgibin_parse_request
函数里调用 parse_uri
完后,将返回值存储到 &(heap + 8) 处
往下就跳转到了 loc_406760
这个分支,sobj_get_string
函数从堆空间中获取前一步拼接的参数名和参数值,主要是判断 POST 请求中是否存在 AUTHORIZED_GROUP
这个参数
继续往下,这里的开头先判断访问的绝对路径中是否包含 htdocs/mydlink
路径,再经过 strcasecmp
函数的比较之后,跳转到 sess_validate
函数来执行
在这里,sess_validate
函数作用是验证解析 AUTHORIZED_GROUP
这个参数的值,并通过 sprintf
函数后作为全局变量的形式格式化到栈上。即:
sprintf($sp+0xB0-0x90,"AUTHORIZED_GROUP=%d",sess_validate())
之后再调用 sobj_add_string
和 sobj_add_char
函数来进行字符串的拼接。
sobj_add_string
函数将 sprintf
格式化到栈上的数据作为指针,存储到 sobj_new
开辟的堆空间里。
sobj_add_string(&heap,$sp+0xB0-0x90)
这里需要在 POST 数据包中加上 %0a
的原因是在调用 sobj_add_char
函数时,会用 \n 来分隔参数
sobj_add_char(heap,0x10)
因此这里的 payload
为:
SERVICES=DEVICE.ACCOUNT&attack=%0aAUTHORIZED_GROUP=1
所以只要将 %0aAUTHORIZED_GROUP=1
伪造作为任意一个参数名的参数值就行了。
因此在 cgibin
处理完 AUTHORIZED_GROUP
参数后,就将他作为全局变量存储在 php 当中。这样就绕过了 is_power_user
函数的判断,执行下面的任意文件读取操作。
这里直接使用 POST 方法提交上面的 payload 来进行调试,观察一下参数的变化。
这里其实还有一处的任意文件读取,漏洞位于 htdocs/webinc/fatlady.php
中,代码如下:
$service
参数未经过滤就直接拼接到 $target
变量中,导致这里可以通过目录穿越读取到其他目录下的 .php 后缀的文件,例如存放 admin 用户信息的 DEVICE.ACCOUNT.xml.php 文件。
<?xml version "1.0" encoding "utf-8"><postxml><module><service>../../../htdocs/webinc/getcfg/DEVICE.ACCOUNT.xml</service></module></postxml>
这里只要满足 POST 数据包中 xml 形式的 postxml
即可,$service
变量即可控,同样可以达到任意文件读取的效果。
Content-Type
的值要为 text/xml
这里复现一下第一种任意文件读取的效果,我们的目的是读取在 /htdocs/webinc/getcfg
目录下的 DEVICE.ACCOUNT.xml.php
文件。
SERVICES
的值可控,那么这里就可以传入 SERVICES
的值为 /htdocs/webinc/getcfg
文件夹下后缀为 .xml.php
的文件名,达到未授权读取配置文件的目的。
使用 Burp 构造数据包发送即可看到漏洞效果。
Content-Type
需要为 application/x-www-form-urlencoded
,发送的 POST 数据才能正常被服务器解析。DIR-850L 任意文件读取复漏洞现重点还是要仔细分析 phpcgi_main
这个函数的在处理 http 请求时的相关逻辑。
这里在读取到 DEVICE.ACCOUNT
配置文件,得到 admin 的密码之后,可以进一步利用一处命令执行来 getshell,具体的后面的文章中会有具体的分析。