PHP 反序列化
Drunkbaby Lv6

PHP 反序列化

0x01 前言

Java 基础的反序列化已经学了一些内容了,因为最近要打省赛,打算粗略的过一遍 PHP 反序列化吧,学习 PHP 也是给自己少点压力,学 Java 确实难。

0x02 PHP 的反序列化

  • 和 Java 一样,也是两个方法

序列化:serialize()
反序列化:unserialize()

简单的 demo

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

public function __construct()
{
echo 'Test ' .$this->name . ' is ' . $this->age . 'years old ';
}
}

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

echo PHP_EOL;

$b = serialize($a);

echo $b;

?>

PHP 反序列化其实是有点让人难受的,因为它的序列化过程,将对象转化为字符串,这个字符串并非是二进制文件,而且说实话,我个人觉得可读性有点不太好,看着不太舒服吧……

输出结果:

1
Test Drunkbaby is 19years old O:4:"Test":2:{s:3:"age";s:2:"19";s:4:"name";s:9:"Drunkbaby";}

此时就可以看出序列化后这里有多个字母,下面依次来进行解释

1
2
3
O:4:"Test":2:{s:3:"age";s:2:"19";s:4:"name";s:9:"Drunkbaby";}

对象类型:长度:类名:变量个数:{类型:长度:"值";类型:长度:"值";类型:长度:"值";类型:长度:"值";}

字母的含义如下

1
2
3
4
5
6
a - array                  b - boolean
d - double i - integer
o - common object r - reference
s - string C - custom object
O - class N - null
R - pointer reference U - unicode string

正常遇到的这种反序列化和序列化都是和 PHP 类与对象这部分知识点相关联的,所以我们需要简单了解类和对象这个知识点,同时掌握魔术方法的基础用法

0x03 PHP 魔术方法

  • 翻看自己以前的文章,发现已经有所提及到了,但是我自己却没什么印象哈哈,还是重新过一下吧,也要不了多少时间。

常见魔术方法有以下几种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__construct()   当一个对象创建时被调用,
__destruct() 当一个对象销毁时被调用,
__toString() 当一个对象被当作一个字符串被调用。
__wakeup() 使用unserialize时触发
__sleep() 使用serialize时触发
__destruct() 对象被销毁时触发
__call() 在对象上下文中调用不可访问的方法时触发
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据
__set() 用于将数据写入不可访问的属性
__isset() 在不可访问的属性上调用isset()或empty()触发
__unset() 在不可访问的属性上使用unset()时触发
__toString() 把类当作字符串使用时触发,返回值需要为字符串
__invoke() 当脚本尝试将对象调用为函数时触发

这是整体的,但这样看似乎显得过于抽象,因此我们将其进行分类,依次进行举例讲解

__construct() 与 __destruct()

1
__construct : 在创建对象时候初始化对象,一般用于对变量赋初值。 __destruct : 和构造函数相反,当对象所在函数调用完毕后执行。

__destruct 还有一种利用方式,就是 __destruct() 在对象被主动销毁的时候,其实这里用销毁这个词我觉得并不妥当,严格意义上来说,其实是对象在结束整个过程后,最后进行的这么一个操作。

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
<?php
class bai{
public $name;
public $age;
public function __construct($name,$age)
{
echo "__construct()初始化
";
$this->name=$name;//将传入的第一个参数赋值给name变量
$this->age=$age;
}
public function __destruct()
{
echo "__destruct()执行结束
";
}
}
//主动销毁
$a=new bai('quan9i',19);
unset ($a);//主动销毁对象,此时先触发destruct魔法函数再echo
echo"777
";
echo "------------分隔符----------------
";
//自动销毁
$b=new bai('quan9i',19);
echo "123
";
//此时先echo再触发destruct函数
?>

__sleep()

1
__sleep()    serialize 之前被调用,可以指定要序列化的对象属性。

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class bai{
public $name;
public $age;
public function __construct($name,$age)
{
echo "__construct()初始化
";
$this->name=$name;
$this->age=$age;
}
public function __sleep()
{
echo "当使用serialize时触发此方法
";
return array('name','age');
}
}
$a=new bai('Drunkbaby',19);
echo serialize($a);
?>

__wakeup()

1
__wakeup()   反序列化恢复对象之前调用该方法

实例如下

实例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class bai{
public $name;
public $age;
public function __construct($name,$age)
{
echo "__construct()初始化<br>
";
$this->name=$name;
$this->age=$age;
}
public function __wakeup()
{
echo "当使用unserialize时触发此方法<br>
";
$this->age=1000;//更改$age的值为1000
}
}
$a=new bai('Drunkbabye',19);
$b= serialize($a);
var_dump(unserialize($b));
?>

__toString()

1
__toString() :在对象当做字符串的时候会被调用。

实例如下

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 = 'This is a string';

public function good(){
echo $this->a.'
';
}

// 在对象当做字符串的时候会被调用
public function __toString()
{
return '__toString方法被调用
';
}
}

