最近研究了一下java、php、python三种语言的反序列化漏洞,总结一下异同。都是反序列化漏洞,漏洞原理自然是相同的,只是由于各自的语言特性在利用场景和利用方式上会有一些差异。

序列化常常用在网络传输和数据库存储中,我们可以将一个立体的对象转化为一段字节序列来进行存储和传输。

0x01 java

java作为一门强类型语言,其漏洞利用的要求是较为苛刻的。
先通过一段代码来看看java序列化和反序列化的方式。

public class test{
    public static void main(String args[]) throws Exception{
        //定义myObj对象
        MyObject myObj = new MyObject();
        myObj.name = "hi";
        //创建一个包含对象进行反序列化信息的”object”数据文件
        FileOutputStream fos = new FileOutputStream("object");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        //writeObject()方法将myObj对象写入object文件
        os.writeObject(myObj);
        os.close();
        //从文件中反序列化obj对象
        FileInputStream fis = new FileInputStream("object");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //恢复对象
        MyObject objectFromDisk = (MyObject)ois.readObject();
        System.out.println(objectFromDisk.name);
        ois.close();
    }
}

class MyObject implements Serializable{
    public String name;
    //重写readObject()方法
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        //执行默认的readObject()方法
        in.defaultReadObject();
        //执行打开计算器程序命令
        Runtime.getRuntime().exec("open /Applications/Calculator.app/");
    }
}

在java代码中,只有实现了Serializable接口的类才能进行序列化和反序列化,而这个接口包含了readObject方法,其作用是从一个源输入流中读取字节序列,把它们反序列化为一个对象。readObject有点类似构造函数,在反序列化时会调用它来重建对象。要想挖掘java中的反序列化漏洞必须从readObject方法中找文章,如上例在反序列化时执行了readObject方法中的打开计算机命令。当然这个例子有点过于勉强,程序员不会傻到在readObject里写一个命令执行的函数。

举个实际点的例子——spring rmi反序列化漏洞。从结果来看这个漏洞的产生是由于JtaTransactionManager类存在问题,我们来看看该类的readObject方法做了什么。

首先调用了initUserTransactionAndTransactionManager(),在没有进入该函数前,我们可以根据函数名可以判断它大概的作用是进行初始化。
C2C8401D-D36A-405A-86DF-91BE63842150.png

我们进入该函数,如果能满足前两个if语句,它将会调用lookupUserTransaction()。第一个条件,userTransaction为空,第二个条件,userTransactionName不为空。
D7DE0075-1A00-4A46-A66C-33BF10A25AC1.png

我们再进入lookupUserTransaction函数,发现它又调用了getJndiTemplate().lookup(),第一个参数是自身的userTransactionName属性
28573F55-771C-485C-B347-E5F74608E3E3.png

后面的细节就不跟了,官方文档中lookup()方法的描述是:Look up the object with the given name in the current JNDI context。这里的name即userTransactionName属性,如果我们赋予该属性一个jndi地址,在反序列化时即能够加载该jndi地址。那这里就存在问题了,如果该jndi地址是一个包含恶意类的rmi接口,那么反序列化的时候就会加载这个恶意类了。具体的漏洞分析和利用移步深入理解JAVA反序列化漏洞,描述得很透彻。

0x02 php

与java的readObject对应,php在反序列化后会先执行自身的魔幻函数__wakeup(),在销毁时又会执行魔幻函数__destruct()。php的反序列化问题往往出在session上。

php内置的session机制是基于序列化的,存在三种序列化引擎,使用不同的引擎进行序列化会得到不同格式的结果:

php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值

查阅了一些资料以后,发现php反序列化问题主要存在于以下两种场景
1.在同一个应用中如果使用了两种引擎来处理序列化
php默认的序列化引擎是php,如果序列化的时候使用php_serialize,然后读取的时候使用php的方式进行反序列化就存在漏洞。

一个经典的例子,来自乌云PHP Session 序列化及反序列化处理器设置使用不当带来的安全隐患。我们来看下面这个精心构造的session

$_SESSION['ryat'] = '|O:11:"PeopleClass":0:{}';

使用php_serialize序列化后的内容为:a:1:{s:4:"ryat";s:24:"|O:11:"PeopleClass":0:{}";}
使用php反序列化时,将a:1:{s:4:"ryat";s:24:"这部分作为key,将O:11:"PeopleClass":0:{}作为value,反序列化后就实例化了 PeopleClass 这个对象。但我们注意到,即使得到了这个对象,我们也无法完成利用,因为我们无法主动执行它的方法。因此漏洞的利用条件还是依赖于这个类实现了wakeup或者destruct两种魔幻方法,且魔幻方法中存在某种利用,我们可以构造pop链来进行攻击,类似上文的java反序列化的利用,可以参考php对象注入-pop链的构造

但是有一种情况,我们可以不依赖于魔幻方法来完成攻击。

2.在应用中如果使用了spl_autoload_register()方法
spl_autoload_register()即注册__autoload()方法,在很多php框架中,常常会用到该方法来工厂化地生成对象。具体原理如下,当PHP实例化一个未声明的类时,会在include_path中(php.ini中配置)寻找包含该类名称的php文件或者inc文件。当反序列化得到了一个没有类声明的对象时,php也会在include_path寻找包含该类名称的php文件或者inc文件,若存在则会加载该php,如果这个php文件可控,我们便能够获得一个webshell。
具体的利用思路就是,控制session,session经过反序列化得到了一个没有类定义的对象,且我们能够在include_path上传一个(类名).inc,配合spl_autoload_register()就能获取一个shell:

  1. 往include_path写一个th1s.inc
    <?php file_put_contents("web路径/shell.php",'<?php eval($_POST[1]);?>');

  2. 若session.serialize_handler =
    php,构造session文件a|O:4:"th1s":0:{},对应的代码是$_SESSION['a']=new th1s();,遵守文件名格式'sessxxxxx'往session存放路径写session文件。

  3. 最后,带着PHPSESSID=xxxxx的cookie访问开启了spl_autoload_register的web应用即可获取webshell了。

这是来自于一道ctf题的思路 SCTF2016 Pentest(Web) Hackme Writeup

python

首先先来看看php序列化跟python序列化的结果有什么区别。

<?php 
class Foo { 
    public $entity = 'test'; 
    
    function func() { 
        print 'test'; 
    } 
} 

$foo = new Foo; 
print(serialize($foo));
?> 

1.png
php序列化后的内容只保存了属性。

import cPickle
import subprocess
import os

class A(object):
    a = 1
    b = 2
    def __reduce__(self):
        return (subprocess.Popen, (('calc.exe',),))

print cPickle.dumps(A())
cPickle.loads(cPickle.dumps(A()))

IMG20170710_104200.png
而python在序列化后的内容除了保存对象的属性以外也保存了类的方法。

如果该类方法能够在反序列化后自动执行,那么我们就能够完美利用了。事实上这样的方法是存在的,即reduce。我们在python命令行中直接反序列化由上面代码序列化生成的字符串,可以看到成功命令执行了。
IMG20170710_105030.png

因此,我们仅仅需要一个python反序列化的接口,不依赖于类似php中wakeup函数存在漏洞的场景,既能通过构造恶意类完成攻击。
for example,通过redis未授权配合python反序列化来完成攻击的实例 掌阅iReader某站Python漏洞挖掘

除了python以外,nodejs在序列化对象以后也能保存类的方法,因此其利用条件也十分简单。具体的构造方式可以看这篇文章HACKING NODE SERIALIZE - 进一步利用 Node.JS 代码执行漏洞