0x01 前言
从一开始接触Java序列化漏洞就经常看到以Java集合作为反序列化入口,但是也没有仔细思考过原因。分析的Gadget多了觉得很多东西有必要总结一下,所以有了本篇文章。
分析过ysoserial的同学应该经常会遇到将HashMap、HashSet、PriorityQueue等Java集合作为反序列化入口的情况,总结了下大致如下:
反序列化载体 | 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
HashMap.readObject() -> HashMap.hash() -> AbstractTableModel$ff19274a.hashCode() -> ...
Hibernate1和Hibernate2
HashMap.readObject() -> HashMap.hash() -> org.hibernate.engine.spi.TypedValue.hashCode() -> ...
JSON1
HashMap.readObject() -> HashMap.putVal() -> javax.management.openmbean.TabularDataSupport.equals() -> ...
Myfaces1和Myfaces2
HashMap.readObject() -> HashMap.hash() -> org.apache.myfaces.view.facelets.el.ValueExpressionMethodExpression.hashCode() -> ...
ROME
HashMap.readObject() -> HashMap.hash() -> com.sun.syndication.feed.impl.ObjectBean.hashCode() -> com.sun.syndication.feed.impl.EqualsBean.beanHashCode() -> ...
URLDNS
HashMap.readObject() -> HashMap.hash() -> java.net.URL.hashCode() -> ...
无外乎两种:
HashMap.readObject() -> HashMap.hash() -> XXX.hashCode()
HashMap.readObject() -> HashMap.putVal() -> XXX.equals()
那我们看看这几个方法,HashMap.readObject()中恢复HashMap时调用HashMap.putVal()插入键值对,并且调用HashMap.hash()将返回值作为参数。
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 进行异或 ^ 操作。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
在HashMap.putVal()中多次调用key.equals(k)进行比较,保证HashMap的键唯一的特点。
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():
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的一样。
// 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:
LinkedHashSet.readObject() -> (HashSet)LinkedHashSet.add() -> HashMap.put() -> HashMap.hash() -> TemplatesImpl.hashCode() -> ...
LinkedHashSet继承了HashSet,底层也是HashMap,内部没有直接定义readObject方法,但是可以调用HashSet.readObject(),跟HashSet的调用一样。
4、PriorityQueue
之前的文章里分析过了,详细不再赘述。
PriorityQueue.readObject() -> java.util.PriorityQueue.heapify() -> java.util.PriorityQueue.siftDown() -> PriorityQueue.siftDownUsingComparator() -> XXXComparator.compare()
5、Hashtable
ysoserial中CommonsCollections7用到了Hashtable,Hashtable和HashMap类似,不过Hashtable是支持同步的。
Hashtable.readObject() -> Hashtable.reconstitutionPut()-> org.apache.commons.collections.map.AbstractMapDecorator.equals() -> ...
Hashtable反序列化时创建Entry数组,将key和value通过Hashtable.reconstitutionPut()插入数组。
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()动态加载。
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中也常被用到,道理都一样。
public boolean equals(Object obj) {
return (this == obj);
}
public native int hashCode();
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
在挖掘漏洞时可以作为反序列化入口可以总结如下:
HashMap.readObject() -> ... -> XXX.hashCode() -> ...
HashMap.readObject() -> ... -> XXX.equals() -> ...
... -> XXX.toString() -> ...
PriorityQueue.readObject() -> ... -> Comparator.compare() -> ...