基於PHP擴充套件的WAF實現

wyzsk發表於2020-08-19
作者: lightless · 2015/07/24 11:24

0x00 前言


最近上(ri)網(zhan)上(ri)多了,各種狗啊盾啊看的好心煩,好多蜜汁shell都被殺了,搞的我自己也想開發這麼一個斬馬刀,順便當作畢設來做了。

未知攻,焉知防。我們先來看看shell們都是怎麼躲過查殺的:加密、變形、回撥、隱藏關鍵字……總之就是一句話,讓自己變得沒有特徵,這樣就可以躲過狗和盾的查殺。但是萬變不離其宗,無論怎麼變形,最終都會回到類似這樣的格式:

#!php
$_GET($_POST)

分為執行資料部分($_GET)和傳遞資料的部分($_POST),也就是說,無論怎麼變形,在執行的過程中都會變成這個樣子,接下來還是去執行類似system(), exec(), eval()等等函式,那麼我們就直接定位到這裡,檢測該指令碼是否呼叫危險函式,或是在指令碼呼叫這些函式的時候進行分析,判斷該指令碼是否為非法使用者的shell,會取到很不錯的防禦效果。

0x01 如何獲取先機


既然分析出來了問題的根結,那麼下一步就是要控制住這些危險函式的入口點,即system()等函式的底層入口,就像Windows APIhookzw系列函式一樣,我們也要hooksystem()這些函式。

這裡我們需要用到PHP Extension, 即PHP擴充套件,位於PHP核心zend和PHP應用程式碼之間,很明顯,擴充套件可以監控應用層程式碼的執行細節,也可以呼叫核心提供的ZEND API介面,包括禁用類、禁用函式等等。圖為PHP結合其他必要元件的基本結構。

在ZEND中提供了介面供我們進行這樣的操作,透過zend_set_user_opcode_handler就可以達到目的,根據某大牛所說,只需要hook三個OPCODE即可。

#!c
ZEND_INCLUDE_OR_EVAL        處理eval()、require等
ZEND_DO_FCALL               函式執行system()等
ZEND_DO_FCALL_BY_NAME       變數函式執行 $func = "system";$func();等

ZEND_DO_FCALL為例,在MINIT中將ZEND_DO_FCALL替換掉:

#!c
zend_set_user_opcode_handler(ZEND_DO_FCALL, LIGHT_DO_FCALL);

接著定義自己的處理函式,這裡是LIGHT_DO_FCALL

#!c
static int LIGHT_DO_FCALL(ZEND_OPCODE_HANDLER_ARGS)
{
    /* 檢測是否為合法呼叫 */

    if (/*非法呼叫*/)
    {
        /*攔截掉,不執行。*/
    }
    else
    {
        /*合法呼叫,繼續執行*/
        return ZEND_USER_OPCODE_DISPATHC;
    }   
}

這樣一個骨架就完成了,其餘的內容可以自行新增。這樣每當呼叫system()等函式時,都會經過你自己的LIGHT_DO_FCALL,於是就可以對上層的程式碼執行進行檢查了。

0x02 上面的方法太粗暴


上面的方法固然是好,但是誤判會很多,甚至一個普通的函式呼叫都會直接被ban掉。這樣的WAF放在業務中一定會被噴出翔的。我們需要換個思路。

之前已經說過了,無論如何變形、加密、隱藏關鍵字等,最終都需要呼叫到system()eval()等函式,終究是逃不過這些的。那麼我們把剛才的思路的變一下,不要直接hook那幾個OPCODE,向上走一層。

eval()函式在底層是要呼叫zend_compile_string函式的,那麼我們是不是可以在底層過載掉zend_compile_string函式,或是用自己的函式替換掉呢?答案肯定是可以的,我們只需要重寫自己的zend_compile_string函式即可。

但是對於system來說有些不一樣,上面我們hook掉了ZEND_DO_FCALL,但是太粗暴了,我們這裡可以在function_table中刪除掉system,並用我們自己的函式註冊system函式,這樣就可以做到了實時檢查,如果為合法呼叫,可以呼叫舊的system函式並繼續執行。

