共计 14135 个字符,预计需要花费 36 分钟才能阅读完成。
原标题:【技术分享】从内核层面分析部分 PHP 函数
前言
这里根据红日安全 PHP-Audit-Labs 对一些函数缺陷的分析,从 PHP 内核层面来分析一些函数的可利用的地方,标题所说的函数缺陷并不一定是函数本身的缺陷,也可能是函数在使用过程中存在某些问题,造成了漏洞,以下是对部分函数的分析
[+]in_array/array_search 函数缺陷
下面请看代码
$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_search 和in_array的内部实现一样,跟进:
/* void php_search_array(INTERNAL_FUNCTION_PARAMETERS, int behavior)
* 0 = return boolean
* 1 = return key
*/staticinline 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;
}elseif(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 时都是用该函数进行判断,跟进:
staticzend_always_inline int fast_equal_check_string(zval *op1, zval *op2)
{
zval result;if(EXPECTED(Z_TYPE_P(op2) == IS_STRING)) {returnzend_fast_equal_strings(Z_STR_P(op1), Z_STR_P(op2));
}// 参数 1:执行结果
// 参数 2:php 输入的查找项: 1
// 参数 3:php 数组当前项
compare_function(&result, op1, op2);returnZ_LVAL(result) ==0;
}
这里先了解一下 PHP 源码所定义的 8 中数据类型:
这里 op2 是整形,因此会直接使用 compare_function 进行比较,该函数实在太长,主要是现根据两个参数的数据类型来进行逻辑处理,这里op1 = IS_STRING,op2 = IS_LONG,但是IS_LONG 跟 IS_STRING 的情况不存在,就进入转换类型的分支, 所以这里直接将对应的处理部分贴下:
// Zend/zend_types.h 源码定义了如下,这里在比较时用得上,因此一起列出defineIS_UNDEF 0defineIS_NULL 1defineIS_FALSE 2defineIS_TRUE 3defineIS_LONG 4 // op2defineIS_DOUBLE 5defineIS_STRING 6 // op1defineIS_ARRAY 7defineIS_OBJECT 8defineIS_RESOURCE 9defineIS_REFERENCE 10ZEND_API int ZEND_FASTCALL compare_function(zval *result, zval *op1, zval *op2)/* {{{*/{intret;intconverted =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);returnSUCCESS;
}elseif(Z_TYPE_P(op1) == IS_TRUE) {
ZVAL_LONG(result, zval_is_true(op2) ?0:1);returnSUCCESS;
}elseif(Z_TYPE_P(op2) < IS_TRUE) {
ZVAL_LONG(result, zval_is_true(op1) ?1:0);returnSUCCESS;
}elseif(Z_TYPE_P(op2) == IS_TRUE) {
ZVAL_LONG(result, zval_is_true(op1) ?0:-1);returnSUCCESS;
}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);
}returnFAILURE;
}
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,这里放出最后的处理:
staticzend_always_inline zval* _zendi_convert_scalar_to_number_ex(zval *op, zval *holder, zend_bool silent)/* {{{*/{switch(Z_TYPE_P(op)) {caseIS_NULL:caseIS_FALSE:
ZVAL_LONG(holder,0);returnholder;caseIS_TRUE:
ZVAL_LONG(holder,1);returnholder;caseIS_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”);
}
}returnholder;caseIS_RESOURCE:
ZVAL_LONG(holder, Z_RES_HANDLE_P(op));returnholder;caseIS_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);
}returnholder;caseIS_LONG:caseIS_DOUBLE:default:returnop;
}
}
这里终于涉及到 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_search 和in_array在底层实现一样,因此 array_search 也存在该缺陷:
$arr =array(“true”=>1);
var_dump(array_search(1Crispr,$arr));//Output: string(4) “true”
[+]filter_var/parse_url 函数缺陷
function is_valid_url($url){if(filter_var($url,FILTER_VALIDATE_URL)){if(preg_match(/data:\/\//i, $url)) {returnfalse;
}returntrue;
}returnfalse;
}//$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,不过该判断是一个非常弱的判断,可以从下面的例子中发现:
//$url = “java://123”;//$url = “data://123”;//$url = “ftp://”;//$url = “php://123″//$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, 其调用情况:
staticvoid php_filter_call(zval *filtered, zend_long filter, zval *filter_args,constint copy, zend_long filter_flags)/* {{{*/{…
php_zval_filter(filtered, filter, filter_flags, options, charset, copy);
…
}
staticvoid 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: doesnt return null on failure for objects */
if(flags & FILTER_NULL_ON_FAILURE) {
ZVAL_NULL(value);
}else{
ZVAL_FALSE(value);
}gotohandle_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) {gotobad_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,“?”)) {gotoparse_port;
}elseif(s +1< ue && *s ==/&& *(s +1) ==/) {/* relative-scheme URL */
s +=2;
e =0;gotoparse_host;
}else{gotojust_path;
}
}
p++;
}if(e +1== ue) {/* only scheme is available */
ret->scheme = estrndup(s, (e – s));
php_replace_controlchars_ex(ret->scheme, (e – s));returnret;
}/*
* 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) {gotoparse_port;
}
ret->scheme = estrndup(s, (e-s));
php_replace_controlchars_ex(ret->scheme, (e – s));
s = e +1;gotojust_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;
}gotojust_path;
}
}
}else{
s = e +1;gotojust_path;
}
}
}elseif(e) {/* no scheme; starts with colon: look for port */
[+]php_url_parse_ex 函数
当 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.c 中php_curl_option_url函数的处理涉及到对 url 的处理,跟进看下:
staticint php_curl_option_url(php_curl *ch,constchar *url,constsize_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);returnFAILURE;
}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);returnFAILURE;
}
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] )
class Login{private$format;publicfunction __construct($user,$pass)
{$this->format =<<
XML;
$this->loginViaXml($user,$pass);
}publicfunction 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);
}
}newLogin(user,pass);
这里的业务是通过格式化字符串的方式,使用 xml 结构存储用户的登录信息,并且判断 $user 或者 $pass 不能含有尖括号以避免注入,但是使用这种方式真的可以防止注入吗?
我们可以构造new Login(“<> \n
成功进行了注入,但是在我们的输入中是存在尖括号,为何 strpos 会失效呢?
strpos 函数返回查找到的子字符串的下标。如果字符串开头就是我们要搜索的目标,则返回下标 0; 如果搜索不到, 则返回 false这里没有对 strpos 有深刻的理解,导致在这里直接将结果进行取反, 而我们知道 false 和0取反后的效果是等价的,均为真,因此这里针对尖括号的过滤是有权限的,但事实上并非 strpos 的缺陷,只是在使用时存在缺陷导致了绕过,因此如果用 strpos 来判断字符串中是否存在某个字符时必须使用===false
下面简单分析 strpos 底层的实现:
PHP_FUNCTION(strpos)
{
zval *needle;
zend_string *haystack;constchar *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 的宏定义:
staticinlineconstchar * zend_memnstr(constchar *haystack/* 目标符串 */,constchar *needle/* 预查找字符串 */, int needle_len, char *end){constchar *p = haystack;// 目标字符串首指针
constchar ne = needle[needle_len-1];// 预查找字符串的最后一个字符
if(needle_len ==1){// 所查找的字符串是一个字符, 则使用系统函数 memchr
return(char *)memchr(p, *needle, (end-p));
}if(needle_len > end-haystack){// 所查找的字符串比目标字符串还长, 则无需查找, 直接返回找不着
returnNULL;
}
end -= needle_len;// 其实只要查找到 end-neele_len 位置就可以了
while(p
// 如果头字符和尾字符同时匹配, 则用 memcmp 比较是否已经找到
if((p = (char *)memchr(p, *needle, (end-p+1))) && ne == p[needle_len-1]){if(!memcmp(needle, p, needle_len-1)){returnp;
}
}if(p ==NULL){// 如果连头一个字符都没找着, 则停止查找最合适
returnNULL;
}
p++;// 头指针向后移动一下
}returnNULL;// 到尾了还没找着, 则返回 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) {casep:caseP: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,NULLTSRMLS_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;casec:caseC:if(!_gpc_flags[1]) {
sapi_module.treat_data(PARSE_COOKIE,NULL,NULLTSRMLS_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;caseg:caseG:if(!_gpc_flags[2]) {
sapi_module.treat_data(PARSE_GET,NULL,NULLTSRMLS_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;
}
}elseif(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;
}
}elseif(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) {gotonext_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.c 的php_register_variable_ex函数
会对变量的 空格、点 变成下划线,而当解析变量中出现形如 a[b 中会将is_array=1
进入到 is_array 后的处理,我们根据注释以及代码也可以发现:
并且在此之前前文代码中已经提到,会先对传入变量进行 urldecode 处理,因此总结起来为:
PHP 需要将所有参数转换为有效的变量名,在解析查询字符串时,它会做两件事:
- 删除空白符
- 将某些字符 (点、括号、空格等) 转换为下划线(包括空格)
因此我们基于此可以将填充数据后最终解析成相同变量的所有不同填充形式的数据都进行 fuzz, 这里贴下已有的脚本
得到如下数据都能解析成相同变量:
[+]escapeshellcmd/escapeshellarg 函数利用
我们知道命令注入是很常见的一个问题,原因就是开发者对于用户的输入没有进行过滤而直接拼接到语句中进行执行,因此在 PHP 中提供了若干对输入进行过滤的函数以避免命令注入,这里最常见的便是 escapeshellcmd 和escapeshellarg
这里先将两者的函数原型贴一下:
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 的输出
//$file = “sth -or -exec cat /etc/passwd ; -quit”;//system(“find /tmp -iname “.escapeshellcmd($file));$arg =“1 or 1=1”;echoescapeshellarg($arg);// Output: /etc/passwd// Output: 1 \ or \1=1?>
escapeshellcmd允许执行多个参数意味着如果该可执行程序中本身就有能够执行命令或者读取文件的参数,最典型的例子就是 find,示例如上,escapeshellarg 的处理是如果字符串中出现了 则先将其转义后,再将每一部分用单引号括起来,但是这里如果将 escapeshellarg 和escapeshellcmd混用将会导致参数的注入.
这里我们借助 BUUCTF 2018 Online Tool 这个题来分析这两个函数的源码以及利用的过程:
(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]);echoyou are in sandbox.$sandbox;
@mkdir($sandbox);
chdir($sandbox);echosystem(“nmap -T5 -sT -Pn –host-timeout 2 -F “.$host);
}
可以看到这里使用 escapeshellarg 和escapeshellcmd对 $host 进行过滤后拼接到了 nmap 语句中,这样两重过滤应该显得更严格,但是事实并非如此,namp中有一个参数 -oG 可以实现将命令和结果写到文件,所以我们可以控制自己的输入写入文件。但是如何进行绕过上面两个函数的过滤呢?
我们先来看看 payload:
?host= -oG exp.php
为何加入单引号后能够绕过,不是对单引号进行转义了吗?
在 exec.c 中escapeshellarg调用了 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);returnZSTR_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;
}elseif(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);returnZSTR_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;returncmd;
}
可以很明显的看到,如果字符串中存在单引号,则先在其前面一次填充单引号,斜杠,单引号,再将这个原本的单引号处理
ZSTR_VAL(cmd)[y++] = str[x];
而在未处理和循环完成后都进行了
ZSTR_VAL(cmd)[y++] =\;
因此总结起来就是首先将字符串用单引号包围,然后遇到单引号则在其前面加上单引号,反斜杠,单引号,各位有兴趣也可以自己验证一下看是否和源码一致
因此可以自己推导一遍 exp 经过 escapeshellarg 处理后的样子:
$arg =” -oG exp.php “;echoescapeshellarg($arg);/* Output: \ -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 */
}elseif(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-timeout2-F\\\<\?php phpinfo\(\)\;\?\> -oG exp.php\\
而在 linux 中是支持空白连接符的,并且 \ 这里视为将反斜杠进行转义,linux 解析时双反斜杠也不会影响其他语句的执行:
再将空白连接符去除进行简化得到
nmap -T5 -sT -Pn –host-timeout2-F \<\?php phpinfo\(\)\;\?\> -oG exp.php
[+]深入理解 $_REQUEST 数组
关于 $_REQUEST 这个全局变量应该都不陌生,在官方文档中是如下描述的:
$_REQUEST — HTTP Request 变量
默认情况下包含了 $_GET,$_POST 和$_COOKIE的数组。
其实官方文档的介绍中容易让人产生误解,我们来看这样一个简单的 demo
//Dont 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.$_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{publicfunction filterParams{
$_GET = array_map(addslashes,$_GET);
$_POST = array_map(addslashes,$_POST);
}publicfunction 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) {returnFAILURE;
}
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;returnretval;
}
其中 php_hash_environment(TSRMLS_C) 函数调用,这个函数就是在请求处理前,初始化请求相关的变量的函数, 在这个函数中,会通过 php_auto_globals_create_request 来注册 $_REQUEST 大变量:
staticzend_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) {caseg:caseG:if(!_gpc_flags[0]) {
php_autoglobal_merge(Z_ARRVAL(form_variables), Z_ARRVAL(PG(http_globals)[TRACK_VARS_GET]));
_gpc_flags[0] =1;
}break;casep:caseP:if(!_gpc_flags[1]) {
php_autoglobal_merge(Z_ARRVAL(form_variables), Z_ARRVAL(PG(http_globals)[TRACK_VARS_POST]));
_gpc_flags[1] =1;
}break;casec:caseC: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);return0;
}
这里只是将 $_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
;DefaultValue:“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
– 结尾 –
【技术分享】从长城杯两道题目看新老 libc 的利用
【技术分享】Tomcat 内存马技术分析(一)—— Filter 型
【技术分享】从合规角度谈企业 App 使用第三方 SDK 时个人信息保护措施
戳阅读原文查看更多内容 返回搜狐,查看更多
责任编辑: