[原]使用PHP安全檢測拓展Taint檢測你的PHP代碼 (附源碼分析)

一.拓展簡介

Taint是鳥哥寫的一個PHP拓展 支持PHP5.2~PHP7.2。拓展啟用后能監(jiān)控某些關(guān)鍵函數(shù)是否直接使用了來源于用戶輸入($_GET,$_POST,$COOKIE)而沒有經(jīng)過特殊處理的字符串。

舉個例子,在你web服務(wù)器的根目錄下創(chuàng)建一個如下的taint.php文件

<?php
// <YOUR_WEB_ROOT/taint.php>
$strA = trim($_GET['test']);
$strB='input a '.sprintf('%s',$strA);
echo $strB;

當(dāng)Taint啟動后,訪問http://host/taint.php?test=dog執(zhí)行該腳本會收到如下的警告

Warning: main() [echo]: Attempt to echo a string that might be tainted in /YOUR_WEB_ROOT/taint.php on line 5
input a dog

這可以幫助你及早潛在的Xss,SQL Inject等攻擊點。

二.拓展搭建

Taint非常輕量級,沒有PHP版本以外的任何依賴,使用常規(guī)方法即可編譯出動態(tài)模塊

$ git clone https://github.com/laruence/taint.git
$ cd ./taint
$ /PHP_PATH/bin/phpize
$ ./configure --with-php-config=/PHP_PATH/bin/php-config
$ make && make install

編輯php.ini文件

$ vim /PHP_INI_PATH/php.ini

在末尾添加以下內(nèi)容

[taint]
extension=taint.so

;taint.enable 表示Taint的開關(guān),默認(rèn)0為關(guān)閉,打開需要顯式配置為1
taint.enable = 1  

;taint.error_level 表示發(fā)現(xiàn)潛在注入問題時拋出錯誤的等級,一般使用默認(rèn)值E_WARNING即可。根據(jù)實際情況也可以選擇為E_NOTICE,E_ERROR等值
taint.error_level = E_WARNING

重啟你的php-fpm或者apache服務(wù),使用瀏覽器訪問上面的taint.php即可看到拓展效果

三.源碼實現(xiàn)

由于這個拓展的文檔和其他資料基本沒有,這里附上關(guān)鍵源碼輔助講解實現(xiàn)機制。

污染標(biāo)記

Taint定義了3個核心宏

#define TAINT_MARK(str)     (GC_FLAGS((str)) |= IS_STR_TAINT_POSSIBLE)//該宏標(biāo)記一個字符串為受污染(后續(xù)使用污染代替Taint)
#define TAINT_POSSIBLE(str) (GC_FLAGS((str)) & IS_STR_TAINT_POSSIBLE)//該宏返回一個字符串是否是受污染的
#define TAINT_CLEAN(str)    (GC_FLAGS((str)) &= ~IS_STR_TAINT_POSSIBLE)//該宏清除污染標(biāo)記

GC_FLAGS()是PHP內(nèi)核宏#define GC_FLAGS(p) (p)->gc.u.v.flags,參數(shù)p類型為zend_value指針

//代表PHP中的一個值
typedef union _zend_value {
    zend_long lval;             /* long value */
    double dval;             /* double value */
    zend_refcounted *counted;
    zend_string *str;
    zend_array *arr;
    zend_object *obj;
    zend_resource *res;
    zend_reference *ref;
    zend_ast_ref *ast;
    zval *zv;
    void *ptr;
    zend_class_entry *ce;
    zend_function *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;

//代表PHP中的一個字符串值
struct _zend_string {
    zend_refcounted_h gc;
    zend_ulong h; /* hash value */
    size_t len;
    char val[1];
};

//zend_value中的成員,存放內(nèi)存回收相關(guān)信息
typedef struct _zend_refcounted_h {
    uint32_t refcount;          /* reference counter 32-bit */
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar type,
                zend_uchar flags, /* used for strings & objects */
                uint16_t gc_info) /* keeps GC root number (or 0) and color */
        } v;
        uint32_t type_info;
    } u;
} zend_refcounted_h;

