[原]使用PHP安全检测拓展Taint检测你的PHP代码 (附源码分析)

一.拓展简介

Taint是鸟哥写的一个PHP拓展 支持PHP5.2~PHP7.2。拓展启用后能监控某些关键函数是否直接使用了来源于用户输入($_GET,$_POST,$COOKIE)而没有经过特殊处理的字符串。

举个例子,在你web服务器的根目录下创建一个如下的taint.php文件

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

当Taint启动后,访问http://host/taint.php?test=dog执行该脚本会收到如下的警告

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版本以外的任何依赖,使用常规方法即可编译出动态模块

$ 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

在末尾添加以下内容

[taint]
extension=taint.so

;taint.enable 表示Taint的开关,默认0为关闭,打开需要显式配置为1
taint.enable = 1  

;taint.error_level 表示发现潜在注入问题时抛出错误的等级,一般使用默认值E_WARNING即可。根据实际情况也可以选择为E_NOTICE,E_ERROR等值
taint.error_level = E_WARNING

重启你的php-fpm或者apache服务,使用浏览器访问上面的taint.php即可看到拓展效果

三.源码实现

由于这个拓展的文档和其他资料基本没有,这里附上关键源码辅助讲解实现机制。

污染标记

Taint定义了3个核心宏

#define TAINT_MARK(str)     (GC_FLAGS((str)) |= IS_STR_TAINT_POSSIBLE)//该宏标记一个字符串为受污染(后续使用污染代替Taint)
#define TAINT_POSSIBLE(str) (GC_FLAGS((str)) & IS_STR_TAINT_POSSIBLE)//该宏返回一个字符串是否是受污染的
#define TAINT_CLEAN(str)    (GC_FLAGS((str)) &= ~IS_STR_TAINT_POSSIBLE)//该宏清除污染标记

GC_FLAGS()是PHP内核宏#define GC_FLAGS(p) (p)->gc.u.v.flags,参数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中的成员,存放内存回收相关信息
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;

污染标记的原理是借助_zend_string的内存回收结构的u.v.flags字段的一个未被使用的标记位去记录字符串是否被污染。
基于该原理,Taint可能会和更新版本的PHP或者借用该标记位的其他PHP拓展冲突。

初始化外部字符串污染标记

/* {{{ 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,对每个字符串调用TAINT_MARK(),标记字符串为受污染的
        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阶段调用,即对于每个WEB请求到来后,对_GET,_POST,$_COOKIE中所有字符串进行污染标记。

污染扩散

Taint通过在MODULE_INIT阶段覆盖PHP内核原生的大量相关的字符串函数和opcode的handler来保证污染字符串的有效扩散。新句柄主要都是代理,在底层委托原本的handler,并附加上Taint的一些处理。

sprintf()作为函数覆盖的示例:

    //覆盖原生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参数解析,后文略
    if (zend_parse_parameters(ZEND_NUM_ARGS(), "+", &args, &argc) == FAILURE) {
        RETURN_FALSE;
    }
    //检查sprintf()的所有参数,包括模板参数和绑定参数,是否存在污染字符串
    for (i = 0; i < argc; i++) {
        if (IS_STRING == Z_TYPE(args[i]) && TAINT_POSSIBLE(Z_STR(args[i]))) {
            tainted = 1;
            break;
        }
    }
    //调用本来sprintf()的句柄;
    TAINT_O_FUNC(sprintf)(INTERNAL_FUNCTION_PARAM_PASSTHRU);
    //根据参数污染监测的结果对sprintf()的返回字符串进行污染标记
    if (tainted && IS_STRING == Z_TYPE_P(return_value) && Z_STRLEN_P(return_value)) {
        TAINT_MARK(Z_STR_P(return_value));
    }
}

ZEND_CONCAT作为函数覆盖的示例:

    //覆盖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;

    //提取字符串链接两个操作数以及返回值的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);

    //结果字符串污染标记
    if (tainted && IS_STRING == Z_TYPE_P(result) && Z_STRLEN_P(result)) {
        TAINT_MARK(Z_STR_P(result));
    }
    
    //其他opcode常规操作(操作数释放,当前opline指针递增并执行后面的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已经经过修改,却仍然能够被识别出是个被污染的字符串的原因。PHP内核中的字符串相关的处理函数和opcode都被改写了,保证由污染字符串产生的衍生字符串也都会被标记成污染字符串。

类似的被覆盖函数有:

  • 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

污染告警/注入监控

为了在关键点使用了被污染的字符串时能够做出告警,除了污染拓展章节提到的opcode,Taint还覆盖了大量其余opcode的handler。

一方面覆盖了以下Opcode在echo,print,include,require,eval,动态方法调用中直接使用污染字符串时抛出警告

  • ZEND_ECHO
  • ZEND_EXIT
  • ZEND_INCLUDE_OR_EVAL
  • ZEND_INIT_USER_CALL
  • ZEND_INIT_DYNAMIC_CALL

一方面覆盖了一下Opcode在函数调用前对特定的参数进行污染检查

  • ZEND_DO_FCALL
  • ZEND_DO_ICALL
  • ZEND_DO_FCALL_BY_NAME

会在以下内部函数/方法的调用前进行taint参数检查和错误抛出:
注:本文提到的 内部函数 是区别于使用PHP实现的用户函数的函数。内部函数指使用C语言在PHP内核或拓展层面实现的提供给用户在PHP中调用方法或函数,如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();

提供内部函数

/* {{{ 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;
    }
   //该方法支持不定参数
    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 */
            //重建字符串并标记新字符串为污染字符串,gc计数更变,变量赋值
            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个函数作为对TAINT_MARK(),TAINT_POSSIBLE(),TAINT_CLEAN()宏的封装,以便用户可以直接在PHP中利用相关机制对Taint进行拓展。