很明顯這樣做的話,確實減少了誤殺,但是也增加了風險,因為除了system()之外還有很多可以用於執行命令的函式。除了執行命令,還有遍歷目錄、讀敏感檔案的問題,需要考慮很多的方面,偏僻的函式都要考慮在內,還有PHP SPLDirectoryIterator等等各種方面。

0x03 說了這麼多我們看看實際效果吧


我們來看看hook三個OPCODE的方式會產生什麼樣子的效果。首先我們在MINIT函式中將這三個OPCODE的處理函式換成我們自己的。

#!c
ZEND_MINIT_FUNCTION(lightWAF)
{
    /*
     * hook掉ZEND_DO_FCALL
     * 處理system函式等
     */
    zend_set_user_opcode_handler(ZEND_DO_FCALL, LIGHT_DO_FCALL);

    /*
     * hook掉ZEND_DO_FCALL_BY_NAME
     * 處理$func='system';$func();等型別
     */
    zend_set_user_opcode_handler(ZEND_DO_FCALL_BY_NAME, LIGHT_DO_FCALL_BY_NAME);

    /*
     * hook掉ZEND_INCLUDE_OR_EVAL
     * 處理eval, require等
     */
    zend_set_user_opcode_handler(ZEND_INCLUDE_OR_EVAL, LIGHT_INCLUDE_OR_EVAL);

    return SUCCESS;
}

LIGHT_DO_FCALLLIGHT_DO_FCALL_BY_NAMELIGHT_INCLUDE_OR_EVAL的內容類似,均為判斷指令碼檔案是否在upload目錄中,如果在的話就禁止危險函式的執行,否則放行。這裡我們用LIGHT_DO_FCALL說明一下:

#!c
/* LIGHT_DO_FCALL */
static int LIGHT_DO_FCALL(ZEND_OPCODE_HANDLER_ARGS)
{
    char *filePath;

    filePath = zend_get_executed_filename(TSRMLS_C);
    php_printf("[Debug] filePath: %s\n<br />", filePath);

    if (strstr(filePath, "/upload/"))
    {
        /* 非法呼叫,攔截 */   
        php_printf("[Warning] Execute command via system() etc.\n<br />");
        return ZEND_USER_OPCODE_RETURN;
    }
    else    /* Not Found */
    {
        /* 合法呼叫,放行 */
        return ZEND_USER_OPCODE_DISPATCH;
    }
}

這裡我們看到,一旦檢測到在upload目錄中,就返回ZEND_USER_OPCODE_RETURN,實際作用就是不繼續執行危險函式了,如果不在upload目錄中,就返回ZEND_USER_OPCODE_DISPATCH,實際作用就是繼續執行該函式。

擴充套件編寫完成後,編譯即可得到so檔案,如果對PHP擴充套件開發以及編譯不熟悉的同學,請參考這個擴充套件開發教程的第五章:http://www.walu.cc/phpbook/ ,開發方面的知識超出了本文的內容,所以這裡不再贅述了。

編譯完成後,將得到的lightWAF.so檔案複製到php的擴充套件目錄下,我這裡是/usr/local/php/lib/php/extensions/,根據自己的實際情況進行調整。接下來修改php.ini,讓PHP自動載入我們的擴充套件,在其中加上一行:

extension=/path/to/our/lightWAF.so

我這裡是:

extension=/usr/local/php/lib/php/extensions/lightWAF.so

根據之前so檔案放的位置不同,請自行修改路徑。

最後我們重啟PHP服務,讓設定生效。PHP重啟完成後,在終端中執行php -m,來驗證擴充套件是否載入成功,如果在結果中看到了lightWAF,則說明載入成功。(請無檢視中的錯誤資訊,那個是我本地環境的問題,對lightWAF以及PHP沒什麼影響)

成功載入後,我們看一下實際效果,這裡有一個上傳檔案的頁面,可以上傳任何型別的檔案並將上傳的檔案儲存在upload目錄中。現在模擬一下getshell的過程。

上傳頁面

我們寫個小馬,內容如下:

#!php
<?php
    system($_GET["cmd"]);
