变量覆盖 定义 变量覆盖指的是可以用我们的传参值替换程序原有的变量值 。
变量覆盖的场景 能造成变量覆盖的相关函数:
extract()
parse_str()
$$
mb_parse_str()
import_request_variables() deprecated in >= PHP 5.4.0
array_merge()
register_globals
相关函数的认识 extract — 从数组中将变量导入到当前的符号表
参数
作用
array
一个关联数组,此函数会将数组的键名作为变量名,相应的值作为变量的值。 对每个键/值对都会在当前的符号表中建立变量
other
其他参数请参考官方文档
详细请参考:https://www.php.net/manual/zh/function.extract.php
示例:
1 2 3 4 5 6 7 8 9 <?php $arr = array ('name' => 'x1ong' , 'age' => '20' , 'address' => 'Henan' );extract ($arr );echo ($name ) . "\n" ;echo ($age ) . "\n" ;echo ($address ) . "\n" ;?>
运行结果如下:
可以看到,extract 函数已经将关联数组中的相关键转化为变量,并将其键所相对应的值,作为相应变量的值。
案例1:
1 2 3 4 5 6 7 8 9 <?php $flag = false ;extract ($_GET );if ($flag ) { echo "success" ; } else { echo "fail" ; } ?>
由于 $_GET 变量的值为一个关联数组,其值来自于 GET 传参,将 GET 传参的键名作为数组中的键名,将 GET 传参的值作为数组中相应键的值。
故而直接传入 ?flag=true(注意这里传入任意字符串都可以,只要不为空,因为这里会发生类型转换) ,接着通过 extract() 函数就会将其转为相应的变量和值,从而覆盖原有 $flag 变量的值。
案例2:
1 2 3 4 5 6 7 8 9 <?php $flag = array ('name' =>'guest' );extract ($_GET );if ($flag ['name' ] == 'admin' ) { echo "admin" ; } else { echo "not admin" ; } ?>
我们需要将 $flag 变量的值 覆盖为 array('name'=>'admin') ,在 GET 请求中,我们是可以传入索引数组和关联数组的。可参考:GET传入索引或关联数组
parse_str parse_str — 将字符串解析成多个变量
参数
作用
string
输入的字符串
result
如果设置了第二个参数 result, 变量将会以数组元素的形式存入到这个数组,作为替代。
详细请参考:https://www.php.net/manual/zh/function.parse-str
示例1:
1 2 3 4 5 6 7 <?php $str = 'name=x1ong&age=20&address=Henan' ;parse_str ($str );echo $name . "\n" ;echo $age . "\n" ;echo $address . "\n" ;?>
注意: 该函数接收的是一个字符串,故而不能直接通过 GET 传入 一般配合 $_SERVER['QUERY_STRING'] 全局变量使用。
程序运行结果:
示例2:
当设置了 result 参数之后,则会将 参数和值 作为关联数组,存到 result 参数定义的变量中。并不会直接创建 name、age、address 变量。
警告:极度不建议 在没有 result 参数的情况下使用此函数, 并且在 PHP 7.2 中将废弃 不设置参数的行为。PHP 8.0.0 起,result 参数是强制的 。
案例1:
1 2 3 4 5 6 7 8 9 10 <?php $flag = false ;$query = $_SERVER ['QUERY_STRING' ];parse_str ($query )if ($flag ) { echo "success" ; } else { echo "fail" ; } ?>
$$ $$ 产生的漏洞主要是因为foreach遍历数组的值,然后将获取的数组键名作为变量,数组中的值作为变量的值。
foreach循环只适用于数组,并用于遍历数组中的每个 键/值 对。
1 2 3 4 5 6 7 <?php $arr = array ('name' => 'x1ong' ,'age' => 18 , 'address' => 'Henan' );foreach ($arr as $key => $value ) { echo $value . "\n" ; } ?>
$$符号在 php 中叫做可变变量,可以使变量名动态设置。举个例子:
1 2 3 4 5 6 <?php $a ='hello' ;$$a ='world' ;echo "$a ${$a} " ; echo "$a $hello " ; ?>
可以看到在这里${$a}等同于$hello,接着我们再来看怎么来进行变量覆盖:
案例1:
1 2 3 4 5 $auth =0 ;foreach ($_GET as $key => $value ) { $$key =$value ; } echo $auth ;
案例2:
1 2 3 4 5 6 7 8 <?php $a = 1 ;foreach (array ('_COOKIE' , '_POST' , '_GET' ) as $_request ) { foreach ($$_request as $_key => $_value ) { $$_key = addslashes ($_value ); } } echo $a ;
foreach (array('_COOKIE', '_POST', '_GET') as $_request) {
上述代码依次遍历赋值给 $_request,接着 $$_request 就依次等于 $_COOKIE、$_GET、$_POST 。
1 2 3 foreach ($$_request as $_key => $_value ) { $$_key = addslashes ($_value ); }
以上代码就会遍历来自$_COOKIE、$_GET、$_POST 传入的值,并将参数名作为变量( $$_key 的作用) ,参数值作为变量的值。
因此我们传入 ?a=2 则会将变量 a 的值 重新赋值为 2。
mb_parse_str mb_parse_str — 解析 GET/POST/COOKIE 数据并设置全局变量
参数
作用
string
URL 编码过的数据
result
一个 array,包含解码过的、编码转换后的值。
具体用法与 mb_parse_str 函数基本一致。
import_request_variables
该函数从 php5.3 开始就被废弃了。因此只做了解
1 2 3 4 5 6 7 8 <?php import_request_variables ("p" , "post_" );import_request_variables ("gp" , "gp_" );import_request_variables ("cg" , "cg_" );?>
具体参考:http://php.adamharvey.name/manual/zh/function.import-request-variables.php
例题-1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php if (!isset ($_GET ['id' ])) { show_source (__FILE__ ); die ; } include_once ("flag.php" );$a = "TESTCTF" ;$id = $_GET ['id' ];@parse_str ($id ); if ($a [0 ] != 'QNKCDZO' && md5 ($a [0 ]) == md5 (QNKCDZO)) { die ($flag ); } else { die ("emmm" ); } ?>
关键代码:
1 2 3 if ($a [0 ] != 'QNKCDZO' && md5 ($a [0 ]) == md5 (QNKCDZO)) { die ($flag ); }
我们将 QNKCDZO 进行 MD5 加密之后,发现得到 一个 0e开头 后面全数字的值,并且 第二个条件使用的是 == 因此存在弱类型比较,我们只需要让 $a 数组索引为 0 的值 为 s878926199a 或 其他符合要求的即可。
但是由于 $a 变量的值固定为 TESTCTF 但是由于 parse_str 存在,且在 $a 变量定义后调用,其参数 id 我们可控,故而我们只需要将 id 的值 设置为关联数组 即可实现变量覆盖。
构造 PAYLOAD:
例题-2 该例题来自于:ISCC 2019 web4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php error_reporting (0 ); include ("flag.php" ); $hashed_key = 'ddbafb4eb89e218701472d3f6c087fdf7119dfdd560f9d1fcbe7482b0feea05a' ; $parsed = parse_url ($_SERVER ['REQUEST_URI' ]); if (isset ($parsed ["query" ])){ $query = $parsed ["query" ]; $parsed_query = parse_str ($query ); if ($parsed_query !=NULL ){ $action = $parsed_query ['action' ]; } if ($action ==="auth" ){ $key = $_GET ["key" ]; $hashed_input = hash ('sha256' , $key ); if ($hashed_input !==$hashed_key ){ die ("no" ); } echo $flag ; } }else { show_source (__FILE__ ); }?>
通过代码审计我们可以得知,题目考点是变量覆盖,通过 parse_str() 函数可实现变量覆盖。
这里需要注意的是,parse_str() 函数是没有返回值的,故而变量 $parsed_query 的值为 NULL:
1 2 3 4 $parsed_query = parse_str ($query ); if ($parsed_query !=NULL ){ $action = $parsed_query ['action' ]; }
接着我们通过 GET 请求传入 action=auth 经过 parse_str() 函数处理之后,$action 变量的值为 auth,从而下列条件成立:
1 2 3 4 5 6 7 if ($action ==="auth" ){ $key = $_GET ["key" ]; $hashed_input = hash ('sha256' , $key ); if ($hashed_input !==$hashed_key ){ die ("no" ); } echo $flag ;
但是这里我们有个条件我们通过 GET 请求传入的 key 经过 sha256 要与 $hashed_key 变量的值一致,由于 $hashed_key 是密文,我们无法进行解密,故而此路应该是不通的。
但是由于存在 $hashed_key 变量在 parse_str 函数之前就定义了,同时 parse_str 函数的参数我们可控。于是可以 覆盖掉 $hashed_key 的值。
构造 PAYLOAD:
1 ?action=auth&key=x1ongsec&hashed_key=72 bf62a54f52b67c16cdab542d8dbe97018bf67e94aab3e277ca2eb99325cf8f
例题-3 例题来自于:[DASCTF 2020圣诞赛] WEB-easyphp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <?php error_reporting (E_ALL);$sandbox = '/var/www/html/uploads/' . md5 ($_SERVER ['REMOTE_ADDR' ]);if (!is_dir ($sandbox )) { mkdir ($sandbox ); } include_once ('template.php' );$template = array ('tp1' =>'tp1.tpl' ,'tp2' =>'tp2.tpl' ,'tp3' =>'tp3.tpl' );if (isset ($_GET ['var' ]) && is_array ($_GET ['var' ])) { extract ($_GET ['var' ], EXTR_OVERWRITE); } else { highlight_file (__file__); die (); } if (isset ($_GET ['tp' ])) { $tp = $_GET ['tp' ]; if (array_key_exists ($tp , $template ) === FALSE ) { echo "No! You only have 3 template to reader" ; die (); } $content = file_get_contents ($template [$tp ]); $temp = new Template ($content ); } else { echo "Please choice one template to reader" ; } ?>
这里我们先分析一下代码的功能,首先看如下代码:
源码原本实现了一个模板渲染的功能,但是加入了不相干的代码(上图中的代码块1)并在里面使用了 extract() 函数其参数我们可控,因此可能存在 变量覆盖。
在 CTF 比赛中,原本代码可以实现某个功能,但是加入了一些不相干的代码,往往这段代码就是解题的关键。
由于 $template 在 extract 函数之前被定义,故而可以进行值的覆盖。这里我们需要覆盖 $template 其值为数组类型。假设为:array('tp1'=>'/etc/passwd')。
构造 PAYLOAD:
1 ?var[template][tp1]=/etc/passwd
此时就会将 template 变量的值修改为 array('tp1'=>'/etc/passwd'),接着我们使用该模版进行渲染即可:
1 ?var [template][tp1]=/etc/passwd&tp=tp1
访问渲染之后的文件,内容则是 /etc/passwd,即可实现文件读取的效果。
最后我们读取 template.php 文件:
1 ?var [template][tp1]=template.php&tp=tp1
访问得到 template.php 的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <?php class Template { public $content ; public $pattern ; public $suffix ; public function __construct ($content ) { $this ->content = $content ; $this ->pattern = "/{{([a-z]+)}}/" ; $this ->suffix = ".html" ; } public function __destruct ( ) { $this ->render (); } public function render ( ) { while (True) { if (preg_match ($this ->pattern, $this ->content, $matches )!==1 ) break ; global ${$matches [1 ]}; if (isset (${$matches [1 ]})) { $this ->content = preg_replace ($this ->pattern, ${$matches [1 ]}, $this ->content); } else { break ; } } if (strlen ($this ->suffix)>5 ) { echo "error suffix" ; die (); } $filename = '/var/www/html/uploads/' . md5 ($_SERVER ['REMOTE_ADDR' ]) . "/" . md5 ($this ->content) . $this ->suffix; file_put_contents ($filename , $this ->content); echo "Your html file is in " . $filename ; } } ?>
这里我们可以利用 phar 反序列化进行 RCE,由于现在是对变量覆盖进行总结,故而暂不加入其他知识点,后续在反序列化总结中,我们在来看这道题。