Java反序列化漏洞-Fastjson
前言(2020/03/16更新)
一直想学习一下Java反序列化漏洞的相关利用,寒假期间学习了Java原生类的反序列化漏洞(Apache Commons Collections组件),不过到现在都还没有做一个总结。刚好最近有师傅问到shiro反序列化漏洞的相关事情,所以现在从这篇文章开始,学习各种Java反序列化漏洞,顺便做一个总结并记录下来。环境搭建采用的是Maven+Spring MVC,方便快捷。
Fastjson
Fastjson是一个性能很好的Java语言实现的Json解析器和生成器,由来自阿里巴巴的工程师开发。具有极快的性能,接口简单易用。Fastjson使用parseObject来进行反序列化。
Fastjson提供了两个主要接口toJSONString和parseObject来实现Json数据的序列化和反系列化。
项目地址:https://github.com/alibaba/fastjson
影响范围
fastjson <= 1.2.24
JDK8: 1.2.22 <= fastjson <= 1.2.24
利用方式
基于TemplateImpl
利用条件
- 解析时设置了
Feature.SupportNonPublicField
,否则不支持传入私有属性 - 目标使用的jdk中存在
TemplatesImpl
类
测试
向maven项目的pom.xml文件中加入需要的依赖:
1 |
|
然后写个测试方法:
1 |
|
Poc(坑:之前没写package一直弹不出来计算器):
1 |
|
然后javac编译成class并base64编码一下生成恶意_bytecodes。访问http://localhost:8080/Fastjson?data=,即可看到弹出计算器。
payload
1 |
|
@type:解析类,_bytecodes:恶意字节码,_outputProperties:调用outputProperties变量的get方法getOutputProperties。
漏洞分析
我们给parseObject函数下断点进行调试,可以看到代码先根据input、config和featureValues生成Json解析类,然后调用它的parseObject函数进行反序列化处理:
我们继续走下去,可以看到下一步是derializer.deserialze函数:
然后是parser.parse:
在这个函数中开始对Json进行解析,先判断了第一个字符,因为payload的第一个字符是{,所以进入了分支LBRACE中:
然后调用了parseObject函数对Json的内容进行解析,获取了键值:
接着开始获取type中所写明的类:
接下来调用deserializer.deserialze函数:
我们跟进去,里面对Json字符串中的各个键值进行了解析,然后调用了parseField函数,当传入的key为_outputProperties的时候,漏洞触发,我们跟进去看看。看到代码调用了fieldDeserializer.parseField->setValue,最后调用invoke函数:
调用了getOutputProperties函数,我们继续走下去,最后来到newTransformer函数:
可以看到这里调用getTransletInstance()函数实例化了类,然后就执行了类的构造函数,导致了代码执行漏洞:
以上就是对基于TemplateImpl的Fastjson反序列化漏洞利用的分析。
基于JNDI
RMI
利用条件
JDK<=8u121,Oracle在jdk8u121之后设置了com.sun.jndi.rmi.object.trustURLCodebase为 false,限制了 RMI 利用方式中从远程加载,会报错:
1 |
|
测试
开启RMI服务:
1 |
|
在Web服务上部署编译后的恶意class:
1 |
|
然后发送payload。
payload
1 |
|
漏洞分析
同样给JSON.parseObject下断点进行调试,可以看到与之前不同的是这次通过了if (matchField)的判断,进入了另一个分支,调用了setValue函数:
然后调用了autoCommit的setter函数setAutoCommit:
最后调用了connect函数,连接了攻击者开启了RMI服务:
以上就是对RMI的Fastjson反序列化漏洞利用的简要分析,跟基于TemplateImpl的原理差不多,都是利用了类的getter或者setter。
绕过方式
在服务端有Tomcat的org.apache.naming.factory.BeanFactory类的时候,可以尝试绕过。
https://www.veracode.com/blog/research/exploiting-jndi-injections-java
LDAP
利用条件
JDK<=8u191,Oracle在jdk8u191之后设置了com.sun.jndi.ldap.object.trustURLCodebase为 false,限制了 LDAP 利用时从远程加载 Class。
安装marshalsec
1 |
|
测试
开启LDAP服务:
1 |
|
同样在Web服务上部署编译后的恶意class,发送
payload1:
1 |
|
payload2:
1 |
|
payload3:
1 |
|
payload4:
1 |
|
漏洞分析-payload1
payload1分析同RMI,不再加以分析。
漏洞分析-payload2
首先跟RMI一样进入了if (matchField)并调用了setValue函数,然后调用了PropertyPath的setter函数setPropertyPath,给PropertyPath变量赋了值,到这里还没有触发漏洞。接下来的处理跟基于TemplateImpl的一样,调用了parseField函数,将字段beanFactory作为参数传入:
这里调用fieldValueDeserilizer.deserialze获取了beanFactory中的org.springframework.jndi.support.SimpleJndiBeanFactory实例,然后进入setValue函数:
再调用setter函数setBeanFactory:
这里可以看到有一个判断this.propertyPath == null,如果不传入propertyPath就会直接抛出异常终止程序:
然后将攻击者提供的恶意IDAP地址作为参数,调用this.beanFactory.getBean,即org.springframework.jndi.support.SimpleJndiBeanFactory.getBean函数,最后调用lookup函数连接了攻击者的恶意IDAP:
以上就是对payload2的简要分析,同样原理差不多,但是两层嵌套结构很精妙。
漏洞分析-payload3
payload3分析大致同RMI(应该?),且我本地没有这个类,所以也不加以分析。
漏洞分析-payload4
payload4跟payload3相似,不过payload4是Set类型,且结构更复杂一点。
这次我偷个懒,直接找到最后的触发点,然后查看调用栈,最后触发点跟payload3遗址,同样是lookup函数。因为类型为Set的缘故,所以不会进入其他payload进入的分支LBRACE,而是进入SET分支中:
这里通过HashSet来对Set类型进行处理,最后在putVal函数中调用了key.equals函数:
即DefaultBeanFactoryPointcutAdvisor的equals,由于继承关系,最后调用了AbstractPointcutAdvisor的equals,再调用了getAdvice函数:
然后调用了getBean:
最后就是触发点的doGetSingleton函数了:
以上就是对payload4的简要分析,跟其他payload不同的就是这种payload没有用到getter或者setter,而是用的Set->HashMap->equals这样一个调用链。
2020/03/15 更新 - fastjson 1.2.61
参考文章:https://curz0n.github.io/2019/09/24/fastjson_1_2_61_blacklist_bypass/
现在开始偶尔看看安全相关的东西,因为之前高校战疫的题目提到了 fastjson 1.2.61 版本的绕过,所以我就来更新一番。
首先因为我更新过 Java,所以要改一改之前项目的 JDK 配置,然后修改 pom.xml 里的 fastjson 版本配置为 1.2.61 。
看漏洞描述,好像跟之前的利用方式有点相似,都是利用属性的 getter 和 setter,最后的命令执行方式是调用 RMI 加载远程恶意 class,使用的 commons-configuration 组件,不过我本地都没有这个包,得安装才行。去 maven 仓库上看看这个包:https://mvnrepository.com/artifact/org.apache.commons/commons-configuration2 ,有好多版本,我先装个最新版本试试,在 pom.xml 里加入:
1 |
|
下载好像有点慢,打算加个镜像服务器代理试试,仔细一想才发现不知道 setting.xml 在哪里,于是去看了看环境变量里的 path,发现在 IDEA 的目录 plugins\maven\lib\maven3\conf 里面,有 2 和 3 两个版本,于是又仔细一想,发现不知道项目配置的 maven 是怎么样的,想着干脆看看配置的 maven 版本、配置文件和仓库地址,顺便拯救一下我快被塞满了的 C 盘,在 IDEA 中点击 File - Other Settings - Default Settings,然后搜索 maven 就可以看到配置了,可以看看这篇文章:https://blog.csdn.net/konzy/article/details/81626629
然后修改配置,在 setting.xml 的 mirrors 标签里面里面加上镜像,先试试腾讯云:https://www.cnblogs.com/slankka/p/12202640.html
加个镜像服务器代理试试:
1 |
|
速度飞快啊,然后修改一下后端代码,支持一下 1.2.61 的 autotype 黑名单:
1 |
|
1.2.61 的黑名单存在 ParserConfig 类里面,好像是采用了人类无法看懂的十进制进行存储,具体是什么类可以看这:https://github.com/LeadroyaL/fastjson-blacklist 。
运行的时候看到一个报错,虽然无伤大雅但还是解决一下比较好:https://blog.csdn.net/zhaozhihang1234/article/details/50143903 ,电脑上的 tomcat 也很老了,涉及 EL 的时候会出错好像,什么时候应该换个新的了。
现在大概可以开始研究了吧。
好像不行,运行的时候又遇到一个错误:
1 |
|
在网上找到一个脚本:https://blog.csdn.net/qq_28675967/article/details/90579636 ,可以检测类是从哪里加载的,我试了一下结果发现它加载的是 target 下面的 1.2.23 版本的 fastjson?里面两个版本的包都有,看来 IDEA 的编译运行还是需要注意一下包的引入的。删掉了 1.2.23 版本的包之后,运行正常了。
现在我回过头来看看脚本的做法,先用 getProtectionDomain 获取 ProtectionDomain 这个对象,再调用里面的 getCodeSource,这时获取的对象就已经看得出类的来源地了。
现在终于可以开始测试了,先布置好恶意 class 然后启动 RMI 服务和 Web 服务,我这里好像 Tomcat 和 RMI 服务端口冲突了,需要修改一下 Tomcat 的端口。payload:
1 |
|
然后报错了:
1 |
|
看来这个 payload 最后用到了 JDK 中的 com.sun.jndi.rmi.registry.RegistryContext 类,被 trustURLCodebase 拦截了,所以不能在高版本 Java 里面使用。
以后有时间整理一下 RMI LDAP JNDI JRMP 相关的东西, 我对这些概念还不是特别熟悉,还有就是反序列化中除了直接加载字节码之外的方式,比如从远程加载序列化字符串绕过前面的反序列化黑名单之类的。
参考链接:
https://www.freebuf.com/vuls/178012.html
https://www.restran.net/2018/10/29/fastjson-rce-notes/#%E5%9F%BA%E4%BA%8E-JNDI-%E7%9A%84-PoC
https://mp.weixin.qq.com/s/Eeq4nFdmM8a57l-tZXfvrQ
https://bl4ck.in/tricks/2019/01/04/JNDI-Injection-Bypass.html
http://www.yulegeyu.com/2019/01/11/Exploitng-JNDI-Injection-In-Java/