污染標(biāo)記的原理是借助_zend_string的內(nèi)存回收結(jié)構(gòu)的u.v.flags字段的一個未被使用的標(biāo)記位去記錄字符串是否被污染。
基于該原理,Taint可能會和更新版本的PHP或者借用該標(biāo)記位的其他PHP拓展沖突。

初始化外部字符串污染標(biāo)記

/* {{{ PHP_RINIT_FUNCTION
*/
PHP_RINIT_FUNCTION(taint)
{
    if (SG(sapi_started) || !TAINT_G(enable)) {
        return SUCCESS;
    }

    if (Z_TYPE(PG(http_globals)[TRACK_VARS_POST]) == IS_ARRAY) {
        //php_taint_mark_strings()功能是遞歸遍歷array,對每個字符串調(diào)用TAINT_MARK(),標(biāo)記字符串為受污染的
        php_taint_mark_strings(Z_ARRVAL(PG(http_globals)[TRACK_VARS_POST]));
    }

    if (Z_TYPE(PG(http_globals)[TRACK_VARS_GET]) == IS_ARRAY) {
        php_taint_mark_strings(Z_ARRVAL(PG(http_globals)[TRACK_VARS_GET]));
    }

    if (Z_TYPE(PG(http_globals)[TRACK_VARS_COOKIE]) == IS_ARRAY) {
        php_taint_mark_strings(Z_ARRVAL(PG(http_globals)[TRACK_VARS_COOKIE]));
    }

    return SUCCESS;
}

該方法會在REQUEST_INIT階段調(diào)用,即對于每個WEB請求到來后,對_GET,_POST,$_COOKIE中所有字符串進(jìn)行污染標(biāo)記。

污染擴散

Taint通過在MODULE_INIT階段覆蓋PHP內(nèi)核原生的大量相關(guān)的字符串函數(shù)和opcode的handler來保證污染字符串的有效擴散。新句柄主要都是代理,在底層委托原本的handler,并附加上Taint的一些處理。

sprintf()作為函數(shù)覆蓋的示例:

    //覆蓋原生sprintf()
    php_taint_override_func(f_sprintf, PHP_FN(taint_sprintf), &TAINT_O_FUNC(sprintf));
/* {{{ proto string sprintf(string $format, ...)
*/
PHP_FUNCTION(taint_sprintf) {
    zval *args;
    int i, argc, tainted = 0;
    //PHP參數(shù)解析,后文略
    if (zend_parse_parameters(ZEND_NUM_ARGS(), "+", &args, &argc) == FAILURE) {
        RETURN_FALSE;
    }
    //檢查sprintf()的所有參數(shù),包括模板參數(shù)和綁定參數(shù),是否存在污染字符串
    for (i = 0; i < argc; i++) {
        if (IS_STRING == Z_TYPE(args[i]) && TAINT_POSSIBLE(Z_STR(args[i]))) {
            tainted = 1;
            break;
        }
    }
    //調(diào)用本來sprintf()的句柄;
    TAINT_O_FUNC(sprintf)(INTERNAL_FUNCTION_PARAM_PASSTHRU);
    //根據(jù)參數(shù)污染監(jiān)測的結(jié)果對sprintf()的返回字符串進(jìn)行污染標(biāo)記
    if (tainted && IS_STRING == Z_TYPE_P(return_value) && Z_STRLEN_P(return_value)) {
        TAINT_MARK(Z_STR_P(return_value));
    }
}

ZEND_CONCAT作為函數(shù)覆蓋的示例:

    //覆蓋ZEND_CONCAT (如字符串連接'a'.$str)原本的handler
    zend_set_user_opcode_handler(ZEND_CONCAT, php_taint_concat_handler);
