2016年12月7日,国外网站exploit-db上爆出一个关于NETGEAR R7000路由器的命令注入漏洞。一时间,各路人马开始忙碌起来。厂商忙于声明和修复,有些人忙于利用,而我们则在复现的过程中寻找漏洞的本质。
2016年12月7日,NETGEAR R7000路由器在exploit-db上被爆出存在远程命令执行漏洞,随着安全研究人员的不断深入,R8000和R6400这两款路由器也被证实有同样的问题。
2016年12月13日,NETGEAR官网上确认漏洞存在,对部分受影响的设备发出了 beta
版的固件补丁。
2016年12月14日,受影响的设备型号增加至11种。
NETGEAR R6250*
NETGEAR R6400*
NETGEAR R6700*
NETGEAR R6900*
NETGEAR R7000*
NETGEAR R7100LG*
NETGEAR R7300DST*
NETGEAR R7900*
NETGEAR R8000*
NETGEAR D6220*
NETGEAR D6400*
通过ZoomEye网络空间搜素引擎我们可以寻找此次受影响的设备的ip
curl -v "https://ip:port/cgi-bin/;echo$IFS"testt" --insecure
此次漏洞分析的灵感源于小伙伴检测中发现的一个问题,让我们走了不少弯路,顺利定位到问题所在。 当我们执行 ps
命令时,出现了如下的结果: 很有意思的是,我们请求的url出现在了以 nobody
用户运行的进程里,这让我们可以根据关键词"sh -c"和"/tmp/cgi_result"去定位关键代码的位置。
在漏洞分析的过程中,另一个小伙伴给出了一个 链接 ,DD-WRT HTTPd的远程命令执行。其中命令执行的部分如下,与本次漏洞很像。于是 HTTPD
就成了我们这次重点关注的对象。
从官网下载NETGEAR R7000的 固件 并通过如下命令解开固件。
binwalk -eM R7000-V1.0.7.2_1.1.93.chk
解开固件之后我们寻找到了相关的文件 /usr/sbin/httpd
,确认与 cgi_result
有关之后,我们对 httpd
进行了逆向。
根据前文的介绍,我们通过搜索字符串,找到了对应的函数 sub_36C34
,F5看反编译的伪码,出现 cgi_result
这一字符串的地方如下:
if ( !strcmp((const char *)&v53, "POST") ) { ... } else if ( !strcmp((const char *)&v53, "OPTIONS") ) { ... } else { v36 = fopen("/tmp/cgi_result", "r"); if ( v36 ) { fclose(v36); system("rm -f /tmp/cgi_result"); if ( acosNvramConfig_match((int)&unk_F0378, (int)"2") ) puts("/r/n##########delete /tmp/cgi_result ############/r"); } v33 = (const char *)&unk_F070F; v34 = (char *)&v45; } sprintf(v34, v33, &v50); system((const char *)&v45); memset(&v49, 0, 0x40u);
可以看到 else
里的逻辑是先判断 /tmp/cgi_result
这一文件是否存在,存在则删除该文件, v33
为 unk_F070F
的地址值, unk_F070F
的值为 /www/cgi-bin/%s > /tmp/cgi_result
, v34
为 v45
的地址值。继续向下,可以看到 v34
对应的值由 v33
和 v50
的值拼接而成。 v50
暂时不清楚。然后再使用 system()
函数执行 v45
对应的值,也就是之前拼接的内容。据此推断,此处应该就是命令执行的触发点了。我们开始向上溯源。 首先,我们先看一下sub_36C34函数的几个参数的内容
int __fastcall sub_36C34(const char *a1, int a2, const char *a3, int a4)
根据其中的代码内容以及写入的文件 /tmp/post_data.txt
可知, a1
为 POST
数据包的 body
部分, a3
可能为 url
, a4
为一个整数,用于判断是否为 POST
的数据包 查看 xrefs graph to
生成的调用图:
我们从main函数开始看起,main函数直接调用了sub_147A0函数
sub_147A0函数中如此调用了sub_100A0函数
sub_100A0(&s1, a105, (int)&a87, dword_F217F8);
其中 s1
为 http
报文内容, a105
为s1的地址值。
在sub_100A0函数中, POST
数据交给 sub_19600
函数处理, GET
数据交给 sub_19B3C
函数处理,还有一些其它情况交给 sub_1A1C0
函数处理,再交给 sub_19B3C
处理。我们跟了调用sub_19600函数的一些关键过程。
int __fastcall sub_100A0(char *a1, const char *a2, int a3, int a4) { char *v9; // r4@5 const char *v10; // r3@6 int v11; // r7@6 bool v12; // zf@6 ... s1 = a1; v9 = (int)s1; ... do { v10 = (unsigned __int8)*v9; v11 = v9++; v12 = v10 == 0; if ( v10 ) v12 = v10 == 32; } while ( !v12 ); //移动到HTTP报文第一个空格的位置 ... LABEL_27: if ( *(_BYTE *)(v11 + 1) == 47 ) ++v9; //移动到HTTP报文中/的位置 .. return (int)sub_19600((const char *)v9, v248, v4); }
可以看到,sub_19600函数将指针移动到HTTP报文中第一个 /
的位置,也就是网站路径的位置,然后通过调用LABEL_27:就可以获取网站的目录。这样获取到的内容就是最初始的内容被传递到了下一个函数 sub_19600。
char *__fastcall sub_19600(const char *a1, const char *a2, int a3) { const char *v3; // r6@1 const char *v4; // r4@1 int v5; // r5@1 char *result; // r0@1 v3 = a2; v4 = a1; v5 = a3; result = strstr(a1, "cgi-bin"); if ( result ) { if ( acosNvramConfig_match((int)"cgi_debug_msg", (int)"1") ) printf("/r/n##########%s(%d)url=%s/r/n", "handle_options", 1293, v4); result = (char *)sub_36C34(v3, v5, v4, 2); } return result; }
sub_19600
函数没有做任何处理,就直接将获取到的路径传递到了 sub_36C34
。
int __fastcall sub_36C34(const char *a1, int a2, const char *a3, int a4) { v6 = a3; v12 = strstr(v6, "cgi-bin"); if(v12) { ... memset(&v50, 0, 0x40u);//给V50分配了64字节的空间,故我们可执行命令的最大长度为64字节 ... } else { if ( v24 ) { if ( v22 ) v25 = 0; else v25 = v23 & 1; if ( v25 ) strcpy((char *)&v50, v20); } else { strncpy((char *)&v50, v20, v22 - 1 - v21); } } ... ... ... if ( !strcmp((const char *)&v53, "POST") ) { ... } else if ( !strcmp((const char *)&v53, "OPTIONS") ) { ... } else { v36 = fopen("/tmp/cgi_result", "r"); if ( v36 ) { fclose(v36); system("rm -f /tmp/cgi_result"); if ( acosNvramConfig_match((int)&unk_F0378, (int)"2") ) puts("/r/n##########delete /tmp/cgi_result ############/r"); } v33 = (const char *)&unk_F070F; v34 = (char *)&v45; } sprintf(v34, v33, &v50); system((const char *)&v45); memset(&v49, 0, 0x40u); }
在 sub_36C34
函数中,会检测 url
中是否含有 cgi-bin
,如果含有,则进行一系列分割操作,并将 cgi-bin
后面的值赋给 v50
,而参数 v50
则正如我们之前分析的那样,和 v33
拼接之后被 system()
函数执行,造成了命令执行漏洞。
之后我们继续跟了 sub_19B3C
和 sub_1A1C0
这两个函数,发现最终也跟 sub_19600
函数殊途同归。不过是因为 HTTP
请求的不同( POST
和 OPTIONS
)而导致不同的函数去处理罢了。
2016年11月13日, NETGEAR
在其官网发布了新的 beta
固件 ,我们对其进行了更进。
httpd
进行了逆向,
xrefs
图如下:
整体函数没有太大变化,让我们来看一下具体细节上的变化。 一路跟下来,在 sub_14958
, sub_100A0
和 sub_197B8
这些函数中都没有看到对 url
进行处理,在 sub_36EB4
中我们发现官方对其进行了过滤。
int __fastcall sub_36EB4(const char *a1, int a2, const char *a3, int a4) { const char *v6; // r4@1 ... v6 = a3; if ( !strchr(a3, 59) && !strchr(v6, 96) && !strchr(v6, 36) && !strstr(v6, "..") ) //ascii对照表:59=>; 96=>` 36=>$ { ... } }
我们可以看到,官方的beta固件过滤了 ; / $
和 ..
,但是我们依旧可能可以绕过这些过滤执行命令,例如 ||
(未测试)。
至于官方后续的更新,我们会继续更进。 如有错误,欢迎指正:)
下图为ZoomEye网络空间搜素引擎上最早曝光的受影响设备R7000,R8000和R6400的全球分布情况。
事实上,还有很多内网的设备我们无法探测,对于这些内网设备,通过 ssrf
攻击仍然可以威胁到内网的安全。这里提供一个以往的案例供大家参考: http://www.2cto.com/article/201311/254364.html 。由于本次漏洞可以执行任意命令,故威胁远比案例中修改 dns
要大,希望可以引起大家的重视。
目前官方仅推出了beta版的补丁,可以根据官网的提示刷新固件 由于新版本beta固件可能还存在一定的安全问题,我们仍然建议关闭路由器远程管理页面。 对比之前受影响的设备,如果不能进入管理界面,也可以通过下列 url
关闭。
http(s)://ip:port/cgi-bin/;killall$IFS’httpd’
1. https://www.seebug.org/vuldb/ssvid-92571
2. https://www.exploit-db.com/exploits/40889/
3. http://kb.netgear.com/000036386/CVE-2016-582384
4. http://www.kb.cert.org/vuls/id/582384
5. https://github.com/rapid7/metasploit-framework/issues/7698
6. http://www.freebuf.com/news/122596.html