变量覆盖
0x01 register_globals
register_globals是php.ini里的一个配置,这个配置影响到php如何接收传递过来的参数。
顾名思义,register_globals的意思就是注册为全局变量,所以当On的时候,传递过来的值会被直接的注册为全局变量直接使用,而Off的时候,我们需要到特定的数组里去得到它。
例如:
当register_globals=Off的时候,下一个程序接收的时候应该用$_GET['user_name']
和$_GET['user_pass']
来接受传递过来的值。
当register_globals=On的时候,下一个程序可以直接使用$user_name
和$user_pass
来接受值。
**注意,在没有开启全局Register_blobals的情况下,调用extract(),parse_str(),import_request_variables()相当于开启了全局变量注册 **
0x02 php中的$str
这种写法叫做可变变量,先声明一个普通变量:
<?php
$a = "hello";
?>
再申明一个可变变量来获取一个普通变量的值作为该可变变量的变量名。
<?php
$$a = "world";
?>
这时,两个变量都被定义了:$a
的内容是“hello”并且 $hello
的内容是“world”。因此,可以表述为:
<?php
echo "$a ${$a}";
?>
等同于:
<?php
echo "$a $hello";
?>
均输出"hello world"
我刚开始总是搞不明白这几个变量之间的关系,其实结合实例对比一下就知道了:
<?php
foreach(Array('_GET') as $_request)
{
var_export($_GET);
echo "/////";
echo $_request;
echo "///////";
foreach($$_request as $_k => $_v) {
echo $_k;
${$_k} = $_v;
echo "/////";
echo ${$_k};
}
}
使用外部传递进来的参数不是类似与$_GET['key']
这样的原始的数组变量,而是通过$$key
把里面的key注册成了一个变量$key
,导致覆盖
0x03 php中的unset()
unset()在函数中的行为会依赖于想要销毁的变量的类型而有所不同.
- 如果在函数中 unset() 一个通过引用传递(&$str)的变量,则只是局部变量被销毁,而在调用环境中的变量将保持调用
unset() 之前一样的值。 - 如果在函数中 unset() 一个全局变量,则只是局部变量被销毁,而在调用环境中的变量将保持调用
unset() 之前一样的值。
意为unset()函数无法销毁一个全局变量,要销毁全局变量必须使用$GLOBALS['var_name']
0x04 extract()变量覆盖
extract(array,flags=EXTR_OVERWRITE,EXTR_IF_EXISTS,prefix)
extract用于从数组中将变量导入到当前的符号表,意思就是将数组中的键值对注册成变量。
此函数会将键名当作变量名,值作为变量的值。
flags为对待非法/数字和冲突的键名的方法
prefix 仅在flags 的值是EXTR_PREFIX_SAME,EXTR_PREFIX_ALL,EXTR_PREFIX_INVALID或EXTR_PREFIX_IF_EXISTS时需要。
0x05 import_request_variables()变量覆盖
相当于开启了全局变量注册,这时候只要找哪些变量没有初始化并且操作之前没有赋值的,然后就去提交这个变量作为参数。
写在
import_request_variables()
之前的变量,不管是否已经初始化都可以覆盖(PHP 4.1-5.4.0)
将 GET/POST/Cookie 变量导入到全局作用域中。如果你禁止了
register_globals
,但又想用到一些全局变量,那么此函数就很有用。
你可以使用
types
参数指定需要导入的变量。可以用字母‘G’、‘P’和‘C’分别表示
GET、POST 和 Cookie。这些字母不区分大小写,所以你可以使用‘g’、‘p’和‘c’的任何组合。
例如:import_request_variables('G')导入GET请求中的变量
0x06 parse_str() 变量覆盖
void parse_str( $encoded_string, $result)
作用是解析字符串并且注册成变量,它在注册变量之前不会验证当前变量是否已经存在,所以会直接覆盖掉已有变量。
如果
encoded_string
是 URL 传递入的查询字符串(query string),则将它解析为变量并设置到当前作用域(如果提供了result
则会设置到该数组里 )
与parse_str类似的还有mb_parse_str().
实例一
可以看到HTTP_REFERER中传入的参数id会被解析为参数$id,带入sql语句进行查询
我们知道,全局变量注册之前的参数都会被覆盖掉,如果我们在这里提交类似 config[dbhost]=127.0.0.1
的数据,可以覆盖掉前面连接数据库的配置,连接我们自己的数据库,可以在登陆时绕过验证。
实例二
//index.php
<?php
$a = “hongri”;
$id = $_GET['id'];
@parse_str($id);
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
echo '<a href="uploadsomething.php">flag is here</a>';
}
?>
//uploadsomething.php
<?php
header("Content-type:text/html;charset=utf-8");
$referer = $_SERVER['HTTP_REFERER']; //通过a标签点击的链接,会自己自动携带上refer字段
if(isset($referer)!== false) {
$savepath = "uploads/" . sha1($_SERVER['REMOTE_ADDR']) . "/";
if (!is_dir($savepath)) {
$oldmask = umask(0);
mkdir($savepath, 0777);
umask($oldmask);
}
if ((@$_GET['filename']) && (@$_GET['content'])) {
//$fp = fopen("$savepath".$_GET['filename'], 'w');
$content = 'HRCTF{y0u_n4ed_f4st} by:l1nk3r';
file_put_contents("$savepath" . $_GET['filename'], $content);
$msg = 'Flag is here,come on~ ' . $savepath . htmlspecialchars($_GET['filename']) . "";
usleep(100000);
$content = "Too slow!";
file_put_contents("$savepath" . $_GET['filename'], $content);
}
print <<<EOT
<form action="" method="get">
<div class="form-group">
<label for="exampleInputEmail1">Filename</label>
<input type="text" class="form-control" name="filename" id="exampleInputEmail1" placeholder="Filename">
</div>
<div class="form-group">
<label for="exampleInputPassword1">Content</label>
<input type="text" class="form-control" name="content" id="exampleInputPassword1" placeholder="Contont">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
EOT;
}
else{
echo 'you can not see this page';
}
?>
index.php需要我们传入一个数组,并在a[0]处将原变量$a覆盖掉,然后利用PHP弱类型即可绕过MD5值判断,而parse_str函数会将传入的字符串自动解码,故payload:?id=a%5b0%5d%3d240610708
或是?id=a[0]=s878926199a
成功进入下一步index.php
uploadsomething.php就需要用到竞争条件,程序通过$_SERVER['REMOTE_ADDR']
获取用户IP并将其SHA1加密作为文件目录,然后将FlAG内容写入文件,在0.1秒后重写文件覆盖掉FLAG的内容,所以需要我们写脚本来获取FLAG的内容。
官方WP的解法是,开Burp的200线程,不断发送上传文件的数据包。在start attack之前需要一个脚本不断请求所上传的文件地址:
import requests as r
r1=r.Session()
while (1):
r2=r1.get("http://127.0.0.1/parse_str/uploads/4b84b15bff6ee5796152495a230e45e3d7e947d9/flag")
print r2.text
pass
实例三
get_object_vars
返回由对象属性组成的关联数组
var_export
此函数返回关于传递给该函数的变量的结构信息,它和
var_dump()
类似,不同的是其返回的表示是合法的PHP 代码意思就是把全局变量全部返回到页面上,返回的内容会当作PHP代码执行。
注意!PHP中的变量自增不起作用,即
$count++ = $count
变量不会发生改变
在 第10-11行 处, Carrot 类的构造方法将超全局数组
$_GET
进行变量注册,这样即可覆盖 第8行 已定义的$this->id
变量。而在 第16行 处的析构函数中,file_put_contents
函数的第一个参数又是由$this->
变量拼接的,这就导致我们可以控制写入文件的位置,最终造成任意文件写入问题。
要写入自定义的内容,我们需要先闭合var_export函数,它的返回形式是:
array('id'=>'../var/www/','lost'=>0,'bought'=>'0','var'=>'string',)
参考参数id,我们构造payload:
?id=../var/www/html/shell.php&shell=',)%0a<?php phpinfo; ?>//
我们还可以利用PHP中的"
能够执行代码的特点,构造如"<?php phpinfo();?>"
的payload:id=../../var/www/html/test/shell.php&t1=1"<?php phpinfo()?>"//
//shell.php
array (
'id' => '../../var/www/html/test/shell.php',
'lost' => 0,
'bought' => 0,
't1' => '1"<?php phpinfo()?>"//',
)
所以本题的重点是在变量覆盖和$count++
没有起作用,但如果是++$count
就要注意了:
$test = 123; echo ++$test; // 124
$test = '123'; echo ++$test; // 124
$test = '1ab'; echo ++$test; // '1ac'
$test = 'ab1'; echo ++$test; // 'ab2'
$test = 'a1b'; echo ++$test; // 'a1c'
$test =array(2,'name'=>'wyj'); echo ++$test; //Array
通过分析发现,在进行++
操作时会进行隐式类型转换,如果能够转换成功,则会进行加法操作;如果不能转换成功,则将最后一个字符进行加法操作。
若将本题的代码改为下面这种:
foreach ($input as $field => $count) {
$this->$field = ++$count;
}
payload:?id=../var/html/test/shell.pho&shell=1"<?php phpinfo(); ?>"123
pho自增过后就会变为php
挖掘经验
要挖可用的变量覆盖漏洞,一定要看漏洞代码行之前存在那些变量可以覆盖并且后面又被使用到。
由函数导致的变量覆盖比较好挖掘,只要搜寻以上的几个高危变量,然后回溯变量是否可控。
$REQUEST
解析与parse_str解析传入的数据有区别,parse_str会对数据进行解码,比如传入[a=1&b=2%26c=3]
,$REQUEST
解析为[a=1,b=2%26c=3]
,parse_str解析为[a=1,b=2,c=3]