$a = new Test();
$a->good();
echo $a;
?>

__invoke()

1
__invoke() :将对象当作函数来使用时执行此方法。

示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

class Test{
public $data ="调用正常方法
";

public function __invoke()
{
echo"调用__invoke()方法";
}
}
$a= new test();
echo $a();
?>
  • 这一个 __invoke 方法接触的比较少

__get

1
__get() 访问不存在的成员变量时调用的

实例如下

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

class Test {
public $n=123;
public function __get($name){
echo '__get 方法被调用,其中不存在变量 '.$name.'
';
}
}
$a = new Test();
echo $a->Drunkbaby;//调用对象a中的变量Drunkbaby,由于不存在Drunkbaby这个变量,这时候就会调用__get魔术方法
?>

__set

1
__set() :设置不存在的成员变量时调用的;

这一块 get 和 set 是可以放一起讲的,这两个方法很有意思,从某种程度来说是一种特别的抛出异常的手段。

如果我一个类当中不存在某个属性,如果在 Java 里面,会直接抛出异常,而在 PHP 里面会调用魔术方法,这其实是有概率造成 Gadget Chain 的反序列化的,不同于 Java 里面死的抛出异常,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
26
27
28
29
30
31
32
33
<?php

class xg{
public $data = 100;
// protected $test=0;

public function __set($name,$value){
echo '__set 不存在成员变量 '.$name.'
';
$this->test=$value;
}

public function __get($name){
echo '__get 方法被调用,其中不存在变量 '.$name.'
';
}

public function show(){
echo $this->test;
}
}

$a = new xg();
$a->show(); //调用Get方法,这里就是输出test的值
echo '
';
$a->test= 777; // 给私有变量test赋值为777,但此时它是私有变量,就会调用__set,此时就会更改
$a->show();
echo '
';
$a->Drunkbaby = 566;// 设置对象不存在的属性
$a->show();// 经过__set方法的设置值为566
?>

__call()

1
__call :当调用对象中不存在的方法会自动调用该方法

示例如下

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

class Test{
public $data ="调用正常方法
";
public function __call($name,$value){
echo "__call 被调用,Test 类中不存在方法 ".$name.'
';
var_dump($value);
}
public function show(){
echo $this->data;
}
}
$a= new test();
$a->show(); //调用正常方法
echo"--------------------分隔符----------------
";
$a->Drunkbaby_Be_Clever('Drunkbaby',123); //调用一个不存在的方法,此时就调用了__call魔术方法

?>

我的理解里面,这也算是一种抛出异常

__isset()

1
__isset() : 检测对象的某个属性是否存在时执行此函数。

当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。

实例如下

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

class test{
public $name;
private $age;

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

// __isset():当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。
public function __isset($content){
echo "当使用isset()函数,自动调用<br>
";
return isset($this->$content);
}
}

$a = new test("Drunkbaby", 19);
// public 成员
echo ($a->name),"<br>
";
// private 成员
echo isset($a->name);
echo "-----------分隔符--------------<br>
";
echo isset($a->age);
?>

可以发现私有属性时会调用 isset 魔术方法(调用 protected 作用域的属性也会调用),这两个就属于是不可访问的属性了,被访问的时候会进到 __isset() 里面

__unset()

1
__unset() :在不可访问的属性上使用 unset () 时触发

代码如下

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
<?php

class test{
public $name;
private $age;

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


public function __unset($content){
echo "当使用unset()函数,自动调用
";
echo "Drunkbaby is silly";
}
}

$a = new test("Drunkbaby", 19);
// public 成员
unset($a->name);
echo "
";
// private 成员
echo "-----------分隔符--------------
";
unset($a->age);
echo "
";
?>

常用魔术函数汇总例子

代码如下

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
<?php
class test{
public $varr1="abc";
public $varr2="123";
public function echoP(){
echo $this->varr1."
";
}
public function __construct(){
echo "__construct
";
}
public function __destruct(){
echo "__destruct
";
}
public function __toString(){
return "__toString
";
}
public function __sleep(){
echo "__sleep
";
return array('varr1','varr2');
}
public function __wakeup(){
echo "__wakeup
";
}
}

$obj = new test(); //实例化对象,调用__construct()方法,输出__construct
$obj->echoP(); //调用echoP()方法,输出"abc"
echo $obj; //obj对象被当做字符串输出,调用__toString()方法,输出__toString
$s =serialize($obj); //obj对象被序列化,调用__sleep()方法,输出__sleep
echo $s.'
';//sleep返回的信息此时被输出
echo unserialize($s); //$s首先会被反序列化,会调用__wake()方法,被反序列化出来的对象又被当做字符串,就会调用_toString()方法。后面这个是一个对象,因此会执行一次__destruct方法
// 结束又会调用__destruct()方法,输出__destruct
?>

简单的 PHP 反序列化 CTF 题目

0x04 反序列化之字符串逃逸

其实也不能说算是字符串逃逸吧,个人认为说是字符串拼接,或许是更合理的。逃逸总是听着太高大上了

