PHP变量覆盖

变量覆盖是指利用自定义的参数值替换掉原有的变量值。变量覆盖漏洞通常需要结合代码中的其他功能来实现完整的攻击。
常见可导致变量覆盖漏洞的函数或方法有PHP超全局变量、register_globals、$$、parse_str()函数使用不当、extract()、import_request_variables()等。

1) PHP超全局变量

http://php.net/manual/zh/language.variables.superglobals.php

PHP 中的许多预定义变量都是“超全局的”,在一个脚本的全部作用域中都可用。在函数或方法中无需执行 global $variable; 就可以访问它们。
2019年1月11日ThinkPHP 5.0.x~5.2x爆出的远程代码执行漏洞就与$_POST变量覆盖相关。
这些超全局变量是:

1
2
3
4
5
6
7
8
9
$GLOBALS  包含了全部变量的全局组合数组。
$_SERVER 包含服务器和执行环境等信息的数组。
$_GET 通过 URL 参数传递给当前脚本的变量的数组
$_POST 当 HTTP POST 请求的 Content-Type 是 application/x-www-form-urlencoded 或 multipart/form-data 时,会将变量以关联数组形式传入当前脚本。
$_FILES HTTP 文件上传变量。通过 HTTP POST 方式上传到当前脚本的项目的数组。
$_COOKIE 通过 HTTP Cookies 方式传递给当前脚本的变量的数组
$_SESSION 当前脚本可用 SESSION 变量的数组
$_REQUEST 默认情况下包含了 $_GET,$_POST 和 $_COOKIE 的数组
$_ENV 通过环境方式传递给当前脚本的变量的数组

1
2
3
4
5
6
7
8
<?php
$name = "Tom";
echo $name."<br>"; //输出Tom

$name = $_POST['www'];
echo $name."<br>"; //POST:www=ppl 输出ppl
print_r($_POST); //输出 Array ( [www] => ppl )
?>

 

2) register_globals

http://php.net/manual/zh/security.globals.php
当php.ini 中register_globals= On时 设置注册全局变量,将传递过来的值直接注册为全局变量并可直接使用。(必须是之前没有声明的变量,如果之前变量已经存在就无法覆盖)。
该特性从PHP 4.2.0开始将register_globals默认值设置为off,自 PHP 5.3.0 起废弃,自 PHP 5.4.0 起移除。

1
2
3
4
5
6
<?php  
//测试 PHP Version 5.2.17
//请求: xxx.php?www=1
echo "Register_globals: ".(int)ini_get("register_globals")."<br/>";
echo '$www :'.$www;
?>
1
2
3
4
5
6
7
8
9
请求: xxx.php?www=1
当php.ini 中设置 register_globals = On时,输出:
Register_globals: 1
$www :1
注册了个全局变量$www,并可直接使用

当php.ini 中设置 register_globals = Off时,输出:
Register_globals: 0
$www :

3) $$

$$ 导致的变量覆盖常在foreach中出现,使用foreach来遍历数组中的值,然后将获取到的数组键名作为变量名,数组中的键值作为变量的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$id = 0;
foreach (array('_COOKIE','_POST','_GET') as $_request)
{
foreach ($$_request as $_key=>$_value)
{
//echo $_key."</br>";
$$_key= addslashes($_value); //GET: ?id=1 覆盖掉之前$id得值 现在id值为1
}
}

echo $id; //GET: xxx.php?id=1 输出1
?>

4) parse_str()

parse_str ( string $encoded_string [, array &$result ] )
如果 encoded_string 是 URL 传递入的查询字符串(query string),则将它解析为变量并设置到当前作用域
当没有result 参数时,可造成变量覆盖,在 PHP 7.2 中废弃不设置参数的行为。
推荐的安全写法:设置第二个变量 result, 变量将会以数组元素的形式存入到这个数组。

不安全写法:

1
2
3
4
5
6
<?php
$name = "Tom";
$age = 18;
parse_str("name=ppl&age=20"); //覆盖掉原来变量name和age的值
echo $name."<br>"; //输出ppl
echo $age."<br>"; //输出20

推荐的安全写法:

1
2
3
4
5
6
7
8
9
10
<?php
//推荐用法:
$name = "Tom";
$age = "18";
parse_str("name=ppl&age=20",$output); //不会覆盖原变量
echo $name."<br>"; //还是原来的值Tom
echo $age."<br>"; //还是原来的值18
echo $output['name']."<br>"; //输出ppl
echo $output['age']."<br>"; //输出20
?>

5) extract()

extract ( array &$array [, int $flags = EXTR_OVERWRITE [, string $prefix = NULL ]] )
将变量从数组中导入到当前的符号表中
参数 array一个关联数组。此函数会将键名当作变量名,值作为变量的值。 对每个键/值对都会在当前的符号表中建立变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
//case1:
$a = "Original";
$my_array = array("a" => "Cat","b" => "Dog", "c" => "Horse");
extract($my_array); //变量a值被覆盖为Cat
echo "a: $a; b: $b; c: $c"."<br>"; //输出a: Cat; b: Dog; c: Horse

//case2:
$www = '0'; //请求 GET: xxx.php?www=1
//var_dump($_GET);
extract($_GET); //变量www值被覆盖为1
echo "www: $www"."<br>"; //输出www: 1
?>

6) import_request_variables()

import_request_variables ( string $types [, string $prefix ] )
(PHP 4 >= 4.1.0, PHP 5 < 5.4.0)
将 GET/POST/Cookie 变量导入到全局作用域中。
可以用字母‘G’、‘P’和‘C’分别表示 GET、POST 和 Cookie。不区分大小写。

1
2
3
4
5
6
<?php  
//测试 PHP Version 5.2.17
$a = '0';
import_request_variables('GPC');
echo "a: $a"."<br>"; //请求xxx.php?a=1 输出a: 1
?>

参考:

https://p0sec.net/index.php/archives/35/ PHP变量覆盖漏洞总结
https://paper.tuisec.win/detail/85acab5a4db60f4 由MetInfo 深入理解PHP变量覆盖漏洞