?>

進行上傳,並訪問shell,結果如下:

可以看到被成功攔截了,好吧,我們換隻牛逼的馬兒,試試反射型的。

#!php
<?php
    $func = new ReflectionFunction("system");
    echo $func->invokeArgs(array("$_GET[c][/c]"));
?>

繼續上傳並訪問,看看結果:

依然被攔截了,只不過這次觸發的是ZEND_DO_FCALL_BY_NAME這個OPCODE。 接下來我們看看正常的檔案會不會被攔截,在lightWAF目錄下寫入一個test.php,用於模擬正常的業務檔案,內容如下:

#!php
<?php
    system('ls');
?>

訪問一下看看結果:

可以看到ls命令成功的執行了,也就是說我們的正常檔案是不會被攔截的,而只有upload目錄中的檔案會被攔截,這樣做又會引發另一個弊端,倘若攻擊者透過某種方法將shell寫入正常的檔案中,或是與業務結合起來,那麼這種防禦手段就很難生效了。具體如何防禦還要結合其他的特徵進行檢測,並不是沒有辦法了,實際應用中不能只依靠檢測檔案路徑這一條規則,需要結合業務進行部署防禦方案。

另一種方法與這個hook三個OPCODE的方法類似,無非就是麻煩一點,感興趣的同學可以圍觀下面的參考文獻:http://security.tencent.com/index.php/blog/msg/19 ,這裡描述了hook函式的比較詳細的思路。

0x04 不談業務的安全都是耍流氓


總結一下,攔截的方法大概就是上面兩種,但是攔截的依據還沒有決定,如何判斷一個呼叫system()的指令碼是否為webshell呢?如果我們的WAF放到生產環境,啥都不管亂殺,很有可能造成正常的業務無法工作。我自己總結了幾個方法,可能聯合起來使用效果更好一些。

  • 根據目錄判斷 通常情況下,上傳的檔案一般都有專門的目錄進行存放,例如upload/等,正常的業務檔案是不在這其中的,於是我們可以簡單的只對這個目錄進行處理,其他目錄的檔案一律放行。

  • 根據檔案許可權判斷 這需要網站的維護者對網站的許可權進行嚴格的控制,例如,所有的web檔案均是644,且為root:root所有。上傳的檔案為www:www所有,根據許可權的不同進行查殺,對於預設的web檔案進行放行,對屬於非root:root的檔案進行查殺。也可以根據w標誌判斷,透過未知方式getshell的檔案很多是帶有w標誌的,所以可以根據這些特徵進行查殺。

  • 變形檢測 如果發現一個檔案呼叫了assertsystempreg_replace /e等等,但是在原始檔中沒有發現這些關鍵字,還等什麼,這個檔案很大的可能就是shell了。(zend_get_executed_filename可以獲得檔案的標誌以及檔案的路徑,例如出現了assert,說明該指令碼使用了assert執行程式碼。)

  • 黑名單檢測 就像傳統的殺軟一樣,總會有那麼一部分的特徵病毒庫,我們也可以建立一部分的webshell特徵庫,先依靠特徵庫殺掉一部分,再根據其他的情況進行判斷。如果將動態檢測和靜態檢測結合起來,查殺、攔截效果應當都會有顯著的改善。

0x05 所以這WAF有啥用


安全需要和業務結合起來進行,不談業務的安全都是耍流氓。因為是擴充套件級別的WAF,在部署的時候可能需要重新編譯,修改配置檔案等等。批次式部署可能顯得不是那麼方便,而且要根據業務需要進行各種細微的調整。如果僅僅是幾臺伺服器,相信這種WAF還是十分棒的,調整起來也十分方便。

如果能開發出適合批次部署的基於擴充套件的WAF,那麼可能會比較容易普及,畢竟在業務上部署WAF不是一個簡單的事情。

  • 參考資料:

http://security.tencent.com/index.php/blog/msg/57 http://security.tencent.com/index.php/blog/msg/19 http://www.walu.cc/phpbook/ http://www.php-internals.com/book/?p=index http://www.nowamagic.net/librarys/veda/detail/1543

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章