static int php_taint_concat_handler(zend_execute_data *execute_data) /* {{{ */ {
    const zend_op *opline = execute_data->opline;
    zval *op1, *op2, *result;
    taint_free_op free_op1, free_op2;
    int tainted = 0;

    //提取字符串鏈接兩個操作數(shù)以及返回值的zval指針
    op1 = php_taint_get_zval_ptr(execute_data, opline->op1_type, opline->op1, &free_op1, BP_VAR_R, 1);
    op2 = php_taint_get_zval_ptr(execute_data, opline->op2_type, opline->op2, &free_op2, BP_VAR_R, 1);
    result = EX_VAR(opline->result.var);

    //判斷源字符串是存在污染字符串
    if ((op1 && IS_STRING == Z_TYPE_P(op1) && TAINT_POSSIBLE(Z_STR_P(op1)))
            || (op2 && IS_STRING == Z_TYPE_P(op2) && TAINT_POSSIBLE(Z_STR_P(op2)))) {
        tainted = 1;
    }

    //字符串拼接
    concat_function(result, op1, op2);

    //結(jié)果字符串污染標(biāo)記
    if (tainted && IS_STRING == Z_TYPE_P(result) && Z_STRLEN_P(result)) {
        TAINT_MARK(Z_STR_P(result));
    }
    
    //其他opcode常規(guī)操作(操作數(shù)釋放,當(dāng)前opline指針遞增并執(zhí)行后面的opline)
    if ((TAINT_OP1_TYPE(opline) & (IS_VAR|IS_TMP_VAR)) && free_op1) {
        zval_ptr_dtor_nogc(free_op1);
    }

    if ((TAINT_OP2_TYPE(opline) & (IS_VAR|IS_TMP_VAR)) && free_op2) {
        zval_ptr_dtor_nogc(free_op2);
    }

    execute_data->opline++;
    return ZEND_USER_OPCODE_CONTINUE;
} /* }}} */

這就是上文hello world中$strB='input a '.sprintf('%s',$strA);,為何$strB已經(jīng)經(jīng)過修改,卻仍然能夠被識別出是個被污染的字符串的原因。PHP內(nèi)核中的字符串相關(guān)的處理函數(shù)和opcode都被改寫了,保證由污染字符串產(chǎn)生的衍生字符串也都會被標(biāo)記成污染字符串。

類似的被覆蓋函數(shù)有:

  • join();
  • trim();
  • split();
  • rtrim();
  • ltrim();
  • strval();
  • strstr();
  • substr();
  • sprintf();
  • explode();
  • implode();
  • str_pad();
  • vsprintf();
  • str_replace();
  • str_ireplace();
  • strtolower();
  • strtoupper();
  • dirname();
  • basename();
  • pathinfo();

類似的被覆蓋的Opcode有:

  • ZEND_CONCAT
  • ZEND_FAST_CONCAT
  • ZEND_ROPE_END

污染告警/注入監(jiān)控

為了在關(guān)鍵點使用了被污染的字符串時能夠做出告警,除了污染拓展章節(jié)提到的opcode,Taint還覆蓋了大量其余opcode的handler。

一方面覆蓋了以下Opcode在echo,print,include,require,eval,動態(tài)方法調(diào)用中直接使用污染字符串時拋出警告

  • ZEND_ECHO
  • ZEND_EXIT
  • ZEND_INCLUDE_OR_EVAL
  • ZEND_INIT_USER_CALL
  • ZEND_INIT_DYNAMIC_CALL

一方面覆蓋了一下Opcode在函數(shù)調(diào)用前對特定的參數(shù)進(jìn)行污染檢查

  • ZEND_DO_FCALL
  • ZEND_DO_ICALL
  • ZEND_DO_FCALL_BY_NAME