首先来介绍一下这个 str_replace 函数

1
2
3
4
5
str_replace 
str_replace — 字符串替换
说明
mixed str_replace( mixed $search, mixed $replace, mixed $subject[, int &$count] )
该函数返回一个字符串或者数组。该字符串或数组是将 subject 中全部的 search 都被 replace 替换之后的结果。

过滤后字符变多

看一下我们本地的代码

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

function change($str){
return str_replace("x","xx",$str);
}

$name = $_GET['name'];
$age = "I am 11";
$arr = array($name,$age);
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
$new = unserialize($old);
var_dump($new);
echo "<br/>";
echo "<br/>此时,age=$new[1]";

正常情况下是这样的

如果此时多传入一个x的话会怎样,毫无疑问反序列化失败,由于溢出(s本来是4结果多了一个字符出来,这也就是 str_replace)

传入的参数 name,先与 age 一起组成了一个数组,经过过滤之后,再进行反序列化的工作,所以这里我们可以构造恶意的 name,此时我们传入

1
name=Drunkbabyxxxxxxxxxxxxxxxxxxxx";i:1;s:6:"whoami";}

后面这部分:";i:1;s:6:"whoami";} 的长度是 20

由于一个x会被替换为两个,我们输入了一共20个x,现在是40个,多出来的20个x其实取代了我们的这二十个字符 ";i:1;s:6:"whoami";},从而造成";i:1;s:6:"whoami";} 的溢出,而 " 闭合了前串,使得我们的字符串成功逃逸,可以被反序列化,输出 whoami

最后的 ;} 闭合反序列化全过程导致原来的 ";i:1;s:7:"I am 11";}" 被舍弃,不影响反序列化过程

构造 payload 之后成功

bugku Web new php

0CTF 2016 piapiapia 1 字符串逃逸后变多

题目是有源码泄露的,得到源码之后我们看到 config.php 里面有一个 $flag

从这里面就可以猜想,可能是要我们去读取 config.php

继续看,在 profile.php 里面,很明显可以看到存在一个危险函数 file_get_contents,这就更加容易使我们去想到是读取 config.php 的了。

做的 file_get_contents 里面是什么东西呢,是 $profile['photo'],我们溯上去,看一看 $profile 是什么

可以看到 $profile$user 对象中的 show_profile() 方法,传入的参数是 $_SESSION['username']

跟进一下 show_profile() 方法,先看返回值,是 $object->profile,也就是返回一个 profile 对象。$object 则是通过 parent::select($this->table, $where),也就是经过了 select 搞来的,再往上看,发现还调用了一个 filter() 方法,跟进去看一下。

字符串逃逸昭然若揭。

现在基本的攻击思路已经明确了,基本上是依靠 username 这里的可控点进行字符串逃逸的攻击,可是我们看的东西其实全程都和 file_get_contents($profile['photo']) 无关

终于,在 update.php 里面找到了关于 $profile 序列化的代码

所以就是用 username 进行字符串逃逸的工作,让 photo 那里直接读取 config.php,去到 update 界面开始攻击。

我们要把 filename 修改成恶意的 config.php,构造出的结果如下

1
2
3
4
5
6
7
8
9
10
11
$profile = a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:8:"ss@q.com";s:8:"nickname";s:8:"sea_sand";s:5:"photo";s:10:"config.php";}s:39:"upload/804f743824c0451b2f60d81b63b6a900";}
print_r(unserialize($profile));

结果如下:
Array
(
[phone] => 12345678901
[email] => ss@q.com
[nickname] => sea_sand
[photo] => config.php
)

但是很明显,不可能直接可以读到 config.php,需要我们在 nickname 那里进行恶意构造,所以 payload

1
nickname[]=wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

nickname 最后面塞上";}s:5:“photo”;s:10:“config.php”;},一共是34个字符,如果利用正则替换34个where,就可以直接把后面这些字符挤出去,让他们成为 photo

值得注意的是,nickname 要从原本的 nickname 修改成数组,因为要进行闭合

此时我们已经可以读到 config.php,只需要把内容进行 base64 解码即可

过滤后字符变少

  • 异曲同工,之前我们是把字符挤出去,这里我们要把字符塞进来。

源代码如下

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

function change($str){
return str_replace("xx","3",$str);
}

$arr['name'] = $_GET['name'];
$arr['age'] = $_GET['age'];
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
var_dump($old);
echo "<br/>";
$new = unserialize($old);
var_dump($new);
echo "<br/>此时,age=";
echo $new['age'];

正常情况下,回显如此

现在我们把 name 改成 1xx,这里肯定会报错,过滤之后是减少了的,这就和字符变多的道理是一样的。

所以此处我们的利用思路应该是这样的:在后面构造恶意的 payload,然后通过字符减少,把恶意 payload 提前闭合。

payload

1
?name=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&age=11";s:3:"age";s:6:"whoami";}

0x05 Session 反序列化

 评论