1.前言
本文章主要是PHP代码审计的一些基础知识,包括函数的用法,漏洞点,偏向基础部分
1.1代码执行
代码执行是代码审计当中较为严重的漏洞,主要是一些命令执行函数的不适当使用。那么,常见的能够触发这类漏洞的函数有哪些呢?
eval()函数
eval() 函数就是将传入的字符串当作 PHP 代码来进行执行。
eval( string $code) : mixed
返回值
eval() 返回 NULL ,除非在执行的代码中 return了一个值,函数返回传递给 return的值。PHP7开始,执行的代码里如果有一个parse error,eval() 会抛出 ParseError 异常。在 PHP 7 之前,如果在执行的代码中有 parse error,eval() 返回FALSE ,之后的代码将正常执行。无法使用set_error_handler() 捕获 eval() 中的解析错误。
也就是说,我们在利用eval() 函数的时候,如果我们传入的字符串不是正常的代码格式,那么就会抛出异常。所以PHP7和PHP5在这部分最大的不同是什么呢?简而言之,PHP5在代码错误格式错误之后仍会执行,而PHP7在代码发生错误之后,那么eval() 函数就会抛出异常,而不执行之后的代码。
示例:
<?php
$code = "echo 'This is a PHP7';";
eval($code);
?>
执行结果——>This is a PHP7
执行系统命令就需要用到PHP中的system 函数。
<?php
$code = "system('whoami');";
eval($code);
?>
执行结果——>desktop-jhtfasu\666
我们就可以结合其他姿势通过这个函数实现任意代码执行了。
assert()
PHP 5
assert( mixed $assertion[, string $description] ) : bool
PHP 7
assert( mixed $assertion[, Throwable $exception] ) : bool
参数
- assertion
断言。在PHP 5 中,是一个用于执行的字符串或者用于测试的布尔值。在PHP 7 中,可以是一个返回任何值的表达式,它将被执行结果用于判断断言是否成功。 - description
如果assertion 失败了,选项description 将会包含在失败信息里。 - exception
在PHP 7中,第二个参数可以是一个Throwable 对象,而不是一个字符串,如果断言失败且启用了assert.exception ,那么该对象将被抛出
assert()配置
配置项 | 默认值 | 可选值 |
---|
zend.assertions | 1 | 1 - 生成和执行代码(开发模式) 0 - 生成代码,但在执行时跳过它 -1 - 不生成代码(生产环境) | assert.exception | 0 | 1 - 断言失败时抛出,可以抛出异常对象,如果没有提供异常,则抛出AssertionError对象实例 0 - 使用或生成Throwable,仅仅是基于对象生成的警告而不是抛出对象(与PHP 5 兼容) |
assert() 函数到底是干什么的呢?其实assert() 函数是处理异常的一种形式,相当于一个if条件语句的宏定义一样。
一个PHP 7 中的示例
<?php
assert_options(ASSERT_EXCEPTION, 1);
try {
assert(1 == 2, new AssertionError('因为1不等于2,所以前面断言失败,抛出异常'));
} catch (Throwable $error) {
echo $error->getMessage();
}
?>
执行结果——>因为1不等于2,所以前面断言失败,抛出异常
这里就是实例化一个对象,用这个对象来抛出异常。
一个php 5 中的示例
<?php
assert(1 == 2,'前面断言失败,抛出异常');
?>
执行结果——>Warning: assert(): 前面断言失败,抛出异常 failed in D:\phpstudy_pro\WWW\1.php on line 2
<?php
assert(1 == 2);
?>
执行结果——>Warning: assert(): Assertion failed in D:\phpstudy_pro\WWW\1.php on line 2
所以PHP 7 相较于PHP 5 就是多了个用Throwable来发出警告。
那么,如果前面断言成功呢?会发生什么呢?来个最简单,也是我们比较喜欢的示例
<?php
$code = "system(whoami)"
assert($code);
?>
执行结果——>desktop-jhtfasu\666
这段代码在PHP 5 和PHP 7 中都会返回命令执行结果,虽然PHP 7 中对断言函数的参数稍作了改变,但是为了兼容低版本,所以还是会直接返回结果。
preg_replace()
通过函数名字我们也应该能够了解函数大概作用,此函数执行一个正则表达式的搜索和替换。
mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )
搜索 subject 中匹配 pattern 的部分, 以 replacement 进行替换。
参数说明:
- $pattern: 要搜索的模式,可以是字符串或一个字符串数组。
- $replacement: 用于替换的字符串或字符串数组。
- $subject: 要搜索替换的目标字符串或字符串数组。
- $limit: 可选,对于每个模式用于每个 subject 字符串的最大可替换次数。 默认是-1(无限制)。
- $count: 可选,为替换执行的次数。
那这个函数跟我们命令执行有什么关系呢?仅仅看上面的官方解释似乎看不出什么,但是preg_repace()有一个模式是/e模式,这个模式就会发生代码执行的问题,为什么呢? 看一个案例
<?php
function Ameng($regex, $value){
return preg_replace('/(' . $regex . ')/ei', 'strtolower("\\1")', $value);
}
foreach ($_GET as $regex => $value){
echo Ameng($regex, $value) . "\n";
}
?
上面这段我们需要注意的就是\1,\1在正则表达式是反向引用的意思,简而言之就是指定一个子匹配项。
针对上面案例,我们来个payload:
payload=/?.*={${phpinfo()}}
所以语句就成了这样
preg_replace('/(.*)/ei', 'strtolower("\\1")', {${phpinfo()}});
那么我们直接把这段代码放到页面
<?php
preg_replace('/(.*)/ei', 'strtolower("\\1")', '{${phpinfo()}}');
?>
访问页面,结果如下:  我们看到成功执行了代码。
但是这里我是直接将这段代码写到了文件里,那么如果我们是通过GET传参得到参数,这里针对上面那个案例就需要注意一点,在通过GET传参时,.* 会被替换为_* 导致我们要的正则被替换了,达不到我们的效果,所以这里可用使用一些其他的正则表达式来达到目的,比如通过GET传参时我们的参数可以传入\S* 从而达到同样目的。所以以后再遇到这个函数的时候,要留个心眼了。不过,这里要补充一点,就是preg_replace() 函数在PHP 7 后便不再支持,使用preg_replace_callback() 进行替换了,取消了不安全的\e模式。
create_function() 函数
create_function() 用来创建一个匿名函数
create_function( string $args, string $code) : string
参数
- string $args 声明的函数变量部分
- string $code 要执行的代码
返回值 返回唯一的函数名称作为字符串或者返回FALSE错误
create_function() 函数在内部执行eval() 函数,所以我们就可以利用这一点,来执行代码。当然正因为存在安全问题,所以在PHP 7.2 之后的版本中已经废弃了create_function() 函数,使用匿名函数来代替。所以这里为了演示这个函数,我采用的是PHP 5 的环境。那么这个函数到底怎么用呢?
那么来看一个简单的案例
<?php
$onefunc = create_function('$a','return system($a);');
$onefunc(whoami);
?>
执行结果——>desktop-jhtfasu\666
我们看到使用此函数为我们相当于创造了一个匿名的函数,给它赋以相应的变量,就执行了我们要执行的代码。
那么接下来我们再来看一个简单的案例
<?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));
?>
这个主要功能就是实现排序,这段代码就调用了create_function() 函数,那么我们能否利用这个函数执行我们想要执行的代码呢?
当然可以,我们只需要在传参时将前面的符号闭合,然后输入我们想要执行的代码即可。
payload='"]);}phpinfo();/*
执行payload前:$sort_function = ' return 1 * ' . $sorter . '($a["' . $sort_by . '"], $b["' . $sort_by . '"]);';
执行payloda后:$sort_function = ' return 1 * ' . $sorter . '($a["' . $sort_by '"]);}phpinfo();/*
看到这里,你可能会有稍微疑惑,为什么后面多了个;} ,不知道你是否想到了这一点?
那么我就来分析一下这个,上面的那段执行代码,实际上就是一个匿名函数的创建,既然是一个函数,注意是一个函数,那么你觉得有没有花括号呢?看我如下代码
<?php
function sort($a,$b){
' return 1 * ' . $sorter . '($a["' . $sort_by . '"], $b["' . $sort_by . '"]);';
}
function sort($a,$b){
' return 1 * ' . $sorter . '($a["' . $sort_by '"]);
}
phpinfo();/*
}
?>
可以看到,我们借用了匿名函数的位置,插入了我们要执行的代码,然后等这个匿名函数被create_function 当作$code 执行的时候,代码就被执行了。
那么creat_function 函数还有别的用法吗?我们将上面一个案例简单的修改一下,代码如下:
<?php
$onefunc = create_function("","die(`cat flag.php`)");
$_GET['func_name']();
die();
?>
代码简单的来看,我们只需要执行$onefunc 就能得到flag,但是我们不知道这个函数的名称。如果在不知道函数名称的情况下执行函数呢?这里就用到了creat_function 函数的一个漏洞。这个函数在creat之后会自动生成一个函数名为%00lambda_%d 的匿名函数。%d 的值是一直递增的,会一直递增到最大长度直到结束。所以这里可以通过多进程或者多线程访问,从而看到flag。
所以,以后再代码中如果看到调用create_function() 要小心一点,但是如果是CTF题目的话,不会这么直接就吧这个函数暴露给你,它可能会用到拼接或者替换来构造这个函数。最后再强调一下,create_function 函数在PHP 7.2 版本之后就已经被废弃了。
array_map()
array_map() 为数组的每个元素应用回调函数
array_map( callable $callback, array $array1[, array $...] ) : array
array_map():返回数组,是为 array1 每个元素应用 callback 函数之后的数组。callback 函数形参的数量和传给array_map() 数组数量,两者必须一样。
参数
- callback:回调函数,应用到每个数组里的每个元素。
- array1:数组,遍历运行callback函数。
- …:数组列表,每个都遍历运行callback函数。
返回值 返回数组,包含callback函数处理之后array1的所有元素。
那么这个函数到底如何使用呢?简而言之,这个函数的作用可以这么直白的解释一下。你本来有一个数组,然后我通过array_map函数将你这个数组当作参数传入,然后返回一个新的数组。见下图。  代码示例:
<?php
$old_array = array(1, 2, 3, 4, 5);
function func($arg){
return $arg * $arg;
}
$new_array = array_map('func',$old_array);
var_dump($new_array);
?>
执行结果——>
array(5) {
[0]=>
int(1)
[1]=>
int(4)
[2]=>
int(9)
[3]=>
int(16)
[4]=>
int(25)
}
通过上述代码,我们大概知道这个函数就是调用回调函数(用户自定义的函数)来实现对现有数组的操作,从而得到一个新的数组。
那么功能我知道了,可是这个和代码执行有什么关系呢?如何能够利用这个函数执行代码呢?且看下面所示代码。
<?php
$func = 'system';
$cmd = 'whoami';
$old_array[0] = $cmd;
$new_array = array_map($func,$old_array);
var_dump($new_array);
?>
执行结果——>
desktop-jhtfasu\666
array(1) {
[0]=>
string(21) "desktop-jhtfasu\666"
}
这段代码就是,通过array_map() 这个函数,来调用用户自定义的函数,而用户这里的回调函数其实就是system 函数,那么就相当于我们用system 函数来对旧数组进行操作,得到新的数组,那么这个新的数组的结果就是我们想要的命令执行的结果了。
call_user_func()
call_user_func() 是把第一个参数作为回调函数调用
call_user_func( callable $callback[, mixed $parameter[, mixed $...]] ) : mixed
参数 第一个参数callback是被调用的回调函数,其余参数是回调函数的参数。
- callback:即将被调用的回调函数
- parameter:传入回调函数的参数
这个函数还是非常好理解的,看一段简单的示例代码
<?php
function callback($a,$b){
echo $a . "\n";
echo $b;
}
call_user_func('callback','我是参数1','我是参数2');
?>
执行结果——>
我是参数1
我是参数2
可以看到此函数作用就是调用了自定义的函数。那么这个如何实现代码执行呢?在前面自定义的函数中加入能执行命令的代码就可以代码执行了。
示例代码:
<?php
function callback($a){
return system($a);
}
$cmd = 'whoami';
call_user_func('callback',$cmd);
?>
执行结果——>
desktop-jhtfasu\666
call_user_func_array()
这个函数名称跟上没什么大的差别,唯一的区别就在于参数的传递上,这个函数是把一个数组作为回调函数的参数
call_user_func_array( callable $callback, array $param_arr) : mixed
参数
- callback:被调用的回调函数
- param_arr:要被传入回调函数的数组,这个数组需要是索引数组
示例代码
<?php
function callback($a,$b){
echo $a . "\n";
echo $b;
}
$onearray = array('我是参数1','我是参数2');
call_user_func_array('callback',$onearray);
?>
利用也很简单
<?php
function callback($a){
return system($a);
}
$cmd = array('whoami');
call_user_func_array('callback',$cmd);
?>
执行结果——>
desktop-jhtfasu\666
array_filter()
用回调函数过滤数数组中的单元
array_filter( array $array[, callable $callback[, int $flag = 0]] ) : array
依次将array 数组中的每个值传到callback 函数。如果callbac k函数返回true ,则array 数组的当前值会被包含在返回的结果数组中。数组的键名保留不变。
参数
- array:要循环的数组
- callback:使用的回调函数。如果没有提供callback函数,将删除array中所有等值为FALSE的条目。
- flag:决定callback接收的参数形式
代码示例(这里看官方的,很详细):
<?php
function odd($var)
{
return($var & 1);
}
function even($var)
{
return(!($var & 1));
}
$array1 = array("a"=>1, "b"=>2, "c"=>3, "d"=>4, "e"=>5);
$array2 = array(6, 7, 8, 9, 10, 11, 12);
echo "Odd :\n";
print_r(array_filter($array1, "odd"));
echo "Even:\n";
print_r(array_filter($array2, "even"));
?>
执行结果——>
Odd :
Array
(
[a] => 1
[c] => 3
[e] => 5
)
Even:
Array
(
[0] => 6
[2] => 8
[4] => 10
[6] => 12
)
从上面代码我们知道,这个函数作用其实就是过滤,只不过这个过滤调用的是函数,而被过滤的是传入的参数。到这里你心里有没有代码执行的雏形了?
代码示例:
<?php
$cmd='whoami';
$array1=array($cmd);
$func ='system';
array_filter($array1,$func);
?>
执行结果——>
desktop-jhtfasu\666
usort()
使用用户自定义的比较函数对数组中的值进行排序
usort( array &$array, callable $value_compare_func) : bool
参数
- array:输入的数组
- cmp_function:在第一个参数小于、等于或大于第二个参数时,该比较函数必须相应地返回一个小于、等于或大于0的数
代码示例:
<?php
function func($a,$b){
return ($a<$b)?1:-1;
}
$onearray=array(1,3,2,5,9);
usort($onearray, 'func');
print_r($onearray);
?>
执行结果——>
Array
(
[0] => 9
[1] => 5
[2] => 3
[3] => 2
[4] => 1
)
可见实现了逆序的功能。那么倘若我们把回调函数设计成能够执行代码的函数,是不是就可以执行我们想要的代码了呢?
代码示例:
?php
usort(...$_GET);
?>
payload: 1.php?1[0]=0&1[1]=eval($_POST['x'])&2=assert
POST传参: x=phpinfo();
usort 的参数通过GET传参,第一个参数也就是$_GET[0] ,随便传入一个数字即可。第二个参数也就是$_GET[1] 是我们要调用的函数名称,这里采用的是assert 函数。
执行结果: 
uasort()
这个跟上一个差不多,区别不是很大。此函数对数组排序并保持索引和单元之间的关联。也就是说你这个排完序之后呢,它原来对应的索引也会相应改变,类似于“绑定”。
uasort( array &$array, callable $value_compare_func) : bool
参数
- array:输入的数组
- value_compare_func:用户自定义的函数
这里用的仍然官方例子(比较好理解)
<?php
function cmp($a, $b) {
if ($a == $b) {
return 0;
}
return ($a < $b) ? -1 : 1;
}
$array = array('a' => 4, 'b' => 8, 'c' => -1, 'd' => -9, 'e' => 2, 'f' => 5, 'g' => 3, 'h' => -4);
print_r($array);
uasort($array, 'cmp');
print
?>
执行结果——>
Array
(
[a] => 4
[b] => 8
[c] => -1
[d] => -9
[e] => 2
[f] => 5
[g] => 3
[h] => -4
)
Array
(
[d] => -9
[h] => -4
[c] => -1
[e] => 2
[g] => 3
[a] => 4
[f] => 5
[b] => 8
)
我们发现,在排完序之后索引也跟着值的位置变化而变化了。那么代码执行的示例代码其实也和上一个差不多。
代码示例:
<?php
$a = $_GET['a'];
$onearray = array('Ameng', $_POST['x']);
uasort($onearray, $a);
?>
执行结果: 
小结
看完这里不知道你对代码审计中的代码执行部分是否有另一种想法?我的想法就是这个是和后门联系在一起的。我们可以看到很多函数都具有构造执行命令的条件,而且其中很多函数也的确被用在后门中,特别像后面几个回调函数,在后门中更是常见。当然这些后门函数也早已被安全厂商盯住,所以大部分已经无法直接免杀,所以想要免杀就需要结合其他姿势,比如替换、拼接、加密等等。但是这些知识在CTF中还是比较容易出现的。
|