會在以下內(nèi)部函數(shù)/方法的調(diào)用前進(jìn)行taint參數(shù)檢查和錯誤拋出:
注:本文提到的 內(nèi)部函數(shù) 是區(qū)別于使用PHP實現(xiàn)的用戶函數(shù)的函數(shù)。內(nèi)部函數(shù)指使用C語言在PHP內(nèi)核或拓展層面實現(xiàn)的提供給用戶在PHP中調(diào)用方法或函數(shù),如printf()

  • print_r();
  • fopen();
  • unlink();
  • file();
  • readfile();
  • file_get_contents();
  • opendir();
  • printf();
  • vprintf();
  • file_put_contents();
  • fwrite();
  • header();
  • unserialize();
  • mysqli_query();
  • mysqli_prepare();
  • mysql_query();
  • sqlite_query();
  • sqlite_single_query();
  • oci_parse();
  • preg_replace_callback();
  • passthru();
  • system();
  • exec();
  • shell_exec();
  • proc_open();
  • popen();
  • mysqli::query();
  • mysqli::prepare();
  • PDO::query();
  • PDO::prepare();
  • SQLite3::query();
  • SQLite3::prepare();
  • sqlitedatabase::query();
  • sqlitedatabase::singlequery();

提供內(nèi)部函數(shù)

/* {{{ proto bool taint(string $str[, string ...])
*/
PHP_FUNCTION(taint)
{
    zval *args;
    int argc;
    int i;
    //檢查拓展是否啟用
    if (!TAINT_G(enable)) {
        RETURN_TRUE;
    }
    if (zend_parse_parameters(ZEND_NUM_ARGS(), "+", &args, &argc) == FAILURE) {
        return;
    }
   //該方法支持不定參數(shù)
    for (i = 0; i <     ; i++) {
        zval *el = &args[i];
        ZVAL_DEREF(el);
        if (IS_STRING == Z_TYPE_P(el) && Z_STRLEN_P(el) && !TAINT_POSSIBLE(Z_STR_P(el))) {
            /* string might be in shared memory */
            //重建字符串并標(biāo)記新字符串為污染字符串,gc計數(shù)更變,變量賦值
            zend_string *str = zend_string_init(Z_STRVAL_P(el), Z_STRLEN_P(el), 0);
            zend_string_release(Z_STR_P(el));
            TAINT_MARK(str);
            ZVAL_STR(el, str);
        }
    }

    RETURN_TRUE;
}
/* }}} */

/* {{{ proto bool untaint(string $str[, string ...])
*/
PHP_FUNCTION(untaint)
{
    //...
    TAINT_CLEAN()
    //...
}
/* }}} */

/* {{{ proto bool is_tainted(string $str)
*/
PHP_FUNCTION(is_tainted)
{
    //.....
    TAINT_POSSIBLE();
    //...
}

拓展提供了taint(),untaint(),is_tainted()3個函數(shù)作為對TAINT_MARK(),TAINT_POSSIBLE(),TAINT_CLEAN()宏的封裝,以便用戶可以直接在PHP中利用相關(guān)機制對Taint進(jìn)行拓展。

污染標(biāo)記清理

已知有3種方式可以清理字符串上的污染標(biāo)記

一.使用htmlentities(),addslashes(),mysql_escape_string()等轉(zhuǎn)義方法生成了新的字符串。

根據(jù)實現(xiàn),這個說法其實并不嚴(yán)謹(jǐn)。實際上Taint并沒有在以上轉(zhuǎn)義方法上添加特殊處理,不是Taint對轉(zhuǎn)義函數(shù)進(jìn)行了特殊處理,而是因為Taint對轉(zhuǎn)義函數(shù)沒有進(jìn)行處理所以返回的字符串是沒有污染標(biāo)記的。
在污染擴散章節(jié)中沒有提到的字符串處理內(nèi)部函數(shù)如果生成了新的zend_string其實都是沒有污染標(biāo)記的,因此此處我也無法提供一個完整的帶有污染標(biāo)記清理的方法列表。

基于這個原理,可以嘗試使用json_encode()處理一個Taint的字符串,你會發(fā)現(xiàn)雖然json_encode()是一個安全無關(guān)的方法,但是其返回值都是Taint認(rèn)可的干凈的字符串。

