Java反序列化和集合之间的渊源

本文首发于安全客:https://www.anquanke.com/post/id/251220。

0x01 前言

如果分析过ysoserial的同学应该经常会遇到将HashMap、HashSet、PriorityQueue等Java集合作为反序列化载体的情况,本次分析的重点就是Java集合和Java反序列化漏洞的关系。

在ysoserial中,用集合作为反序列化载体的总结如下:

反序列化载体 Gadget
HashMap Clojure、Hibernate1、Hibernate2、JSON1、Myfaces1、Myfaces2、ROME、URLDNS
HashSet AspectJWeaver、CommonsCollections6
PriorityQueue BeanShell1、Click1、CommonsCollections2、CommonsCollections4、Jython1
LinkedHashSet Jdk7u21
Hashtable CommonsCollections7

0x02 Gadget总结

1、HashMap

先来看下各个Gadget中涉及HashMap的部分:

Clojure

1
HashMap.readObject() -> HashMap.hash() -> AbstractTableModel$ff19274a.hashCode() -> ...

Hibernate1和Hibernate2

1
HashMap.readObject() -> HashMap.hash() -> org.hibernate.engine.spi.TypedValue.hashCode() -> ...

JSON1

1
HashMap.readObject() -> HashMap.putVal() -> javax.management.openmbean.TabularDataSupport.equals() -> ...

Myfaces1和Myfaces2

1
HashMap.readObject() -> HashMap.hash() -> org.apache.myfaces.view.facelets.el.ValueExpressionMethodExpression.hashCode() -> ...

ROME

1
HashMap.readObject() -> HashMap.hash() -> com.sun.syndication.feed.impl.ObjectBean.hashCode() -> com.sun.syndication.feed.impl.EqualsBean.beanHashCode() -> ...

URLDNS

1
HashMap.readObject() -> HashMap.hash() -> java.net.URL.hashCode() -> ...

无外乎两种:

1
2
HashMap.readObject() -> HashMap.hash() -> XXX.hashCode()
HashMap.readObject() -> HashMap.putVal() -> XXX.equals()

那我们看看这几个方法,HashMap.readObject()中恢复HashMap时调用HashMap.putVal()插入键值对,并且调用HashMap.hash()将返回值作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
reinitialize();
......
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
for (int i = 0; i < mappings; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
// 调用HashMap.putVal()
putVal(hash(key), key, value, false, false);
}
}
}

HashMap.hash()用于计算key的hash值,会先获取key.hashCode的值,再对 hashcode 进行无符号右移操作,再和 hashCode 进行异或 ^ 操作。

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

在HashMap.putVal()中多次调用key.equals(k)进行比较,保证HashMap的键唯一的特点。

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
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
......
else {
Node<K,V> e; K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
......
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
......
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

2、HashSet

ysoserial中以HashSet为入口的是AspectJWeaver 和CommonsCollections6,这两个都是通过HashSet.readObject()调用TiedMapEntry.hashCode():

1
HashSet.readObject() -> HashMap.put() -> HashMap.hash() -> org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode() -> ...

HashSet底层是HashMap,readObject()反序列化恢复HashSet实例时需要创建HashMap,将其元素恢复并调用HashMap.put()插入元素,因为HashSet是Object的集合,而HashMap是键值对的集合,put插入时统一以e作为key,PRESENT作为value。HashMap.put是用HashMap.putVal()实现的,所以后续的调用和HashMap的一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// HashSet.readObject()
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();
......
// Create backing HashMap
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}

//HashMap.put()
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

3、LinkedHashSet

ysoserial中仅Jdk7u21用到了LinkedHashSet:

1
LinkedHashSet.readObject() -> (HashSet)LinkedHashSet.add() -> HashMap.put() -> HashMap.hash() -> TemplatesImpl.hashCode() -> ...

LinkedHashSet继承了HashSet,底层也是HashMap,内部没有直接定义readObject方法,但是可以调用HashSet.readObject(),跟HashSet的调用一样。

4、PriorityQueue

之前的文章里分析过了,详细不再赘述。

1
PriorityQueue.readObject() -> java.util.PriorityQueue.heapify() -> java.util.PriorityQueue.siftDown() -> PriorityQueue.siftDownUsingComparator() -> XXXComparator.compare()

5、Hashtable

ysoserial中CommonsCollections7用到了Hashtable,Hashtable和HashMap类似,不过Hashtable是支持同步的。

1
Hashtable.readObject() -> Hashtable.reconstitutionPut()-> org.apache.commons.collections.map.AbstractMapDecorator.equals() -> ...

Hashtable反序列化时创建Entry数组,将key和value通过Hashtable.reconstitutionPut()插入数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the threshold and loadFactor
s.defaultReadObject();
......
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--) {
K key = (K)s.readObject();
V value = (V)s.readObject();
// sync is eliminated for performance
reconstitutionPut(table, key, value);
}
}

Hashtable.reconstitutionPut()为了计算hash和保证键唯一,也调用了hashCode和equals(),CommonsCollections7中用到的是equals()动态加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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++;
}

0x03 Java集合为什么备受青睐?

Java所有类都继承于Object。既然都继承于Object,那么所有的类都是有共性的,只要是java对象都可以调用或者重写父类Object的方法。
集合可以理解为一个容器,可以储存任意类型的对象。在集合中经常会有比较或者计算Hash的操作,那么自然会频繁使用equals()和hashCode()方法,而equals()和hashCode()都是Object中定义的方法,在不同的类中也进行了重写。为了实现Gadget的动态加载,自然会用到这些方法进行连接。
这就能解释为什么Java集合会备受Gadget青睐。

此时我们可以拓展下,在PriorityQueue中比较时用的是Comparator或Comparable,Comparator是Java中一个重要的接口,被应用于比较或者排序,Comparator也在很多类中实现了。

除了equals()和hashCode(),上文没有提到的toString也是Object中定义的方法,在Gadget中也常被用到,道理都一样。

1
2
3
4
5
6
7
8
9
public boolean equals(Object obj) {
return (this == obj);
}

public native int hashCode();

public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

因此我们可以总结如下,在挖掘漏洞时可以作为反序列化的载体,当然除了这些可以以类似的思路进行拓展。

1
2
3
4
HashMap.readObject() -> ... -> XXX.hashCode()
HashMap.readObject() -> ... -> XXX.equals()
... -> XXX.toString()
PriorityQueue.readObject() -> ... -> Comparator.compare()

0x04 参考链接

https://github.com/frohoff/ysoserial