<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    【技術分享】從內核層面分析部分PHP函數

    上官雨寶2022-07-27 08:23:59

    前言

    這里根據紅日安全PHP-Audit-Labs對一些函數缺陷的分析,從PHP內核層面來分析一些函數的可利用的地方,標題所說的函數缺陷并不一定是函數本身的缺陷,也可能是函數在使用過程中存在某些問題,造成了漏洞,以下是對部分函數的分析

    [+]in_array/array_search函數缺陷

    下面請看代碼

    <?php$whiteList = range(1,24);//$id = 233;//$id = '1';$id = "1' or '1'='1";if(!in_array($id,$whiteList)){    die("only 1 to 24");
    }else{
        $query = "select * from user where id = '". $id . "';";    echo $query;
    }
    

    三個$id分別對應die、輸出語句、輸出語句,這就是因為in_array函數的第三個參數導致的,先貼出函數原型:

    in_array :(PHP 4, PHP 5, PHP 7)
    功能 :檢查數組中是否存在某個值
    定義 :bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] )
    在 $haystack 中搜索 $needle ,如果第三個參數 $strict 的值為 TRUE ,則 in_array() 函數會進行強檢查,檢查 $needle 的類型是否和 $haystack 中的相同。如果找到 $haystack ,則返回 TRUE,否則返回 FALSE。

    通過$id='1'也可以很明顯的知道,在這里之所以能通過in_array肯定是僅是進行了弱類型的比較,因為此處$whiteList的鍵值的int型,如果進行強比較是無法通過的,下面對該函數進行分析:

    ext/standard/array.c/* {{{ proto bool in_array(mixed needle, array haystack [, bool strict])
       Checks if the given value exists in the array */PHP_FUNCTION(in_array)
    {
        php_search_array(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0);
    }/* }}} *//* {{{ proto mixed array_search(mixed needle, array haystack [, bool strict])
       Searches the array for a given value and returns the corresponding key if successful */PHP_FUNCTION(array_search)
    {
        php_search_array(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1);
    }/* }}} */
    

    其實可以發現array_searchin_array的內部實現一樣,跟進:

    /* void php_search_array(INTERNAL_FUNCTION_PARAMETERS, int behavior)
     * 0 = return boolean
     * 1 = return key
     */static inline void php_search_array(INTERNAL_FUNCTION_PARAMETERS, int behavior) /* {{{ */{
        zval *value,                /* value to check for */
             *array,                /* array to check in */
             *entry;                /* pointer to array entry */
        zend_ulong num_idx;
        zend_string *str_idx;
        zend_bool strict = 0;        /* strict comparison or not */
        /*參數定義,需要2-3個參數,前兩個參數是必須的,后面參數是可選的,而可選參數默認是0*/
        ZEND_PARSE_PARAMETERS_START(2, 3)
            Z_PARAM_ZVAL(value)
            Z_PARAM_ARRAY(array)
            Z_PARAM_OPTIONAL
            Z_PARAM_BOOL(strict)
        ZEND_PARSE_PARAMETERS_END();    if (strict) {
            ...
        } else {        if (Z_TYPE_P(value) == IS_LONG) {         // ZEND_HASH_FOREACH_KEY_VAL(hashtable, 數值索引, 字符串索引, 值)
                ZEND_HASH_FOREACH_KEY_VAL_IND(Z_ARRVAL_P(array), num_idx, str_idx, entry) {                if (fast_equal_check_long(value, entry)) {                    if (behavior == 0) {
                            RETURN_TRUE;
                        } else {                        if (str_idx) {
                                RETVAL_STR_COPY(str_idx);
                            } else {
                                RETVAL_LONG(num_idx);
                            }                        return;
                        }
                    }
                } ZEND_HASH_FOREACH_END();
            } else if (Z_TYPE_P(value) == IS_STRING) {
                ZEND_HASH_FOREACH_KEY_VAL_IND(Z_ARRVAL_P(array), num_idx, str_idx, entry) {                if (fast_equal_check_string(value, entry)) {                    if (behavior == 0) {
                            RETURN_TRUE;
                        } else {                        if (str_idx) {
                                RETVAL_STR_COPY(str_idx);
                            } else {
                                RETVAL_LONG(num_idx);
                            }                        return;
                        }
                    }
                } ZEND_HASH_FOREACH_END();
            } else {
                ZEND_HASH_FOREACH_KEY_VAL_IND(Z_ARRVAL_P(array), num_idx, str_idx, entry) {                if (fast_equal_check_function(value, entry)) {                    if (behavior == 0) {
                            RETURN_TRUE;
                        } else {                        if (str_idx) {
                                RETVAL_STR_COPY(str_idx);
                            } else {
                                RETVAL_LONG(num_idx);
                            }                        return;
                        }
                    }
                } ZEND_HASH_FOREACH_END();
             }
        }
        RETURN_FALSE;
    }
    

    由于使用默認strict,因此這里進行省略,默認可選參數下會進入后面的else分支,當我們傳入的value是字符型時進入else if (Z_TYPE_P(value) == IS_STRING) {中,這里先獲得數組的值,然后進入關鍵函數fast_equal_check_string中,可以看到當是默認strict時都是用該函數進行判斷,跟進:

    static zend_always_inline int fast_equal_check_string(zval *op1, zval *op2)
    {
        zval result;    if (EXPECTED(Z_TYPE_P(op2) == IS_STRING)) {        return zend_fast_equal_strings(Z_STR_P(op1), Z_STR_P(op2));
        }    // 參數1:執行結果
        // 參數2:php輸入的查找項: '1'
        // 參數3:php數組當前項
        compare_function(&result, op1, op2);    return Z_LVAL(result) == 0;
    }
    

    這里先了解一下PHP源碼所定義的8中數據類型:

    這里op2是整形,因此會直接使用compare_function進行比較,該函數實在太長,主要是現根據兩個參數的數據類型來進行邏輯處理,這里op1 = IS_STRING,op2 = IS_LONG,但是IS_LONG跟IS_STRING的情況不存在,就進入轉換類型的分支,所以這里直接將對應的處理部分貼下:

    // Zend/zend_types.h源碼定義了如下,這里在比較時用得上,因此一起列出#define IS_UNDEF                    0#define IS_NULL                     1#define IS_FALSE                    2#define IS_TRUE                     3#define IS_LONG                     4           // op2#define IS_DOUBLE                   5#define IS_STRING                   6           // op1#define IS_ARRAY                    7#define IS_OBJECT                   8#define IS_RESOURCE                 9#define IS_REFERENCE                10ZEND_API int ZEND_FASTCALL compare_function(zval *result, zval *op1, zval *op2) /* {{{ */{    int ret;    int converted = 0;  //注意是0
        zval op1_copy, op2_copy;
        zval *op_free, tmp_free;    if (!converted) {    if (Z_TYPE_P(op1) < IS_TRUE) {
            ZVAL_LONG(result, zval_is_true(op2) ? -1 : 0);                return SUCCESS;
        } else if (Z_TYPE_P(op1) == IS_TRUE) {
            ZVAL_LONG(result, zval_is_true(op2) ? 0 : 1);                return SUCCESS;
        } else if (Z_TYPE_P(op2) < IS_TRUE) {
            ZVAL_LONG(result, zval_is_true(op1) ? 1 : 0);                return SUCCESS;
        } else if (Z_TYPE_P(op2) == IS_TRUE) {
                ZVAL_LONG(result, zval_is_true(op1) ? 0 : -1);                return SUCCESS;
        } else {    //前面都沒有匹配成功,直接到該else分支
            op1 = zendi_convert_scalar_to_number(op1, &op1_copy, result, 1);
            op2 = zendi_convert_scalar_to_number(op2, &op2_copy, result, 1);        if (EG(exception)) {                if (result != op1) {
                        ZVAL_UNDEF(result);
                    }            return FAILURE;
            }
            converted = 1;
        }
    

    if(!converted)判斷了左右操作數還沒有轉換為number時候的處理,接著調用zendi_convert_scalar_to_number直接將其轉換為number.因此這部分代碼當操作數不是數字數據類型時,它們將被轉換為數字,然后使得轉換標志converted = 1,不妨跟蹤下這個宏函數:

    #define zendi_convert_scalar_to_number(op, holder, result, silent) \
        ((Z_TYPE_P(op) == IS_LONG || Z_TYPE_P(op) == IS_DOUBLE) ? (op) : \
            (((op) == result) ? (_convert_scalar_to_number((op), silent, 1), (op)) : \
                (silent ? _zendi_convert_scalar_to_number((op), holder) : \
                    _zendi_convert_scalar_to_number_noisy((op), holder))))
    

    這里op2是IS_LONG,因此不做處理保留,而op1是字符串,所以_zendi_convert_scalar_to_number_noisy,這里放出最后的處理:

    static zend_always_inline zval* _zendi_convert_scalar_to_number_ex(zval *op, zval *holder, zend_bool silent) /* {{{ */{    switch (Z_TYPE_P(op)) {        case IS_NULL:        case IS_FALSE:
                ZVAL_LONG(holder, 0);            return holder;        case IS_TRUE:
                ZVAL_LONG(holder, 1);            return holder;        case IS_STRING:            if ((Z_TYPE_INFO_P(holder) = is_numeric_string(Z_STRVAL_P(op), Z_STRLEN_P(op), &Z_LVAL_P(holder), &Z_DVAL_P(holder), silent ? 1 : -1)) == 0) {
                    ZVAL_LONG(holder, 0);                if (!silent) {
                        zend_error(E_WARNING, "A non-numeric value encountered");
                    }
                }            return holder;        case IS_RESOURCE:
                ZVAL_LONG(holder, Z_RES_HANDLE_P(op));            return holder;        case IS_OBJECT:
                convert_object_to_type(op, holder, _IS_NUMBER, convert_scalar_to_number);            if (UNEXPECTED(EG(exception)) ||
                    UNEXPECTED(Z_TYPE_P(holder) != IS_LONG && Z_TYPE_P(holder) != IS_DOUBLE)) {
                    ZVAL_LONG(holder, 1);
                }            return holder;        case IS_LONG:        case IS_DOUBLE:        default:            return op;
        }
    }
    

    這里終于涉及到op=IS_STRING的處理,可以看到會調用is_numeric_string,并且在調用該函數時silent=0,因此調用is_numeric_string傳入的最后一個參數是-1,也就是dval

    這里就是字符轉數字的關鍵步驟了,將字符串字符一個一個的處理,如果當前字符是數字就進行tmp_lval*10 + (*ptr) - '0'運算,直到遇到第一個不是數字的字符,并且最終會return 0

    回到if ((Z_TYPE_INFO_P(holder) = is_numeric_string(Z_STRVAL_P(op), Z_STRLEN_P(op), &Z_LVAL_P(holder), &Z_DVAL_P(holder), silent ? 1 : -1)) == 0)因此Z_TYPE_INFO_P(holder) = 1再通過ZVAL_LONG(holder,0)將其賦值為0后,又賦值給result,因此回到快檢測函數:

    整個流程分析完成,也就是in_array()如果第三個參數為0或者默認時,僅是對數組中的值和查詢的值進行一個弱類型比較

    前文也看到array_searchin_array在底層實現一樣,因此array_search也存在該缺陷:

    <?php$arr = array("true" => 1);
    var_dump(array_search('1Crispr',$arr));//Output: string(4) "true"
    

    [+]filter_var/parse_url函數缺陷

    <?phpfunction is_valid_url($url){    if(filter_var($url,FILTER_VALIDATE_URL)){         if (preg_match('/data:\/\//i', $url)) {              return false;
               }        return true;
        }    return false;
    }//$url = "data://baidu.com/plain;base64,d2hvYW1p";if(is_valid_url($url)){
        $r = parse_url($url);    if(preg_match('/baidu\.com$/',$r['host'])){
            $code = file_get_contents($url);        echo $code;
        }
    }
    

    上面這個例子中可以知道,如果沒有對data協議進行過濾,我們可以使用data協議來進行繞過,無論怎么繞過,第一層繞過的始終是filter_var($url,FILTER_VALIDATE_URL)對這個函數進行分析,先將其函數原型貼出:

    filter_var :(PHP 5 >= 5.2.0, PHP 7)
    功能 :使用特定的過濾器過濾一個變量
    定義 :mixed filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] )

    FILTER_VALIDATE_URL是一種過濾器,用來判斷是否是一個合法的url,不過該判斷是一個非常弱的判斷,可以從下面的例子中發現:

    <?php//$url = "javascript://123";//$url = "data://123";//$url = "ftp://";//$url = "php://123"http://$url = "compress.zlib://data:123";//$url = "xx://xxx";$url = "xx://xx;google.com";
    var_dump(filter_var($url,FILTER_VALIDATE_URL));
    

    這些都能夠通過filter_var,既然如此看似不合法的url都能通過,這里還是從源碼進行分析:

    PHP_FUNCTION(filter_var)
    {
        zend_long filter = FILTER_DEFAULT;
        zval *filter_args = NULL, *data;    if (zend_parse_parameters(ZEND_NUM_ARGS(), "z|lz", &data, &filter, &filter_args) == FAILURE) {        return;
        }    if (!PHP_FILTER_ID_EXISTS(filter)) {
            RETURN_FALSE;
        }
        ZVAL_DUP(return_value, data);
        php_filter_call(return_value, filter, filter_args, 1, FILTER_REQUIRE_SCALAR);
    }
    

    前面是對參數進行處理,這里直接看到

    php_filter_call,其調用情況:

    static void php_filter_call(zval *filtered, zend_long filter, zval *filter_args, const int copy, zend_long filter_flags) /* {{{ */{       ...
        php_zval_filter(filtered, filter, filter_flags, options,    charset, copy);
        ...
    }
    static void php_zval_filter(zval *value, zend_long filter, zend_long flags, zval *options, char* charset, zend_bool copy) /* {{{ */{
        filter_list_entry  filter_func;
        filter_func = php_find_filter(filter);    if (!filter_func.id) {        /* Find default filter */
            filter_func = php_find_filter(FILTER_DEFAULT);
        }    /* #49274, fatal error with object without a toString method
          Fails nicely instead of getting a recovarable fatal error. */
        if (Z_TYPE_P(value) == IS_OBJECT) {
            zend_class_entry *ce;
            ce = Z_OBJCE_P(value);        if (!ce->__tostring) {
                zval_ptr_dtor(value);            /* #67167: doesn't return null on failure for objects */
                if (flags & FILTER_NULL_ON_FAILURE) {
                    ZVAL_NULL(value);
                } else {
                    ZVAL_FALSE(value);
                }            goto handle_default;
            }
        }    /* Here be strings */
        convert_to_string(value);
        filter_func.function(value, flags, options, charset);
    handle_default:    if (options && Z_TYPE_P(options) == IS_ARRAY &&
            ((flags & FILTER_NULL_ON_FAILURE && Z_TYPE_P(value) == IS_NULL) ||
            (!(flags & FILTER_NULL_ON_FAILURE) && Z_TYPE_P(value) == IS_FALSE))) {
            zval *tmp;        if ((tmp = zend_hash_str_find(Z_ARRVAL_P(options), "default", sizeof("default") - 1)) != NULL) {
                ZVAL_COPY(value, tmp);
            }
        }
    }/* }}} */
    

    根據filter來尋找過濾的函數,將value轉化成字符串后調用尋找到的過濾函數來進行處理,URL過濾器對應的函數:

    //ext/filter/logical_filters.cvoid php_filter_validate_url(PHP_INPUT_FILTER_PARAM_DECL) /* {{{ */{
        php_url *url;
        size_t old_len = Z_STRLEN_P(value);
        ...    /* Use parse_url - if it returns false, we return NULL */
        url = php_url_parse_ex(Z_STRVAL_P(value), Z_STRLEN_P(value));    if (url == NULL) {
            RETURN_VALIDATION_FAILED
        }    if (url->scheme != NULL &&
            (zend_string_equals_literal_ci(url->scheme, "http") || zend_string_equals_literal_ci(url->scheme, "https"))) {
            char *e, *s, *t;
            size_t l;        if (url->host == NULL) {            goto bad_url;
            }
            s = ZSTR_VAL(url->host);
            l = ZSTR_LEN(url->host);
            e = s + l;
            t = e - 1;        /* An IPv6 enclosed by square brackets is a valid hostname */
            if (*s == '[' && *t == ']' && _php_filter_validate_ipv6((s + 1), l - 2)) {
                php_url_free(url);            return;
            }        // Validate domain
            if (!_php_filter_validate_domain(ZSTR_VAL(url->host), l, FILTER_FLAG_HOSTNAME)) {
                php_url_free(url);
                RETURN_VALIDATION_FAILED
            }
        }    if (
            url->scheme == NULL ||        /* some schemas allow the host to be empty */
            (url->host == NULL && (strcmp(ZSTR_VAL(url->scheme), "mailto") && strcmp(ZSTR_VAL(url->scheme), "news") && strcmp(ZSTR_VAL(url->scheme), "file"))) ||
            ((flags & FILTER_FLAG_PATH_REQUIRED) && url->path == NULL) || ((flags & FILTER_FLAG_QUERY_REQUIRED) && url->query == NULL)
        ) {
    bad_url:
            php_url_free(url);
            RETURN_VALIDATION_FAILED
        }
        php_url_free(url);
    }
    

    這里還對Ipv6地址進行了判斷,以及后面對域名的判斷,我們重點還是看php_url_parse_ex函數:

    if ((e = memchr(s, ':', length)) && e != s) {        /* validate scheme */
            p = s;        while (p < e) {            /* scheme = 1*[ lowalpha | digit | "+" | "-" | "." ] */
                if (!isalpha(*p) && !isdigit(*p) && *p != '+' && *p != '.' && *p != '-') {                if (e + 1 < ue && e < s + strcspn(s, "?#")) {                    goto parse_port;
                    } else if (s + 1 < ue && *s == '/' && *(s + 1) == '/') { /* relative-scheme URL */
                        s += 2;
                        e = 0;                    goto parse_host;
                    } else {                    goto just_path;
                    }
                }
                p++;
            }        if (e + 1 == ue) { /* only scheme is available */
                ret->scheme = estrndup(s, (e - s));
                php_replace_controlchars_ex(ret->scheme, (e - s));            return ret;
            }        /*
             * certain schemas like mailto: and zlib: may not have any / after them
             * this check ensures we support those.
             */
            if (*(e+1) != '/') {            /* check if the data we get is a port this allows us to
                 * correctly parse things like a.com:80
                 */
                p = e + 1;            while (p < ue && isdigit(*p)) {
                    p++;
                }            if ((p == ue || *p == '/') && (p - e) < 7) {                goto parse_port;
                }
                ret->scheme = estrndup(s, (e-s));
                php_replace_controlchars_ex(ret->scheme, (e - s));
                s = e + 1;            goto just_path;
            } else {
                ret->scheme = estrndup(s, (e-s));
                php_replace_controlchars_ex(ret->scheme, (e - s));            if (e + 2 < ue && *(e + 2) == '/') {
                    s = e + 3;                if (!strncasecmp("file", ret->scheme, sizeof("file"))) {                    if (e + 3 < ue && *(e + 3) == '/') {                        /* support windows drive letters as in:
                               file:///c:/somedir/file.txt
                            */
                            if (e + 5 < ue && *(e + 5) == ':') {
                                s = e + 4;
                            }                        goto just_path;
                        }
                    }
                } else {
                    s = e + 1;                goto just_path;
                }
            }
        } else if (e) { /* no scheme; starts with colon: look for port */
    

    [+]php_url_parse_ex函數

    這里直接引用 https://www.cnblogs.com/hf99/p/9637354.html

    如果s中含有冒號則e指向冒號,且同時如果冒號不在s的開頭,p指向s

    當p不指向冒號向循環,p指向下一位

    如果p指向的值是字母或者數字或者是+,-,.則指針指向下一位,這就代表冒號前面的值其實是任意的字母、數字、+、-、.

    如果冒號所在位置小于str,且?#在冒號后面(如果有的話),就跳轉到port解析部分

    如果str的長度大于1且str的前兩個字符是//,s指向//后面的一個字符,e變為0,跳轉到host解析

    如果冒號是最后一位字符,則冒號前面的東西會當作scheme返回

    如果冒號后面不是/,則p指向冒號后面一位

    當p小于str且p指向的為數字字符,p一直指向后一位,直到p指向str末尾或者p指向的字符為/,同時冒號后面的數字位數小于6位,跳轉到port解析

    如果冒號后面不是純數字或數字后面有一個/,那么冒號前面的內容就當作scheme,放在ret的scheme參數中,s指向冒號后一位,跳轉到path解析

    如果冒號后面是/,那么冒號前面的內容就當作scheme,放在ret的scheme參數中。如果下面一位也是/,那么s指向//后面一位,如果scheme為file,那么判斷接下來一位是不是/,如果是,判斷冒號后是否有五個字符,如果有那么第五個字符是不是冒號(為了處理file:///c:),s指向///后的一位字符,跳轉到path解析

    如果冒號后面不是三個/,s指向冒號后面一位,之后跳轉到path解析

    如果冒號在str開頭,那么進行port解析

    因此只要是該url滿足其要求,都能夠通過filter_var的url過濾器,所以在這里強調其實一個非常弱的過濾器,其實在parse_url的處理中底層也使用了該函數,不過關于parse_url的函數缺陷,之后在進行分析,因此在此題中如果沒有過濾data協議,完全可以使用data協議進行繞過,但是此時由于過濾data://,因此使用的是另外一個協議compress.zlib

    該函數對壓縮流進行讀取

    可以在官方示例中看到,能夠將流進行讀取,因此compress.zlib同樣讀取能夠data協議流,因此我們構造$url = "compress.zlib://data:@baidu.com/;base64,aGVsbG8=";即可進行繞過

    根據上述內容也就解釋了為什么形如xx://xx;google.com也能通過parse_url,因為在底層處理中,完全沒有涉及分號;的對應處理,在解析時也只是將其視為一個簡單的字符串,但如果是http或者是https協議則會進行_php_filter_validate_domain處理,導致無法使用該種形式進行繞過

    理解libcurl和parse_url解析時的區別

    借blackhat上的這張圖進行說明:

    可以看到curl和parse_url對于解析host的相對位置是不同的,在PHP中parse_url傾向于解析較后面,其實在源碼中有著相應的答案:

    //e是字符串的結尾/* check for login and password */
        if ((p = zend_memrchr(s, '@', (e-s)))) {        if ((pp = memchr(s, ':', (p-s)))) {
                ret->user = zend_string_init(s, (pp-s), 0);
                php_replace_controlchars_ex(ZSTR_VAL(ret->user), ZSTR_LEN(ret->user));
                pp++;
                ret->pass = zend_string_init(pp, (p-pp), 0);
                php_replace_controlchars_ex(ZSTR_VAL(ret->pass), ZSTR_LEN(ret->pass));
            } else {
                ret->user = zend_string_init(s, (p-s), 0);
                php_replace_controlchars_ex(ZSTR_VAL(ret->user), ZSTR_LEN(ret->user));
            }
            s = p + 1;
        }
    

    看到這一段,對于user和password的解析過程,先對整個字符串判斷是否出現@,并將指針指向p,如果有則判斷@前面是否出現冒號,如果有則指向pp,將冒號前的字符串賦值給$ret->user,就是user,將冒號后一直到@符號前的字符串賦值給$ret->pass,否則若是字符串沒有出現冒號,則將@符號前面的整個字符串賦值為$ret->user,這一段的關鍵在于如何定位@符號,也就是zend_memrchr函數是如何實現的?

    可以很明顯的知道,該函數的搜索過程是自后向前的,因此他會從最后一個匹配@的字符串進行返回,因此在parse_url中,host匹配最后一個@后面符合格式的host

    來看一下curl中對url的處理,在ext/curl/interface.cphp_curl_option_url函數的處理涉及到對url的處理,跟進看下:

    static int php_curl_option_url(php_curl *ch, const char *url, const size_t len) /* {{{ */{    /* Disable file:// if open_basedir are used */
        if (PG(open_basedir) && *PG(open_basedir)) {#if LIBCURL_VERSION_NUM >= 0x071304
            curl_easy_setopt(ch->cp, CURLOPT_PROTOCOLS, CURLPROTO_ALL & ~CURLPROTO_FILE);#else
            php_url *uri;        if (!(uri = php_url_parse_ex(url, len))) {
                php_error_docref(NULL, E_WARNING, "Invalid URL '%s'", url);            return FAILURE;
            }        if (uri->scheme && zend_string_equals_literal_ci(uri->scheme, "file")) {
                php_error_docref(NULL, E_WARNING, "Protocol 'file' disabled in cURL");
                php_url_free(uri);            return FAILURE;
            }
            php_url_free(uri);#endif
    

    發現這里居然調用的是php_url_parse_ex,那不是和parse_url調用的一個底層函數,但是我們仔細看,php_url_parse_ex這個函數在這里的作用就是解析這個url使用了什么協議,再根據解析出來的協議$uri->scheme對比是否是file協議,真正調用過程中對url的解析處理在libcurl中,因為phpcurl也相當于是調用libcurl的動態鏈接庫,不過貌似很早已經被修復了,因此到這里不在繼續,能夠從源碼角度了解parse_url解析時,host匹配的是最后一個@后面符合格式的host就行[+]strpos函數利用

    [+]strpos函數利用

    一般在匹配某段輸入中存在黑名單中的關鍵詞,可能會使用strpos進行比較,該函數是用來查找字符串首次出現的位置,下面貼一下其函數原型:

    strpos — 查找字符串首次出現的位置
    作用:主要是用來查找字符在字符串中首次出現的位置。
    結構:int strpos ( string $haystack , mixed $needle [, int $offset = 0 ] )
    <?phpclass Login{    private $format;    public function __construct($user,$pass)
        {        $this->format =<<<XML
    <login>
    <user v='%s' ></user>
    <pass v='%s' ></pass>
    </login>
    XML;
            $this->loginViaXml($user,$pass);
        }    public function loginViaXml($user,$pass)
        {        if(
                !strpos($user,'<') || !strpos($user,'>') &&
                !strpos($pass,'<') || !strpos($pass,'>')
                )
            {
                $xml = sprintf($this->format,$user,$pass);            $this->login($xml);
            }else{            die("no");
            }
        }    function login($xmld)
        {
           print_r($xmld);
        }
    }new Login('user','pass');
    

    這里的業務是通過格式化字符串的方式,使用xml結構存儲用戶的登錄信息,并且判斷$user或者$pass不能含有尖括號以避免注入,但是使用這種方式真的可以防止注入嗎?

    我們可以構造new Login("<'> </user> \n<evil-tag></evil-tag>\n //",'pass');

    成功進行了注入,但是在我們的輸入中是存在尖括號,為何strpos會失效呢?

    strpos函數返回查找到的子字符串的下標。如果字符串開頭就是我們要搜索的目標,則返回下標0;如果搜索不到,則返回false 這里沒有對strpos有深刻的理解,導致在這里直接將結果進行取反,而我們知道false0取反后的效果是等價的,均為真,因此這里針對尖括號的過濾是有權限的,但事實上并非strpos的缺陷,只是在使用時存在缺陷導致了繞過,因此如果用strpos來判斷字符串中是否存在某個字符時必須使用===false

    下面簡單分析strpos底層的實現:

    PHP_FUNCTION(strpos)
    {
        zval *needle;
        zend_string *haystack;    const char *found = NULL;
        char  needle_char[2];
        zend_long  offset = 0;
        ZEND_PARSE_PARAMETERS_START(2, 3)
            Z_PARAM_STR(haystack)
            Z_PARAM_ZVAL(needle)
            Z_PARAM_OPTIONAL
            Z_PARAM_LONG(offset)
        ZEND_PARSE_PARAMETERS_END();    if (offset < 0) {
            offset += (zend_long)ZSTR_LEN(haystack);
        }    if (offset < 0 || (size_t)offset > ZSTR_LEN(haystack)) {
            php_error_docref(NULL, E_WARNING, "Offset not contained in string");
            RETURN_FALSE;
        }    if (Z_TYPE_P(needle) == IS_STRING) {        if (!Z_STRLEN_P(needle)) {
                php_error_docref(NULL, E_WARNING, "Empty needle");
                RETURN_FALSE;
            }
            found = (char*)php_memnstr(ZSTR_VAL(haystack) + offset,
                                Z_STRVAL_P(needle),
                                Z_STRLEN_P(needle),
                                ZSTR_VAL(haystack) + ZSTR_LEN(haystack));
        } else {        if (php_needle_char(needle, needle_char) != SUCCESS) {
                RETURN_FALSE;
            }
            needle_char[1] = 0;
            php_error_docref(NULL, E_DEPRECATED,            "Non-string needles will be interpreted as strings in the future. " \            "Use an explicit chr() call to preserve the current behavior");
            found = (char*)php_memnstr(ZSTR_VAL(haystack) + offset,
                                needle_char,                            1,
                                ZSTR_VAL(haystack) + ZSTR_LEN(haystack));
        }    if (found) {
            RETURN_LONG(found - ZSTR_VAL(haystack));
        } else {
            RETURN_FALSE;
        }
    }
    

    其中第三個參數offset指從字符串的第幾位開始,默認是0,主要是調用php_memnstr進行搜素匹配,其為函數zend_memnstr的宏定義:

    static inline const char * zend_memnstr(const char *haystack /*目標符串*/,                        const char *needle/*預查找字符串*/, int needle_len, char *end){    const char *p = haystack;//目標字符串首指針
        const char ne = needle[needle_len-1];//預查找字符串的最后一個字符
        if(needle_len == 1){//所查找的字符串是一個字符,則使用系統函數memchr
            return (char *)memchr(p, *needle, (end-p));
        }    if(needle_len > end-haystack){//所查找的字符串比目標字符串還長,則無需查找,直接返回找不著
            return NULL;
        }
        end -= needle_len;//其實只要查找到end-neele_len位置就可以了
        while(p<end){//從頭指針一字符一字符地找
            //如果頭字符和尾字符同時匹配,則用memcmp比較是否已經找到
            if((p = (char *)memchr(p, *needle, (end-p+1))) && ne == p[needle_len-1]){            if(!memcmp(needle, p, needle_len-1)){                return p;
                }
            }        if(p == NULL){//如果連頭一個字符都沒找著,則停止查找最合適
                return NULL;
            }
            p++;//頭指針向后移動一下
        }    return NULL;//到尾了還沒找著,則返回NULL}
    

    也是比較容易理解,此處理解strpos在未匹配到時返回False,在第一個位置匹配到將會返回0,并且正確使用strpos即可

    [+]parse_str函數的利用

    先來看一下官方文檔對于該函數的原型介紹:

    功能 :parse_str的作用就是解析字符串并且注冊成變量,它在注冊變量之前不會驗證當前變量是否存在,所以會直接覆蓋掉當前作用域中原有的變量。
    定義 :void parse_str( string $encoded_string [, array &$result ] )
    如果 encoded_string 是 URL 傳入的查詢字符串(query string),則將它解析為變量并設置到當前作用域(如果提供了 result 則會設置到該數組里 )。
    8.0.0 result 是必須項。
    7.2.0 不帶第二個參數的情況下使用 parse_str() 會產生E_DEPRECATED 警告

    也就是說,該函數將字符串賦值式中的字符串解析為變量,值為該變量的值,并且不會檢驗變量是否已經存在,如果存在也會將其覆蓋,變量覆蓋的相關漏洞也很常見了,這里不再談論變量覆蓋的利用問題,而是先從底層開始分析,再看從底層能夠發掘出什么樣的問題,注意從PHP7.2開始不使用第二個參數會出現警告,但是不影響程序執行

    PHP_FUNCTION(parse_str)
    {
        char *arg;
        zval *arrayArg = NULL;
        char *res = NULL;
        size_t arglen;
        ZEND_PARSE_PARAMETERS_START(1, 2)
            Z_PARAM_STRING(arg, arglen)
            Z_PARAM_OPTIONAL
            Z_PARAM_ZVAL(arrayArg)
        ZEND_PARSE_PARAMETERS_END();
        res = estrndup(arg, arglen);    if (arrayArg == NULL) {
            zval tmp;
            zend_array *symbol_table;        if (zend_forbid_dynamic_call("parse_str() with a single argument") == FAILURE) {
                efree(res);            return;
            }
            php_error_docref(NULL, E_DEPRECATED, "Calling parse_str() without the result argument is deprecated");
            symbol_table = zend_rebuild_symbol_table();
            ZVAL_ARR(&tmp, symbol_table);
            sapi_module.treat_data(PARSE_STRING, res, &tmp);        if (UNEXPECTED(zend_hash_del(symbol_table, ZSTR_KNOWN(ZEND_STR_THIS)) == SUCCESS)) {
                zend_throw_error(NULL, "Cannot re-assign $this");
            }
        } else     {
            arrayArg = zend_try_array_init(arrayArg);        if (!arrayArg) {
                efree(res);            return;
            }
            sapi_module.treat_data(PARSE_STRING, res, arrayArg);
        }
    }/* }}} */
    

    在這里可以發現,無論是有沒有第二個參數,最終都是調用sapi_module.treat_data處理數據,而在PHP中$_GET、$_COOKIE、$_SERVER、$_ENV、$_FILES、$_REQUEST這六個變量都是通過如下的調用序列進行初始化。

    [main() -> php_request_startup() -> php_hash_environment() ]
    

    在請求初始化時,通過調用 php_hash_environment 函數初始化以上的六個預定義的變量。如下所示為php_hash_environment函數的部分代碼(考慮到篇幅問題)

    ...for (p=PG(variables_order); p && *p; p++) {    switch(*p) {            case 'p':            case 'P':                    if (!_gpc_flags[0] && !SG(headers_sent) && SG(request_info).request_method && !strcasecmp(SG(request_info).request_method, "POST")) {
                                sapi_module.treat_data(PARSE_POST, NULL, NULL TSRMLS_CC);   /* POST Data */
                                _gpc_flags[0] = 1;                            if (PG(register_globals)) {
                                        php_autoglobal_merge(&EG(symbol_table), Z_ARRVAL_P(PG(http_globals)[TRACK_VARS_POST]) TSRMLS_CC);
                                }
                        }                    break;            case 'c':            case 'C':                    if (!_gpc_flags[1]) {
                                sapi_module.treat_data(PARSE_COOKIE, NULL, NULL TSRMLS_CC); /* Cookie Data */
                                _gpc_flags[1] = 1;                            if (PG(register_globals)) {
                                        php_autoglobal_merge(&EG(symbol_table), Z_ARRVAL_P(PG(http_globals)[TRACK_VARS_COOKIE]) TSRMLS_CC);
                                }
                        }                    break;            case 'g':            case 'G':                    if (!_gpc_flags[2]) {
                                sapi_module.treat_data(PARSE_GET, NULL, NULL TSRMLS_CC);    /* GET Data */
                                _gpc_flags[2] = 1;                            if (PG(register_globals)) {
                                        php_autoglobal_merge(&EG(symbol_table), Z_ARRVAL_P(PG(http_globals)[TRACK_VARS_GET]) TSRMLS_CC);
                                }
                        }                    break;
    ...
    

    可以看到,在$_POST,$_GET等變量的解析中,同樣是調用sapi_module.treat_data進行數據的處理,treat_data是屬于sapi_module_struct中的一個成員,定位到main/php_variables.c

    if (arg == PARSE_GET) {        /* GET data */
            c_var = SG(request_info).query_string;        if (c_var && *c_var) {
                res = (char *) estrdup(c_var);
                free_buffer = 1;
            } else {
                free_buffer = 0;
            }
        } else if (arg == PARSE_COOKIE) {        /* Cookie data */
            c_var = SG(request_info).cookie_data;        if (c_var && *c_var) {
                res = (char *) estrdup(c_var);
                free_buffer = 1;
            } else {
                free_buffer = 0;
            }
        } else if (arg == PARSE_STRING) {        /* String data */
            res = str;
            free_buffer = 1;
        }    if (!res) {        return;
        }
    

    可以看到對于GET型的處理是第一種,而對于parse_str也就是PARSE_STRING是第三種,繼續向下跟進:

    var = php_strtok_r(res, separator, &strtok_buf);
    ...while (var) {
         val = strchr(var, '=');     if (arg == PARSE_COOKIE) {          /* Remove leading spaces from cookie names,
                   needed for multi-cookie header where ; can be followed by a space */
              while (isspace(*var)) {               var++;
              }          if (var == val || *var == '\0') {               goto next_cookie;
              }
         }     if (val) { /* have a value */
              int val_len;
              unsigned int new_val_len;
              *val++ = '\0';
              php_url_decode(var, strlen(var));//先進行urldecode
              val_len = php_url_decode(val, strlen(val));
              val = estrndup(val, val_len);          if (sapi_module.input_filter(arg, var, &val, val_len, &new_val_len TSRMLS_CC)) {
                   php_register_variable_safe(var, val, new_val_len, array_ptr TSRMLS_CC);
              }
              efree(val);
         } else {
    ...
    

    通過php_strtok_r把res根據”&”分割”key=value”段, 接下來分別為var和val復制為key和value, 經過調試在php_register_variable_safe中會調用main/php_variables.cphp_register_variable_ex函數

    會對變量的空格、點變成下劃線,而當解析變量中出現形如a[b中會將is_array=1

    進入到is_array后的處理,我們根據注釋以及代碼也可以發現:

    并且在此之前前文代碼中已經提到,會先對傳入變量進行urldecode處理,因此總結起來為:

    PHP需要將所有參數轉換為有效的變量名,在解析查詢字符串時,它會做兩件事:

    • 刪除空白符
    • 將某些字符(點、括號、空格等)轉換為下劃線(包括空格)

    因此我們基于此可以將填充數據后最終解析成相同變量的所有不同填充形式的數據都進行fuzz,這里貼下已有的腳本

    <?php
        foreach(
            [            "{chr}foo_bar",            "foo{chr}bar",            "foo_bar{chr}"
            ] as $k => $arg) {            for($i=0;$i<=255;$i++) {                echo "\033[999D\033[K\r";                echo "[".$arg."] check ".bin2hex(chr($i))."";
                    parse_str(str_replace("{chr}",chr($i),$arg)."=bla",$o);                /* yes... I've added a sleep time on each loop just for 
                    the scenic effect :)
      like that movie with unrealistic 
                    brute-force where the password are obtained 
                    one byte at a time (∩`-′)?━☆?.*??? 
                    */
                    usleep(5000);                if(isset($o["foo_bar"])) {                    echo "\033[999D\033[K\r";                    echo $arg." -> ".bin2hex(chr($i))." (".chr($i).")\n";
                    }
                }            echo "\033[999D\033[K\r";            echo "\n";
        }
    

    得到如下數據都能解析成相同變量:

    [+]escapeshellcmd/escapeshellarg函數利用

    我們知道命令注入是很常見的一個問題,原因就是開發者對于用戶的輸入沒有進行過濾而直接拼接到語句中進行執行,因此在PHP中提供了若干對輸入進行過濾的函數以避免命令注入,這里最常見的便是escapeshellcmdescapeshellarg

    這里先將兩者的函數原型貼一下:

    escapeshellarg

    escapeshellarg — 把字符串轉碼為可以在 shell 命令里使用的參數
    功能 :escapeshellarg() 將給字符串增加一個單引號并且能引用或者轉碼任何已經存在的單引號,這樣以確保能夠直接將一個字符串傳入 shell 函數,shell 函數包含 exec(),system() 執行運算符(反引號)
    定義 :string escapeshellarg ( string $arg )

    escapeshellcmd

    escapeshellcmd — shell 元字符轉義
    escapeshellcmd() 對字符串中可能會欺騙 shell 命令執行任意命令的字符進行轉義。此函數保證用戶輸入的數據在傳送到 exec() 或 system() 函數,或者 執行操作符 之前進行轉義。
    反斜線(\)會在以下字符之前插入:&#;`|*?~<>^()[]{}$\, \x0A 和 \xFF。‘ 和 “ 僅在不配對兒的時候被轉義。在 Windows 平臺上,所有這些字符以及 % 和 ! 字符都會被空格代替。

    而兩者的功能有著一定的差別:

    escapeshellarg

    • 1.確保用戶只傳遞一個參數給命令
    • 2.用戶不能指定更多的參數
    • 3.用戶不能執行不同的命令

    escapeshellcmd

    • 1.確保用戶只執行一個命令
    • 2.用戶可以指定更多的參數
    • 3.用戶不能執行更多的命令

    來看一下escapeshellarg/escapeshellcmd的輸出

    <?php//$file = "sth -or -exec cat /etc/passwd ; -quit";//system("find /tmp -iname ".escapeshellcmd($file));$arg = "1 ' or '1=1";echo escapeshellarg($arg);// Output: /etc/passwd// Output: '1 '\'' or '\''1=1'?>
    

    escapeshellcmd允許執行多個參數意味著如果該可執行程序中本身就有能夠執行命令或者讀取文件的參數,最典型的例子就是find,示例如上,escapeshellarg的處理是如果字符串中出現了'則先將其轉義后,再將每一部分用單引號括起來,但是這里如果將escapeshellargescapeshellcmd混用將會導致參數的注入.

    這里我們借助BUUCTF 2018 Online Tool這個題來分析這兩個函數的源碼以及利用的過程:

    <?phpif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
    }if(!isset($_GET['host'])) {
        highlight_file(__FILE__);
    } else {
        $host = $_GET['host'];
        $host = escapeshellarg($host);
        $host = escapeshellcmd($host);
        $sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);    echo 'you are in sandbox '.$sandbox;
        @mkdir($sandbox);
        chdir($sandbox);    echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
    }
    

    可以看到這里使用escapeshellargescapeshellcmd$host進行過濾后拼接到了nmap語句中,這樣兩重過濾應該顯得更嚴格,但是事實并非如此,namp中有一個參數-oG可以實現將命令和結果寫到文件,所以我們可以控制自己的輸入寫入文件。但是如何進行繞過上面兩個函數的過濾呢?

    我們先來看看payload:

    ?host=' <?php phpinfo();?> -oG exp.php '
    

    為何加入單引號后能夠繞過,不是對單引號進行轉義了嗎?

    exec.cescapeshellarg調用了php_escape_shell_arg函數對輸入進行處理,跟進查看:

    /* {{{ php_escape_shell_arg
     */PHPAPI zend_string *php_escape_shell_arg(char *str)
    {
        size_t x, y = 0;
        size_t l = strlen(str);
        zend_string *cmd;
        uint64_t estimate = (4 * (uint64_t)l) + 3;    /* max command line length - two single quotes - \0 byte length */
        if (l > cmd_max_len - 2 - 1) {
            php_error_docref(NULL, E_ERROR, "Argument exceeds the allowed length of %zu bytes", cmd_max_len);        return ZSTR_EMPTY_ALLOC();
        }
        cmd = zend_string_safe_alloc(4, l, 2, 0); /* worst case */
        ZSTR_VAL(cmd)[y++] = '\'';    for (x = 0; x < l; x++) {        //這一段是5.2.6新加的,就是在處理多字節符號的時候,當多字節字符小于0的時候不處理,大于1的時候跳過,等于1的時候執行過濾動作
            int mb_len = php_mblen(str + x, (l - x));        /* skip non-valid multibyte characters */
            if (mb_len < 0) {            continue;
            } else if (mb_len > 1) {
                memcpy(ZSTR_VAL(cmd) + y, str + x, mb_len);
                y += mb_len;
                x += mb_len - 1;            continue;
            }        switch (str[x]) {        case '\'':
                ZSTR_VAL(cmd)[y++] = '\'';
                ZSTR_VAL(cmd)[y++] = '\\';
                ZSTR_VAL(cmd)[y++] = '\'';#endif
                /* fall-through */
            default:
                ZSTR_VAL(cmd)[y++] = str[x];
            }
        }
    ZSTR_VAL(cmd)[y++] = '\'';#endif
        ZSTR_VAL(cmd)[y] = '\0';    if (y > cmd_max_len + 1) {
            php_error_docref(NULL, E_ERROR, "Escaped argument exceeds the allowed length of %zu bytes", cmd_max_len);
            zend_string_release_ex(cmd, 0);        return ZSTR_EMPTY_ALLOC();
        }    if ((estimate - y) > 4096) {        /* realloc if the estimate was way overill
             * Arbitrary cutoff point of 4096 */
            cmd = zend_string_truncate(cmd, y, 0);
        }
        ZSTR_LEN(cmd) = y;    return cmd;
    }
    

    可以很明顯的看到,如果字符串中存在單引號',則先在其前面一次填充單引號,斜杠,單引號,再將這個原本的單引號處理

    ZSTR_VAL(cmd)[y++] = str[x];

    而在未處理和循環完成后都進行了

    ZSTR_VAL(cmd)[y++] = '\'';
    

    因此總結起來就是首先將字符串用單引號包圍,然后遇到單引號則在其前面加上單引號,反斜杠,單引號,各位有興趣也可以自己驗證一下看是否和源碼一致

    因此可以自己推導一遍exp經過escapeshellarg處理后的樣子:

    <?php$arg = "' <?php phpinfo();?> -oG exp.php '";echo escapeshellarg($arg);/* Output: ''\'' <?php phpinfo();?> -oG exp.php '\''' */?>
    

    和輸出是一樣的,這樣原來的exp經過過濾后變成了如上的形狀,接著再經過escapeshellcmd的處理,同樣在底層調用php_escape_shell_cmd函數,繼續分析:

    //篇幅原因這里選擇關鍵處理步驟...#ifndef PHP_WIN32
                case '"':            case '\'':                if (!p && (p = memchr(str + x + 1, str[x], l - x - 1))) {                    /* noop */
                    } else if (p && *p == str[x]) {
                        p = NULL;
                    } else {
                        ZSTR_VAL(cmd)[y++] = '\\';
                    }
                    ZSTR_VAL(cmd)[y++] = str[x];                break;#else
                /* % is Windows specific for environmental variables, ^%PATH% will
                    output PATH while ^%PATH^% will not. escapeshellcmd->val will escape all % and !.
                */
                case '%':            case '!':            case '"':            case '\'':#endif
                case '#': /* This is character-set independent */
                case '&':            case ';':            case '`':            case '|':            case '*':            case '?':            case '~':            case '<':            case '>':            case '^':            case '(':            case ')':            case '[':            case ']':            case '{':            case '}':            case '$':            case '\\':            case '\x0A': /* excluding these two */
                case '\xFF':#ifdef PHP_WIN32
                    ZSTR_VAL(cmd)[y++] = '^';#else
                    ZSTR_VAL(cmd)[y++] = '\\';#endif
                    /* fall-through */
                default:
                    ZSTR_VAL(cmd)[y++] = str[x];
            }
        }
    ...
    

    可以看到,如果是有引號存在,會在該引號后面的字符串中尋找是否還出現引號,如果出現則不處理,但如果沒有則會加入反斜杠進行轉義,同時會判斷

    &#;`|*?~<>^()[]{}$\,\x0A和\xFF //如果出現則進行轉義
    

    因此經過escapeshellarg處理后在經過escapeshellcmd處理,最終會變成

    ''\\'' \<\?php phpinfo\(\)\;\?\> -oG exp.php '\\'''
    

    此時在進行拼接,system函數傳入為如下語句:

    nmap -T5 -sT -Pn --host-timeout 2 -F ''\\'' \<\?php phpinfo\(\)\;\?\> -oG exp.php '\\'''
    

    而在linux中是支持空白連接符的,并且\這里視為將反斜杠進行轉義,linux解析時雙反斜杠也不會影響其他語句的執行:

    再將空白連接符去除進行簡化得到

    nmap -T5 -sT -Pn --host-timeout 2 -F  \<\?php phpinfo\(\)\;\?\> -oG exp.php
    

    最終進行寫入,當然關于escapeshell和escapecmd混合使用的漏洞還有許多,有興趣的大佬可以研究PHPmailer的兩個CVE

    CVE-2016-10045和CVE-2016-10033

    [+]深入理解$_REQUEST數組

    關于$_REQUEST這個全局變量應該都不陌生,在官方文檔中是如下描述的:

    $_REQUEST — HTTP Request 變量
    默認情況下包含了$_GET,$_POST$_COOKIE的數組。

    其實官方文檔的介紹中容易讓人產生誤解,我們來看這樣一個簡單的demo

    //Don't forget, because $_REQUEST is a different variable than $_GET and $_POST, it is treated as such in PHP -- modifying $_GET or $_POST elements at runtime will not affect the ellements in $_REQUEST, nor vice versa.<?php$_GET['foo'] = 'a';
    $_POST['bar'] = 'b';
    var_dump($_GET); // Element 'foo' is string(1) "a"var_dump($_POST); // Element 'bar' is string(1) "b"var_dump($_REQUEST); // Does not contain elements 'foo' or 'bar'?>
    

    也就是說雖然$_REQUEST默認情況下包含了$_GET,$_POST$_COOKIE,但是對一方的處理完全不會影響另一方,這里如果我們想要對輸入進行過濾,而代碼是這樣的

    ...class foo{    public function filterParams(){
            $_GET = array_map('addslashes',$_GET);
            $_POST = array_map('addslashes',$_POST);
        }    public function login(){        $this->filterParams();
            $sql = "select * from users where username = $_REQUEST['user'] and password = $_REQUEST['pass']";
        }
    }
    ...
    

    本意是想對輸入全部進行轉義,這樣能夠避免注入,但事實上對$_GET$_POST的操作并不會影響$_REQUEST這個預定義變量,下面我們從內核角度來看$_REQUEST是如何實現的

    其實在PHP中$_GET、$_COOKIE、$_SERVER、$_ENV、$_FILES、$_REQUEST這六個變量都是通過如下的調用序列進行初始化。

    [main() -> php_request_startup() -> php_hash_environment() ]
    

    在請求初始化時,通過調用 php_hash_environment 函數初始化以上的六個預定義的變量。

    而在此之前,apache處理到response階段的時候, 會將控制權交給PHP模塊, PHP模塊會在處理請求之前首先間接調用php_request_startup

     int php_request_startup(TSRMLS_D)
    {
        int retval = SUCCESS;#if PHP_SIGCHILD
        signal(SIGCHLD, sigchld_handler);#endif
        if (php_start_sapi() == FAILURE) {        return FAILURE;
        }
        php_output_activate(TSRMLS_C);
        sapi_activate(TSRMLS_C);
        php_hash_environment(TSRMLS_C);
        zend_try {
            PG(during_request_startup) = 1;
            php_output_activate(TSRMLS_C);        if (PG(expose_php)) {
                sapi_add_header(SAPI_PHP_VERSION_HEADER, sizeof(SAPI_PHP_VERSION_HEADER)-1, 1);
            }
        } zend_catch {
            retval = FAILURE;
        } zend_end_try();    return retval;
    }
    

    其中php_hash_environment(TSRMLS_C)函數調用 , 這個函數就是在請求處理前, 初始化請求相關的變量的函數,在這個函數中,會通過php_auto_globals_create_request來注冊$_REQUEST大變量:

    static zend_bool php_auto_globals_create_request(zend_string *name)
    {
        zval form_variables;
        unsigned char _gpc_flags[3] = {0, 0, 0};
        char *p;
        array_init(&form_variables);    if (PG(request_order) != NULL) {
            p = PG(request_order);
        } else {
            p = PG(variables_order);
        }    for (; p && *p; p++) {        switch (*p) {            case 'g':            case 'G':                if (!_gpc_flags[0]) {
                        php_autoglobal_merge(Z_ARRVAL(form_variables), Z_ARRVAL(PG(http_globals)[TRACK_VARS_GET]));
                        _gpc_flags[0] = 1;
                    }                break;            case 'p':            case 'P':                if (!_gpc_flags[1]) {
                        php_autoglobal_merge(Z_ARRVAL(form_variables), Z_ARRVAL(PG(http_globals)[TRACK_VARS_POST]));
                        _gpc_flags[1] = 1;
                    }                break;            case 'c':            case 'C':                if (!_gpc_flags[2]) {
                        php_autoglobal_merge(Z_ARRVAL(form_variables), Z_ARRVAL(PG(http_globals)[TRACK_VARS_COOKIE]));
                        _gpc_flags[2] = 1;
                    }                break;
            }
        }
        zend_hash_update(&EG(symbol_table), name, &form_variables);    return 0;
    }
    

    這里只是將$_GET, $_POST, $_COOKIEmerge起來,調用php_autoglobal_merge將相關變量的值寫到符號表,最終調用zend_hash_update,將相關變量的值賦值給&EG(symbol_table),因此在這里$_REQUEST可以理解為一個全新的預定義變量,因其對$_POST、$_GET并不是引用,而知相當于將其重新賦值后寫到符號表中,是一個嶄新的變量(個人是如此理解的,如有不當,還請諒解)

    這里就引出了另外一個話題,注意到在進行switch之前進行了p = PG(variables_order);操作

    檢測是否設置request_order,如果未設置則按照variables_order順序來指導大變量的生成

    這一點從官方文檔中也得到了映證

    不過這里php.ini中是存在request_order=”GP”的,但是動調的時候PG(request_order==NULL),這一點沒有想明白,不過即使是根據request_order也是先G后P,因此POST的變量名和GET變量名一致時仍然會進行覆蓋

    variables_order這其實是用來控制PHP是否生成某個大變量以及大變量的生成順序,該順序在php.ini中已經定義

    ; variables_order
    ;   Default Value: "EGPCS";   Development Value: "GPCS";   Production Value: "GPCS"
    

    一般在php.ini中都使用variables_order = "GPCS"

    默認的順序為EGPCS,在官方文檔中也能看到:

    因P在G之后進行處理,因此如果同時存在$_GET['foo']$_POST['foo'],那么根據默認的規則,最終$_POST['foo']的數據會將$_GET['foo']的數據覆蓋掉,最終的$_REQUEST[‘foo’]的數據自然是POST中的數據

    示例如下:

    var_dump($_REQUEST['foo']);
    

    因此整個預定義變量$_REQUEST的原理也就至此,該種特性之前也被用來在CTF中進行考察

    總結

    總的來說,從內核層面來分析函數能夠對函數有更深層次的了解,在此過程中需要結合動態調試來試著分析整個執行過程,并且需要對PHP的內核有一定的了解,對函數的參數類型和個數要有一定的敏感度,并且多去查閱官方文檔,對函數參數以及官方文檔中標注意的地方多關注。

    參考鏈接:

    https://github.com/hongriSec/PHP-Audit-Labs

    https://www.bookstack.cn/read/php-internals/22.md

    str函數php字符串長度
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    最近寫了點反序列化的題,才疏學淺,希望對CTF新手有所幫助,有啥錯誤還請大師傅們批評指正。php反序列化簡單理解首先我們需要理解什么是序列化,什么是反序列化?本質上反序列化是沒有危害的。但是如果用戶對數據可控那就可以利用反序列化構造payload攻擊。
    這里根據紅日安全PHP-Audit-Labs對一些函數缺陷的分析,從PHP內核層面來分析一些函數的可利用的地方,標題所說的函數缺陷并不一定是函數本身的缺陷,也可能是函數在使用過程中存在某些問題,造成了漏洞,以下是對部分函數的分析
    unserialize()函數能夠重新把字符串變回php原來的值。為了能夠unserialize()一個對象,這個對象的類必須已經定義過。如果序列化類A的一個對象,將會返回一個跟類A相關,而且包含了對象所有變量值的字符串。將對象格式化成有序的字符串。序列化的目的是方便數據的傳輸和存儲,在PHP中,序列化和反序列化一般用做緩存,比如session緩存,cookie等。
    關注一波,謝謝各位師傅感謝ch1e師傅幫忙總結ch1e‘blog:https://ch1e.gitee.io
    寫在前面關于無字母數字Webshell這個話題,可以說是老生常談了。之前打 CTF 的時候也經常會遇到,每次都讓人頭大,所謂無字符webshell,其基本原型就是對以下代碼的繞過:PHP中的異或來看這樣一段代碼:
    前言:滲透測試的時候往往會遇到盲注這類的繁雜的手工測試,所以需要編寫半自動化腳本去進行測試減少時間浪費并快速
    0x00前言 接到任務,需要對一些違法網站做滲透測試…… 0x01信息收集 根據提供的目標,打開網站如下
    0x00前言接到任務,需要對一些違法網站做滲透測試……最終定位到該系統為某網絡驗證系統下載最新版的代碼到本地,開始審計。
    對于攻擊者而言,只需要在POST BODY前面添加許多無用的數據,把攻擊的payload放在最后即可繞過WAF檢測。實驗步驟首先使用BurpSuite抓取數據包,并記下數據包的Header信息編寫好我們的Python腳本進行FUZZ:#!
    上官雨寶
    是水水水水是
      亚洲 欧美 自拍 唯美 另类