另外有些方法在參數(shù)無需處理的情況下是不會生成新的zend_string,此時污染標(biāo)記不會清除,譬如

$str=$_GET['userName'];/
$str2=addslashes($str);

預(yù)期上你會認(rèn)為$str2總是干凈的,實際上并不然。
如果$str原來的值是"aa'a",$srr2是沒有污染標(biāo)記的
如果$str原來的值是"aaa",處理后的字符串$str2還是原來的字符串$str,污染標(biāo)記仍然存在,Taint仍然會對該字符串給出警告。

二.使用Taint拓展提供的內(nèi)部方法untaint(&$str,...)清理標(biāo)記

考慮到方案一的處理原理,手動將字符串標(biāo)記為干凈的untaint()是一個更加實用的方案。你可以在類庫的相關(guān)安全處理方法中添加該方法,標(biāo)記字符串的污染狀態(tài)為干凈的。

三.利用Taint未處理的機制構(gòu)造字符串(不推薦)
function trick($str){
    return $str;
    $result='';
    $strlen=strlen($str);
    for ($i=0;$i<$strlen;$i++){
        $result.=$str[$i]    ;
    }    
    return $result;
}

由于Taint未對ZEND_FETCH_DIM_*幾個Opcode進(jìn)行特殊處理,所以雖然上述函數(shù)的返回值和參數(shù)是同一個字符串,但是返回值永遠(yuǎn)是干凈的。

四.實踐思路

Taint提供了一個很好的思路去監(jiān)控應(yīng)用的安全情況。不像人工攻擊測試需要昂貴的人力成本投入和安全掃描工具需要大量系統(tǒng)資源消耗,他資源消耗小,而且
然而Taint目前能夠處理的問題并不夠多,主要在Sql注入,XSS,命令注入幾個方面,而且Taint目前并沒有對不同類型的污染進(jìn)行區(qū)分,而是共享同一個污染標(biāo)記位,任何一個方法都會同時標(biāo)記或者清理所有的污染標(biāo)記,考慮到這個原因僅僅建議使用Taine作為其他安全手段的補充。

考慮到穩(wěn)定性和性能問題,不建議在生產(chǎn)環(huán)境開啟Taint。
作為安全監(jiān)控拓展,在開發(fā)測試環(huán)境安裝并啟用,根據(jù)警告處理問題字段即可。
對于Taint自帶污染標(biāo)記清理機制不能滿足的地方,手動調(diào)用以下方法即可。

function markSafe(string &$string){
    if(function_exists('untaint')){
        return untaint($string);
    }else{
        return true;
    }
}

五.拓展閱讀

Laruence:《PHP Taint – 一個用來檢測XSS/SQL/Shell注入漏洞的擴展》
<laruence/taint-GitHub Readme File>
<PHP: rfc:taint>

原文作者:bromine
轉(zhuǎn)載請注明出處:http://www.itdecent.cn/p/c6dea66c54f3

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Composer Repositories Composer源 Firegento - Magento模塊Comp...
    零一間閱讀 4,021評論 1 66
  • awesome-php 收集整理一些常用的PHP類庫, 資源以及技巧. 以便在工作中迅速的查找所需... 這個列表...
    guanguans閱讀 4,760評論 0 34
  • Awesome PHP 一個PHP資源列表,內(nèi)容包括:庫、框架、模板、安全、代碼分析、日志、第三方庫、配置工具、W...
    guanguans閱讀 6,129評論 0 47
  • 在《驢得水》這部饒有意味,充滿了諷刺現(xiàn)實黑暗的影片中,我卻單單看到了張一曼——一個自由浪漫主義的女人的獨白。 我要...
    黑羊君閱讀 1,332評論 8 6
  • 2018年3月9日凌晨1點14,距離我辭職那天已經(jīng)過去了8天1個小時14分。 是的,我在2018年2月28日離職了...
    一心小茶客閱讀 366評論 0 0

友情鏈接更多精彩內(nèi)容