CTF 中的变量覆盖考点
变量覆盖
定义
变量覆盖指的是可以用我们的传参值替换程序原有的变量值。
变量覆盖的场景
能造成变量覆盖的相关函数:
extract()
parse_str()
$$
mb_parse_str()
import_request_variables()
deprecated in >= PHP 5.4.0array_merge()
register_globals
相关函数的认识
extract
extract — 从数组中将变量导入到当前的符号表
参数 | 作用 |
---|---|
array | 一个关联数组,此函数会将数组的键名作为变量名,相应的值作为变量的值。 对每个键/值对都会在当前的符号表中建立变量 |
other | 其他参数请参考官方文档 |
详细请参考:https://www.php.net/manual/zh/function.extract.php
示例:
1 |
|
运行结果如下:
可以看到,extract
函数已经将关联数组中的相关键转化为变量,并将其键所相对应的值,作为相应变量的值。
案例1:
1 |
|
由于 $_GET
变量的值为一个关联数组,其值来自于 GET 传参,将 GET 传参的键名作为数组中的键名,将 GET 传参的值作为数组中相应键的值。
故而直接传入 ?flag=true(注意这里传入任意字符串都可以,只要不为空,因为这里会发生类型转换) ,接着通过 extract() 函数就会将其转为相应的变量和值,从而覆盖原有 $flag 变量的值。
案例2:
1 |
|
我们需要将 $flag
变量的值 覆盖为 array('name'=>'admin')
,在 GET 请求中,我们是可以传入索引数组和关联数组的。可参考:GET传入索引或关联数组
parse_str
parse_str — 将字符串解析成多个变量
参数 | 作用 |
---|---|
string | 输入的字符串 |
result | 如果设置了第二个参数 result , 变量将会以数组元素的形式存入到这个数组,作为替代。 |
详细请参考:https://www.php.net/manual/zh/function.parse-str
示例1:
1 |
|
注意: 该函数接收的是一个字符串,故而不能直接通过 GET 传入 一般配合 $_SERVER['QUERY_STRING']
全局变量使用。
程序运行结果:
示例2:
当设置了 result
参数之后,则会将 参数和值 作为关联数组,存到 result 参数定义的变量中。并不会直接创建 name、age、address 变量。
警告:极度不建议在没有
result
参数的情况下使用此函数, 并且在 PHP 7.2 中将废弃不设置参数的行为。PHP 8.0.0 起,result
参数是强制的。
案例1:
1 |
|
$$
$$
产生的漏洞主要是因为foreach
遍历数组的值,然后将获取的数组键名作为变量,数组中的值作为变量的值。
foreach
循环只适用于数组,并用于遍历数组中的每个 键/值 对。
1 |
|
$$
符号在 php 中叫做可变变量
,可以使变量名动态设置。举个例子:
1 |
|
可以看到在这里${$a}
等同于$hello
,接着我们再来看怎么来进行变量覆盖:
案例1:
1 | $auth=0; |
案例2:
1 |
|
foreach (array('_COOKIE', '_POST', '_GET') as $_request) {
上述代码依次遍历赋值给 $_request
,接着 $$_request
就依次等于 $_COOKIE
、$_GET
、$_POST
。
1 | foreach ($$_request as $_key => $_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 |
|
具体参考:http://php.adamharvey.name/manual/zh/function.import-request-variables.php
例题-1
1 |
|
关键代码:
1 | if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5(QNKCDZO)) { |
我们将 QNKCDZO
进行 MD5 加密之后,发现得到 一个 0e开头 后面全数字的值,并且 第二个条件使用的是 ==
因此存在弱类型比较,我们只需要让 $a
数组索引为 0 的值 为 s878926199a
或 其他符合要求的即可。
但是由于 $a 变量的值固定为 TESTCTF
但是由于 parse_str
存在,且在 $a 变量定义后调用,其参数 id 我们可控,故而我们只需要将 id 的值 设置为关联数组 即可实现变量覆盖。
构造 PAYLOAD:
1 | ?id=a[]=s878926199a |
例题-2
该例题来自于:ISCC 2019 web4
1 |
|
通过代码审计我们可以得知,题目考点是变量覆盖,通过 parse_str()
函数可实现变量覆盖。
这里需要注意的是,parse_str()
函数是没有返回值的,故而变量 $parsed_query
的值为 NULL:
1 | $parsed_query = parse_str($query); |
接着我们通过 GET 请求传入 action=auth
经过 parse_str()
函数处理之后,$action
变量的值为 auth
,从而下列条件成立:
1 | if($action==="auth"){ |
但是这里我们有个条件我们通过 GET 请求传入的 key 经过 sha256
要与 $hashed_key
变量的值一致,由于 $hashed_key
是密文,我们无法进行解密,故而此路应该是不通的。
但是由于存在 $hashed_key
变量在 parse_str
函数之前就定义了,同时 parse_str
函数的参数我们可控。于是可以 覆盖掉 $hashed_key
的值。
构造 PAYLOAD:
1 | ?action=auth&key=x1ongsec&hashed_key=72bf62a54f52b67c16cdab542d8dbe97018bf67e94aab3e277ca2eb99325cf8f |
例题-3
例题来自于:[DASCTF 2020圣诞赛] WEB-easyphp
1 |
|
这里我们先分析一下代码的功能,首先看如下代码:
源码原本实现了一个模板渲染的功能,但是加入了不相干的代码(上图中的代码块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 |
|
这里我们可以利用 phar 反序列化进行 RCE,由于现在是对变量覆盖进行总结,故而暂不加入其他知识点,后续在反序列化总结中,我们在来看这道题。