污染标记清理

已知有3种方式可以清理字符串上的污染标记

一.使用htmlentities(),addslashes(),mysql_escape_string()等转义方法生成了新的字符串。

根据实现,这个说法其实并不严谨。实际上Taint并没有在以上转义方法上添加特殊处理,不是Taint对转义函数进行了特殊处理,而是因为Taint对转义函数没有进行处理所以返回的字符串是没有污染标记的。
在污染扩散章节中没有提到的字符串处理内部函数如果生成了新的zend_string其实都是没有污染标记的,因此此处我也无法提供一个完整的带有污染标记清理的方法列表。

基于这个原理,可以尝试使用json_encode()处理一个Taint的字符串,你会发现虽然json_encode()是一个安全无关的方法,但是其返回值都是Taint认可的干净的字符串。

另外有些方法在参数无需处理的情况下是不会生成新的zend_string,此时污染标记不会清除,譬如

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

预期上你会认为$str2总是干净的,实际上并不然。
如果$str原来的值是"aa'a",$srr2是没有污染标记的
如果$str原来的值是"aaa",处理后的字符串$str2还是原来的字符串$str,污染标记仍然存在,Taint仍然会对该字符串给出警告。

二.使用Taint拓展提供的内部方法untaint(&$str,...)清理标记

考虑到方案一的处理原理,手动将字符串标记为干净的untaint()是一个更加实用的方案。你可以在类库的相关安全处理方法中添加该方法,标记字符串的污染状态为干净的。

三.利用Taint未处理的机制构造字符串(不推荐)
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进行特殊处理,所以虽然上述函数的返回值和参数是同一个字符串,但是返回值永远是干净的。

四.实践思路

Taint提供了一个很好的思路去监控应用的安全情况。不像人工攻击测试需要昂贵的人力成本投入和安全扫描工具需要大量系统资源消耗,他资源消耗小,而且
然而Taint目前能够处理的问题并不够多,主要在Sql注入,XSS,命令注入几个方面,而且Taint目前并没有对不同类型的污染进行区分,而是共享同一个污染标记位,任何一个方法都会同时标记或者清理所有的污染标记,考虑到这个原因仅仅建议使用Taine作为其他安全手段的补充。

考虑到稳定性和性能问题,不建议在生产环境开启Taint。
作为安全监控拓展,在开发测试环境安装并启用,根据警告处理问题字段即可。
对于Taint自带污染标记清理机制不能满足的地方,手动调用以下方法即可。

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
转载请注明出处:https://www.jianshu.com/p/c6dea66c54f3

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Composer Repositories Composer源 Firegento - Magento模块Comp...
    零一间阅读 3,956评论 1 66
  • awesome-php 收集整理一些常用的PHP类库, 资源以及技巧. 以便在工作中迅速的查找所需... 这个列表...
    guanguans阅读 4,404评论 0 34
  • Awesome PHP 一个PHP资源列表,内容包括:库、框架、模板、安全、代码分析、日志、第三方库、配置工具、W...
    guanguans阅读 5,745评论 0 47
  • 在《驴得水》这部饶有意味,充满了讽刺现实黑暗的影片中,我却单单看到了张一曼——一个自由浪漫主义的女人的独白。 我要...
    黑羊君阅读 1,179评论 8 6
  • 2018年3月9日凌晨1点14,距离我辞职那天已经过去了8天1个小时14分。 是的,我在2018年2月28日离职了...
    一心小茶客阅读 316评论 0 0