在之前 CommonsCollections1 的文章中有分析怎么利用几个 Transformer 类来执行系统命令,以及通过 LazyMap#get 来触发。所以本文不再赘述这一部分内容,只要知道最终目的是执行 LazyMap#get 方法就行了。
CommonsCollections5
这条链需要用到 BadAttributeValueExpException 中重写的 readObject 方法,我搜了一下没找到这个方法具体是在哪个版本加上去的,下面测试用的是 JDK 8u212
。
首先看一下 getObject 方法:
前面构造 transformerChain 的过程和 cc1 一样,触发点同样在 lazyMap#get 方法中。区别主要在红框的部分——生成 TiedMapEntry 对象,并将 lazyMap 赋值给它的 map 属性;然后创建 BadAttributeValueExpException 对象,通过反射将 TiedMapEntry 对象赋值给它的私有属性 val。
在进行反序列化时,执行 BadAttributeValueExpException 重写的 readObject 方法:
如果没有开启安全管理器(System.getSecurityManager() == null
),会执行
TiedMapEntry#toString 方法。TiedMapEntry 的关键方法如下:
public Object getValue() {
return this.map.get(this.key);
}
...
public String toString() {
return this.getKey() + "=" + this.getValue();
}
可见最终会执行我们构造的 lazyMap 的 get 方法,导致命令执行。
CommonsCollections6
前半段和上面一样,先构造一个 TiedMapEntry 类对象,想办法触发它的 getValue 或者 toString 方法。
首先创建一个 HashSet 类对象,通过反射拿到它的私有属性 map:
这个 map 属性是 HashMap 类的,再通过反射获取其 table 属性:
获取数组的第一个非空元素 node,将构造好的 TiedMapEntry 对象赋值给它的 key 属性:
构造好的 HashSet 对象结构如下:
从上图可以看到 map 和 table 两个都是 transient 属性,也就是不会被序列化,那设置它们有什么意义呢?我们看一下 HashSet#writeObject 方法:
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();
// Write out HashMap capacity and load factor
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());
// Write out size
s.writeInt(map.size());
// Write out all elements in the proper order.
for (E e : map.keySet())
s.writeObject(e);
}
关键在最后的 for 循环,主动将我们构造的 TiedMapEntry 对象写到输出流中,所以不受 transient 的影响。
反序列化 HashSet 时,在 readObjet 方法的最后会获取 TiedMapEntry 对象(key),将它添加到一个 HashMap 中。
跟进发现会通过 key.hashCode()
获取 key 的哈希值,而在 TiedMapEntry#hashCode 方法中触发了 lazyMap#get 方法:
最终导致命令执行。
总结
之所以把 cc5 和 cc6 两条链放在一起讲,是因为它们和 cc1 一样都利用 ChainedTransformer 构造反射链来执行命令,而且都利用了 TiedMapEntry 类。
链的前半部分逻辑就比较简单了,比较有趣的知识点是 HashSet 的 writeObject 方法会序列化它的所有 key,而 HashMap 的 writeObject 会序列化它的所有 key 和 value,所以即使属性用 transient 修饰也不影响利用。