CodeQL初探

一、CodeQL的研發背景

最早期,安全人員會通過人工審計的方式來審計項目代碼,查找危險函數,並跟進危險函數的參數是否可控,如果可控,說明存在安全漏洞。

但是隨着項目數量的增加,以上的純靠人工的方式很難實現所有項目漏洞的覆蓋測試。所以出現了一些輔助人工審計的工具,比如前幾年比較火的rips,cobra,通過這些工具,可以把危險函數代碼代碼檢索出來,再通過人工審計來判斷是否存在安全漏洞。但是這種方式主要還是需要人來判斷,工作量還是很大,並且非常依賴安全工程師的個人能力。

但是近些年出現了不少優秀的自動化代碼安全審計產品,比如非常有名的Checkmarx,Fortify SCA。這些軟件可以自動化的幫我們審計出安全漏洞,大大減少了人工工作量,並加快了安全審計速度。但是這些軟件都是商業的,價格比較貴,一般企業可能沒有這麼多預算購買。

與此同時,Github爲了解決其託管的海量項目的安全性問題,收購了CodeQL的創業公司,並宣佈開源CodeQL的規則部分,這樣全世界的安全工程師就可以貢獻高效的QL審計規則給Github,幫助它解決託管項目的安全問題。對於安全工程師,也就多了一個非商業的開源代碼自動化審計工具。

CodeQL支持非常多的語言,在官網有如下支持的語言和框架列表

參考鏈接:

https://codeql.github.com/docs/codeql-overview/supported-languages-and-frameworks/

 

二、CodeQL簡介

CodeQL是一個可以對代碼進行分析的引擎,安全人員可以用它作爲挖洞的輔助或者直接進行挖掘漏洞,節省進行重複操作的精力。

在CodeQL中,代碼被解析成數據,存儲在數據庫中。安全漏洞、錯誤和其他錯誤被建模爲可以針對數據庫執行的查詢。我們可以運行由GitHub研究人員和社區貢獻者編寫的標準CodeQL查詢,也可以編寫自己的查詢以用於自定義分析。

0x1:CodeQL安裝、部署、簡單使用 

CodeQL本身包含兩部分:

  • 解析引擎:解析引擎用於解析數據庫執行查詢等操作。雖然不開源,但是我們可以直接在官網下載二進制文件直接使用,https://github.com/github/codeql-cli-binaries/releases
  • SDK:SDK完全開源,裏面包含了規則查詢中涉及到的公共庫文件,針對不同語言提供了很多函數和類型以方便我們編寫自己的規則,https://github.com/github/codeql

CodeQL提供了命令行工具和vscode插件兩個選擇,vscode插件底層也是調用命令行工具,但是有圖形界面並且封裝了一些功能,用起來會更加方便。

注意,解析引擎和SDK要放在同級目錄,CodeQL引擎會自動在上下級目錄搜索庫。

1、codeql-cli安裝

這裏我們以命令行環境下運行codeql爲例,先下載codeql-cli,

項目地址 : 

https://github.com/github/codeql-cli-binaries/releases

打開項目地址之後進入Releases庫,下載對應操作系統的壓縮包解壓到任意一個文件夾。

接下來安裝codeql規則庫,下載開源的codeql標準庫和查詢庫,

https://github.com/github/codeql/tree/main

保證codeql-cli(下圖中codeql文件夾)和codeql SDK(下圖中codeql-lib文件夾)在同一個目錄下,

2、vscode-codeql安裝

vscode的codeql插件,直接在插件市場安裝,

3、配置環境變量

爲了方便我們使用codeql-cli,我們需要將其路徑放到PATH下,

同時我們最好再配置下codeql插件的可執行文件路徑,打開vscode的設置,搜索codeql,修改Executable Path,

參考鏈接:

https://github.com/github/codeql-cli-binaries/releases
https://github.com/github/codeql
https://juejin.cn/post/6844903878694010893
https://ost.51cto.com/answer/5159

4、建立codeql workspace

兩種方法建立codeql workspace

  • 第一種就是把要審計的代碼放入codeql中
  • 第二種是把codeql加入要審計的代碼的workspace中(較麻煩)

這裏選擇第一種。

下載官方給出的codeql規則庫,starter,

git clone https://github.com/github/vscode-codeql-starter/

項目下載完成後,進入項目目錄,確保包含需要的子模塊。

git submodule update --init
git submodule update --remote

在VS Code中打開starter workspace,

starter子模塊中包括

  • C/C++
  • C#
  • Java
  • JavaScript
  • Python
  • Ruby
  • GO的規則

5、創建codeql數據庫

現在codeql workspace設置好了,codeql規則庫庫也下來好了,接下來要準備被分析的項目project了,項目project是我們做代碼分析的主體。

由於CodeQL的處理對象並不是源碼本身,而是中間生成的AST結構數據庫,所以我們先需要把我們的項目源碼轉換成CodeQL能夠識別的CodeDatabase。如果你之前已經針對項目project創建好了codeql數據庫,在側邊欄打開CodeQL數據庫,如圖有四種添加數據庫的方法。

當添加數據庫之後,會有數據庫視圖,可以右擊列表中的項進行數據庫交互,可以利用Ctrl/Cmd+click選擇多個數據庫 。

這裏基於codeql案例庫中的java安全風險案例創建數據庫,/Users/zhenghan/Projects/codeql-lib/java/ql/test/query-tests/security/CWE-020

codeql database create java-security-CWE-020 -l=java -c="javac SuspiciousRegexpRange.java" --source-root=/Users/zhenghan/Projects/codeql-lib/java/ql/test/query-tests/security/CWE-020

也可以從零新建一個maven項目,然後基於這個項目創建codeql數據庫。

注意!生成數據庫之前,需要先保證被分析程序可以正常跑起來。

進入到項目根目錄下,執行codeql指令:

// 創建新數據庫
codeql database create java-database -l=java -c="mvn clean install -file pom.xml" --source-root=/Users/zhenghan/Projects/codeql-home/hello_codeql

// 更新數據庫
codeql database upgrade java-database
  • codeql database create java-database:利用 codeql 創建名爲 java-database 的 java 數據庫
  • -l=java:編譯語言爲 java
  • -c="mvn clean install -file pom.xml":利用maven命令進行編譯
  • --source-root=/Users/zhenghan/Projects/codeql-home/hello_codeql:設置生成codeql數據庫的路徑

將建好的codeql database導入vscode,

在該路徑增加一個 demo.ql,即可開始編寫ql查詢語句,

6、導入codeql數據庫,運行codeql查詢

和SQL語言一樣,我們執行QL查詢,肯定是要先指定一個數據庫纔可以。

待分析源碼如下,

package org.example;

public class Main {
    public static void main(String[] args) {
        String a = "hello";

        System.out.printf("Hello and welcome!");

        if(1 == 1){

        }

        for (int i = 1; i <= 5; i++) {
            System.out.println("i = " + i);
        }
    }
}

將上面創建好的codeql數據庫導入vdcode。

因爲我們已經添加好了codeql workspace,所以在左邊側欄可以看到已經有官方內置現成的query .ql查詢文件可用了,點擊運行可以查看運行結果。 

同時,我們也可以自行開發新的query查詢文件,用於自定義漏洞挖掘。

查詢程序中是否存在空代碼block, 

參考鏈接:

https://docs.github.com/zh/code-security/codeql-cli/getting-started-with-the-codeql-cli/preparing-your-code-for-codeql-analysis
https://codeql.github.com/docs/codeql-for-visual-studio-code/analyzing-your-projects/ 
https://www.anquanke.com/post/id/266823

0x2:CodeQL開發過程總結

在使用 CodeQL 分析代碼之前,需要創建一個 CodeQL 數據庫,其中包含對代碼運行查詢所需的所有數據。 可以使用 CodeQL CLI 自行創建 CodeQL 數據庫。

CodeQL 分析依賴於從代碼中提取關係數據,並使用它來生成 CodeQL 數據庫。 CodeQL 數據庫包含有關代碼庫的所有重要信息,可通過執行 CodeQL 查詢對其進行分析。

在生成 CodeQL 數據庫之前,需要:

  • 安裝並設置 CodeQL CLI 
  • 查看要分析的代碼:
    • 對於分支,請查看要分析的分支的頭。
    • 對於拉取請求,請簽出拉取請求的頭部提交,或簽出 GitHub 生成的拉取請求的合併提交。
  • 設置代碼庫的環境,確保所有依賴項都可用。 
  • 查找代碼庫的生成命令(如果有)。 通常可在 CI 系統的配置文件中找到。

代碼庫準備就緒後,可以運行 codeql database create 以創建數據庫。

0x3:CodeQL語法

CodeQL的很多語法和現在的主流高級語言有很多相似之處,但也有許多的不同。

舉一個簡單的例子,在CodeQL中不存在==,只有=,當一個變量定義了而沒有初始化的時候,=的意思是賦值,但當其已經被賦值了之後,=的意思就變成了比較。

1、基礎數據類型(Primitive types)

CodeQL 是一種靜態類型的語言,因此每個變量都必須有一個聲明的類型。類型是一組值。例如,int 類型是一組整數。注意,一個值可以屬於這些集合中的多個,這意味着它可以有多個類型。

  • 整型(int)
  • 浮點型(float)
  • 日期型(date)
  • 字符型(stirng)
  • 布爾型(boolean)

簡單介紹下日期型和布爾型。

1)日期型(date)

編寫一個簡單的實例用於計算從今年9月1日到今天(11月2日)一共過了多久:

from date start, date end
where start = "01/09/2021".toDate() and end = "02/11/2021".toDate()
select start.daysTo(end)

2)布爾型(boolean)

布爾型變量用來存放布爾值,即false(假)或者 true(真)。

編寫一個簡單的例子來實現兩個布爾之間的和關係:

from boolean a, boolean b
where a = true and b = false
select a.booleanAnd(b)

2、謂詞(Predicates)

謂詞有點類似於其他語言中的函數,但又與函數不同,謂詞用於描述構成 QL 程序的邏輯關係。確切的說,謂詞描述的是給定參數與元組集合的關係。

1)無結果謂詞 

沒有結果的謂詞以predicate作爲開頭,剩下的語法結構類似於定義函數。這種謂詞只能在where語句中使用。

一個簡單的例子如下:

predicate isCity(string city) {
    city = "Beijing"
    or
    city = "ShangHai"
    }
    
    from string city
    where city = "Beijing" and isCity(city)
    select city

2)結果謂詞

有結果的謂詞的定義類似於c/c++語言的函數定義,以返回類型替代predicate作爲開頭。這種謂詞可以在where與select語句中使用。

一個簡單的例子如下:

int addOne(int i) {
    result = i + 1 and
    i in [1 .. 10]
}

from int v
where v = 9
select addOne(v)

3、綁定行爲與綁定集

謂詞所描述的集合通常不允許是無限的,換句話說,謂詞只能包含有限數量的元組(It must be possible to evaluate a predicate in a finite amount of time, so the set it describes is not usually allowed to be infinite. In other words, a predicate can only contain a finite number of tuples.)

舉個簡單的正例和反例:

// 正例,i被限定在1到10內,或者你也可以給i賦一個確定的值如i=1
int addOne(int i) {
    result = i + 1 and
    i in [1 .. 10]
}

// 反例,i是無限數量值的,此時CodeQL編譯器會報錯: 'i' is not bound to a value
int addOne(int i) {
    result = i + 1 and
    i > 0
}

1)單個綁定集

爲了使上述的反例謂詞能夠通過編譯,我們可以使用綁定集(bindingset),但是當我們去調用這個謂詞時,傳遞的參數還是隻能在有限的參數集中。

上面的反例可以修改爲如下:

bindingset[i]
int addOne(int i) {
    result = i + 1 and
    i > 0
}

// 此時我們可以去調用這個謂詞,但是需要注意傳遞過來的參數還是隻能在有限的參數集中
from int i
where i = 1
select addOne(i)

2)多個綁定集

我們同樣可以添加多個綁定集,下面是一個例子:

bindingset[x] bindingset[y]
predicate plusOne(int x, int y) {
  x + 1 = y
}

這個綁定集的意思是如果x或y綁定(bound)了,那麼x和y都綁定,即至少有一個參數受到約束。 

如果我們想要兩者都受約束,可以將例子修改一下:

bindingset[x, y]
predicate plusOne(int x, int y) {
  x + 1 = y
}

那麼這個謂詞就變爲了一個類似於校驗的函數,即x+1 == y。

4、查詢(Query)

查詢是CodeQL的輸出。查詢有兩種類型,分別是

  • select子句
  • 查詢謂詞,這意味着我們可以在當前模塊中定義或者從其他模塊中導入

1)select子句 

select子句的格式如下:

[from] /* ... variable declarations ... */
[where] /* ... logical formula ... */
select /* ... expressions ... */

其中from和where語句是可選的。我們可以在from中定義變量,在where中給變量賦值和對查詢結果的過濾,最後在select中顯示結果。

在select語句中我們還可以使用一些關鍵字:

  • as關鍵字,後面跟隨一個名字。作用相當於sql中的as,爲結果列提供了一個"標籤",並允許在後續的select表達式中使用它們。
  • order by關鍵字,後面跟隨一個一個結果列名。作用相當於sql中的order by,用於排序結果,並且在結果列名後可選asc(升序)或desc(降序)關鍵字。

一個簡單的例子如下:

from int x, int y
where x = 3 and y in [0 .. 2]
select x, y, x * y as product, "product: " + product

2)查詢謂詞

查詢謂詞是一個非成員謂詞,並在最開頭使用query作爲註解。它返回謂詞計算結果的所有元組,下面是一個簡單的示例:

query int getProduct(int x, int y) {
    x = 3 and
    y in [0 .. 2] and
    result = x * y
  }

編寫查詢謂詞而不是select子句的好處是我們可以在代碼的其他部分中調用謂詞。例如,我們可以在類中的特徵謂詞內部調用: 

query int getProduct(int x, int y) {
    x = 3 and
    y in [0 .. 2] and
    result = x * y
  }
  class MultipleOfThree extends int {
    MultipleOfThree() { this = getProduct(_, _) }
  }
  
  from MultipleOfThree m  
  select m

5、類(Classes)

我們可以在CodeQL中定義自己的類型,一個方法是定義一個類。

類提供了一種簡單的方法來重用和構造代碼。例如,我們可以:

  • 在類中定義成員謂詞
  • 定義子類以重寫成員謂詞

一個簡單的例子如下:

class OneTwoThree extends int {
    OneTwoThree() { // characteristic predicate
      this = 1 or this = 2 or this = 3
    }
  
    string getAString() { // member predicate
      result = "One, two or three: " + this.toString()
    }
  
    predicate isEven() { // member predicate
      this = 2
    }
  } 

6、Method內置方法

import java

from Method method
where method.hasName("main")
select method.getName(), method.getDeclaringType()

  • method.getName() 獲取的是當前方法的名稱
  • method.getDeclaringType() 獲取的是當前方法所屬class的名稱
  • method.hasName() 判斷是否有該方法

7、設置Source-Sink污點傳播流

在代碼自動化安全審計的理論當中,有一個最核心的三元組概念,就是(source,sink和sanitizer)。

  • source是指漏洞污染鏈條的輸入點。比如獲取http請求的參數部分,就是非常明顯的Source。
  • sink是指漏洞污染鏈條的執行點,比如SQL注入漏洞,最終執行SQL語句的函數就是sink(這個函數可能叫query或者exeSql,或者其它)。
  • sanitizer又叫淨化函數,是指在整個的漏洞鏈條當中,如果存在一個方法阻斷了整個傳遞鏈,那麼這個方法就叫sanitizer。

污點追蹤是CodeQL提供的一個非常強大的功能,也是進行代碼審計的基礎,CodeQL會分析代碼得到一張有向圖,參數和表達式就是裏面的節點,以下面一段代碼爲例子。

int func(int tainted) {
    int x = tainted;
    if (someCondition) {
        int y = x;
        callFoo(y);
    } else {
        return x;
    }

    return -1;
}

有了這樣的圖我們可以藉此分析代碼參數的流向來尋找漏洞,庫提供了TaintTracking::Configuration這個類,我們需要繼承這個類,通過覆蓋實現isSource方法和isSink方法來設置起始點和終點,方法會提供dataflow::node參數,我們通過把邏輯加在節點上來設置我們想要的起點和終點,這樣CodeQL分析變量的流向,如果發現了有變量從source到sink,就可能會發現潛在的漏洞,比如從getParameter到query,這可能就是一個sql注入。

CodeQL還提供了更強大的功能,isSanitizer()方法可以讓我們設置淨化方法,設置一個節點,當流到達這個節點後中斷,比如replace()這樣的過濾函數,CodeQL並不知道他的作用,我們可以中斷調用了這個方法的數據流來降低誤報。

同樣的,CodeQL並不能識別全部的變量傳遞,這時候我們可以通過isAdditionalTaintStep()方法告訴污點追蹤把這兩個節點連起來。

1)設置source

override predicate isSource(DataFlow::Node src) {}

// 通用的source入口規則
override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }

2)設置Sink

override predicate isSink(DataFlow::Node sink) {

  }

// 查找一個query()方法的調用點,並把它的第一個參數設置爲sink
override predicate isSink(DataFlow::Node sink) {
exists(Method method, MethodAccess call |
  method.hasName("query")
  and
  call.getMethod() = method and
  sink.asExpr() = call.getArgument(0)
)
}

3)FLow數據流

連通工作就是CodeQL引擎本身來完成的。我們通過使用config.hasFlowPath(source, sink)方法來判斷是否連通。

from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"

//我們傳遞給config.hasFlowPath(source, sink)我們定義好的source和sink,系統就會自動幫我們判斷是否存在漏洞了

4)使用jdbcTemplate.query方法的SQL注入

import java 
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.security.QueryInjection
import DataFlow::PathGraph

class VulConfig extends TaintTracking::Configuration {
    VulConfig() { this = "SqlinjectionConfig" }
    
    override predicate isSource(DataFlow::Node source) {
        source instanceof RemoteFlowSource
    }
    
    override predicate isSink(DataFlow::Node sink) {
        exists(Method method, MethodAccess call | 
            method.hasName("query")
            and call.getMethod() = method
            and sink.asExpr() = call.getArgument(0))
    }
}

from VulConfig vulconfig, DataFlow::PathNode source, DataFlow::PathNode sink
where vulconfig.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"

參考鏈接:

https://longlone.top/%E5%AE%89%E5%85%A8/%E5%AE%89%E5%85%A8%E7%A0%94%E7%A9%B6/codeql/2.CodeQL%E8%AF%AD%E6%B3%95/

0x4:codeql審計代碼原理圖

原理:編寫查詢語句找出代碼中的漏洞,codeql 內的編譯器調用 extractor 將 java 代碼編譯成可查詢的數據流,並以數據庫的形式搭配 ql 庫與編寫的查詢語句進行查詢,得出結果並生成報告。

CodeQL的查詢需要建立在一個數據庫的基礎之上,這個數據庫是通過Extractor模塊對源代碼進行分析、提取後得到的。數據庫建立之後,我們就可以使用CodeQL去探索源碼,並發現代碼中的一些已知問題。

  • 對於編譯型語言,CodeQL會在建立數據庫時“模擬”編譯的過程,在make等編譯工具鏈調用gcc等編譯器時,用相同的編譯參數調用extractor模塊取而代之,收集源代碼的所有相關信息,如AST抽象語法樹、函數變量類型、預處理器操作等等。
  • 對於解釋型語言,因爲沒有編譯器的存在,CodeQL會以跟蹤執行的方式獲取類似的信息。

使用CodeQL CLI對代碼倉庫運行分析後,我們就得到了一個“快照數據庫”(SnapshotDatabase),這個數據庫中存儲了代碼倉庫在特定時間點(數據庫建立時)的層級表示方式,包括

  • AST語法樹
  • CFG控制流程關係
  • DFG數據流向關係

在這個數據庫中,代碼中的每一個要素,比如函數定義(Function)、函數調用(FunctionCall)、宏調用(MacroInvocation)都是可以被檢索的實體。在這些基礎上,我們再編寫CodeQL語句對代碼進行分析。

查詢包括上圖查詢編譯部分和執行部分,我們的查詢會和庫一起交給編譯器編譯,編譯成功後會進行查詢,去數據庫中提取數據。

參考鏈接:

https://github.com/ASTTeam/CodeQL#02-codeql%E5%9F%BA%E7%A1%80 
https://www.sec-in.com/article/2043
https://cloud.tencent.com/developer/article/1645870
https://www.wangan.com/p/7fy7fg448fb3b026

 

三、CodeQL進階

0x1:一些開源的優秀CodeQL規則

codeql的核心在於它的規則。

0x2:提升挖掘效率的一些高級QL技巧

1、一些語法技巧

1)獲取具體QL類型

不確定使用什麼方式獲取目標時,除了通過查看AST,還可以通過詞getAQlClass()獲取調用它實體的具體QL類型。

from Expr e, Callable c
where e.getEnclosingCallable() = c
select e, e.getAQlClass()

2)儘可能縮小範圍

如下定義,如果項目代碼量很大,則非常耗時,

override predicate isSink(DataFlow::Node sink) {
    sink.asExpr().getParent() instanceof ReturnStmt
}

可以設置return語句在哪個函數中調用來縮小範圍,乃至其Type的全限定名,

override predicate isSink(DataFlow::Node sink) {
    sink.asExpr().getParent() instanceof ReturnStmt
    and sink.asExpr().getEnclosingCallable().hasName("xxxxx")
}

以某個方法的參數作爲source (添加了幾種過濾方式,第一個參數、該方法當前類的全限定名爲xxxx),

override predicate isSource(DataFlow::Node source) {
    exists(Parameter p |
        p.getCallable().hasName("readValue") and
        source.asParameter() = p and
        source.asParameter().getPosition() = 0
        and p.getCallable().getDeclaringType().hasQualifiedName("com.service.impl", "xxxxx")
    )
}

以某個實例的所有參數作爲source(`X1 x1 = new X1(a,b)`,這裏a、b作爲source),過濾:調用該實例的方法名稱爲`Caller`,實例類型名稱爲`X1`,

override predicate isSource(DataFlow::Node source) {
    exists(ClassInstanceExpr ma |
        source.asExpr() = ma.getAnArgument()
        and ma.getTypeName().toString() = "X1"
        and ma.getCaller().hasName("Caller")
    )
}

3)調用端點路徑

比如我們想知道方法A到方法G之間調用端點路徑,則可以使用edges謂詞,編寫如下所示,如果也想找覆寫的某個方法(如:接口實現類中的方法)可以將calls替換爲polyCalls,

import java

class StartMethod extends Method {
  StartMethod() { getName() = "main" }
}

class TargetMethod extends Method {
  TargetMethod() { getName() = "vulMain" }
}

query predicate edges(Method a, Method b) { a.calls(b) }

from TargetMethod end, StartMethod entryPoint
where edges+(entryPoint, end)
select end, entryPoint, end, "Found a path from start to target."

待分析的源碼如下,

package org.example;

import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        Main error = new Main();
        error.readValue("open -a Calculator");
    }

    private void readValue(String comm) throws IOException {
        vulMain(comm);
        taintVulMain(comm);
    }

    private void taintVulMain(String comm) {
        Runtime rt = Runtime.getRuntime();
        rt.getClass();
    }

    private void vulMain(String comm) throws IOException {
        Runtime rt = Runtime.getRuntime();
        rt.exec(comm);
    }


}

生成codeql數據庫,

cd /Users/zhenghan/Projects/codeql-home/hello_codeql
codeql database create hello_codeql-database -l=java -c="mvn clean install -file pom.xml" --source-root=/Users/zhenghan/Projects/codeql-home/hello_codeql

4)對某接口實現

主要是通過codeql自帶謂詞overridesOrInstantiates判斷該函數是否進行了重寫。

如下,就能獲取實現JSONStreamAware接口,重寫的方法

class JsonInterface extends Interface{
    JsonInterface(){
        this.hasQualifiedName("com.alibaba.fastjson", "JSONStreamAware")
    }

    Method getJsonMethod(){
        result.getDeclaringType() = this
    }
}

class CMethod extends Method{
    CMethod(){
        this.overridesOrInstantiates*(any(JsonInterface i).getJsonMethod())
    }
}

from CMethod m select m, m.getDeclaringType()

2、AdditionalTaintStep

在爲一些項目編寫規則查詢時,經常碰到數據流中斷的情況,下面列出經常碰到中斷的情況和解決方案。

1)setter和getter 

CodeQL爲減少誤報很多地方都需要我們根據相應場景自己連接數據流,比如getter。

這種情況需要將調用方法的對象(通過getQualifier謂詞獲取限定符)和調用方法的返回值連接起來。如下操作就是從get%方法訪問到它的限定符作爲附加步驟重新連接起來。

class GetSetTaintStep extends TaintTracking::AdditionalTaintStep{
    override predicate step(DataFlow::Node src, DataFlow::Node sink){
        exists(MethodAccess ma |
            (ma.getMethod() instanceof GetterMethod or ma.getMethod() instanceof SetterMethod or ma.getMethod().getName().matches("get%") or ma.getMethod().getName().matches("set%"))
            and
             src.asExpr() = ma.getQualifier()
            and sink.asExpr() = ma
            )
    }
}

2)mapper

使用mybatis通常將接口命名爲xxxxMapper或者xxxxDao這種形式,在xml配置文件中通過namespace指定其全限定名,當數據流需要經過數據庫查詢到這裏會斷開,那麼需要手動將其連接起來。

3)污染源作爲參數傳入

如下圖所示,instance作爲污染源,workNode也被污染,將其傳入t.setSceneKey爲t對象的sceneKey屬性賦值,那麼這裏t對象理應也是被污染的。但當我們將instance作爲source,return t作爲sink是獲取不到路徑的,

圖片來自https://xz.aliyun.com/t/10852#toc-8

要解決這個問題,需要加上額外3個步驟。

  1. 將調用方法的所有參數作爲source(圖中setSceneKey方法的workNode.getSceneKey()參數),將調用方法的對象作爲sink(圖中的t對象),代碼如下
class SrcTaintStep extends TaintTracking::AdditionalTaintStep{
    override predicate step(DataFlow::Node src, DataFlow::Node sink){
        exists(MethodAccess ma |
            (ma.getMethod() instanceof SetterMethod or ma.getMethod().getName().matches("set%"))
            and
                src.asExpr() = ma.getAnArgument()
            and sink.asExpr() = ma.getQualifier()
            )
    }
}
  1. instance的getter

  2. workNodeMapper

4)實例化

如下圖,將req傳入UploadFile中創建UploadFile對象,再將其傳入systemService.uploadFile方法中,這種情況,uploadFile對象應該是受污染的,但是默認情況下,我們想讓數據流進入systemService.uploadFile中是不行的,因爲在new UploadFile就已經斷開了。那麼就需要將其連接起來。

代碼如下,如果已經知道當前查詢大概斷的位置,可以縮小範圍,這裏將所有的都會連接起來,

class InstanceTaintStep extends TaintTracking::AdditionalTaintStep{
    override predicate step(DataFlow::Node src, DataFlow::Node sink){
      exists(ClassInstanceExpr cie | 
        // cie.getTypeName().toString() = "UploadFile"
         src.asExpr() = cie.getAnArgument()
          and sink.asExpr() = cie)
    }
}

參考鏈接:

https://github.com/ASTTeam/CodeQL#02-codeql%E5%9F%BA%E7%A1%80
https://xz.aliyun.com/t/10852#toc-8

 

四、CodeQL案例學習

這個章節,我們通過一些具體的項目,利用CodeQL挖掘復現一些已知的Nday漏洞,目的是提高對CodeQL的理解。

0x1:micro_service_seclab靶場漏洞復現

這是一個Java漏洞靶場,基於SpringBoot開發,目的是用來檢測SAST工具的準確性(關注漏報和誤報問題)的。可以用此靶場測試(CodeQL, CheckMarx, Fortify SCA)白盒檢測工具,根據預先埋點的漏洞,與測試結果進行對比,判斷在什麼地方存在誤報和漏報的問題。

1、靶場支持的漏洞

1)SQL注入

SQL注入這部分,會出現很多不同白盒寫法導致的SQL注入。

種類解釋僞代碼
String Source 輸入點是字符串類型 one(@RequestParam(value = "username") String username)
List<Long> 輸入點是Long泛型(用來測試誤報) longin(@RequestBody List<Long> user_list)
Optional<String> 新特性 optionalLike(@RequestParam(value = "username") Optional<String> optinal_username)
List<String> Source 輸入點是String泛型 in(@RequestBody List<String> user_list)
Object Source 對象類型 objectParam(@RequestBody Student user)
MyBatis注入 XML分離SQL檢測 myBatis(@RequestParam(value = "name") String name)
In類型注入 In類型注入 參照代碼
Like類型 Like類型注入 參照代碼
Lombok Lombok對注入漏洞的影響 參照代碼
MyBatis註解方式注入 MyBatis註解方式注入 參照代碼
Spring Data JPA JPA 方式 參照代碼

2)RCE命令執行

種類解釋僞代碼
processBuilder processBuilder導致的RCE --
Runtime.getRuntime().exec(args) Runtime.getRuntime().exec(args)導致的RCE --

3)FastJson反序列化漏洞

提供1.2.31版本的Fastjson供進行測試。

@RestController
@RequestMapping(value = "/fastjson")
public class FastJsonController {

    @PostMapping(value = "/create")
    public Teacher createActivity(@RequestBody String applyData,
                                  HttpServletRequest request, HttpServletResponse response){
        Teacher teachVO = JSON.parseObject(applyData, Teacher.class);
        return teachVO;
    }
}

4)SSRF漏洞

種類解釋僞代碼
url.openConnection() url.openConnection()引起的SSRF 參照代碼
Request.Get() Request.Get()引起的SSRF 參照代碼
OkHttpClient OkHttpClient引起的SSRF 參照代碼
DefaultHttpClient DefaultHttpClient引起的SSRF 參照代碼
url.openStream() url.openStream()引起的SSRF 參照代碼

5)XXE漏洞

種類解釋僞代碼
DocumentBuilderFactory DocumentBuilderFactory引起的SSRF 參照代碼

2、基本分析過程

CodeQL的核心引擎是不開源的,這個核心引擎的作用之一是幫助我們把micro-service-seclab轉換成CodeQL能識別的中間層數據庫。

然後我們需要編寫QL查詢語句來獲取我們想要的數據。

由於CodeQL開源了所有的規則和規則庫部分,所以我們能夠做的就是編寫符合我們業務邏輯的QL規則,然後使用CodeQL引擎去跑我們的規則,發現靶場的安全漏洞。

3、生成codeql數據庫

  • OS: Mac
  • Java JDK: 1.8.0_291, vendor: Oracle Corporation
  • Maven: Apache Maven 3.9.4 (dfbb324ad4a7c8fb0bf182e6d91b0ae20e3d2dd9)
cd /Users/zhenghan/Projects/codeql-home/micro_service_seclab
codeql database create micro-service-seclab-database -l=java -c="mvn clean install -file pom.xml" --source-root=/Users/zhenghan/Projects/codeql-home/micro_service_seclab

參考鏈接:

https://github.com/l4yn3/micro_service_seclab/
https://blog.gm7.org/%E4%B8%AA%E4%BA%BA%E7%9F%A5%E8%AF%86%E5%BA%93/02.%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1/03.codeql/01.codeql%E5%85%A5%E9%97%A8.html
https://www.freebuf.com/articles/web/283795.html

0x2:基於CodeQL分析XXL-job

xxl-job漏洞原理及編譯部署可以參閱這篇文章

生成codeql數據庫,

cd /Users/zhenghan/Projects/xxl-job_2.4.0
codeql database create xxl-job-database -l=java -c="mvn clean install -file pom.xml" --source-root=/Users/zhenghan/Projects/xxl-job_2.4.0

導入vscode, 

開始構建code ql語句,

import java

from MethodAccess ma, Method m 
where 
  m = ma.getMethod() and
  m.getName().regexpMatch("equals|getResourceAsStream|getResourceAsStream|getSystemResourceAsStream|ClassPathResource|BufferedInputStream|FileInputStream|getSystemResourceAsStream|getBundle") and
  not m.getDeclaringType().getName().matches("SecureUtil|WhiteListedClass")
select ma, "Risky method " + m.getName()

上述語句使用簡單的AST匹配模式檢測危險函數的方法,匹配等式判斷、配置讀取等函數。

通過查詢結果找到讀取配置的位置,即讀取配置函數的定位。 

繼續往上追溯loadProperties的調用源頭,

讀取配置的屬性包括addresses、accessToken、appname、addres、ip、port、logpath、logretentiondays等,如前文所述,accessToken身份繞過漏洞就是accessToken配置值和API請求的XXL-JOB-ACCESS-TOKEN一致通過的校驗。

繼續尋找與上述參數相關的代碼,

找到上述方法的構造出POST或者GET請求方法,即可構造出漏洞。

參考鏈接:

https://mp.weixin.qq.com/s?__biz=Mzg4Nzk3MTg3MA==&mid=2247484600&idx=1&sn=820df60a885378f30f4ffd9a407308a8&scene=21#wechat_redirect

0x3:基於CodeQL分析Shiro

1、maven編譯shiro相關問題

關於shiro源碼編譯及部署可以參閱這篇文章

下載安裝jdk1.7,https://www.oracle.com/java/technologies/javase/javase7-archive-downloads.html

/Library/Java/JavaVirtualMachines/jdk1.7.0_80.jdk/Contents/Home/bin/java -version

修改maven toolchains配置文件,修改maven使用jdk1.7進行編譯,

cat /usr/local/Cellar/maven/3.9.4/libexec/conf/toolchains.xml 

修改內容爲,本地的jdk的java_home,以及對應jdk版本,注意這裏可以寫多個jdk版本,只要本地有:

  <toolchain>
    <type>jdk</type>
    <provides>
      <version>1.8</version>
      <vendor>sun</vendor>
    </provides>
    <configuration>
      <jdkHome>/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home/</jdkHome>
    </configuration>
  </toolchain>

    <toolchain>
    <type>jdk</type>
    <provides>
      <version>1.7</version>
      <vendor>sun</vendor>
    </provides>
    <configuration>
      <jdkHome>/Library/Java/JavaVirtualMachines/jdk1.7.0_80.jdk/Contents/Home/</jdkHome>
    </configuration>
  </toolchain>

拷貝toolchains.xml文件,

cp /usr/local/Cellar/maven/3.9.4/libexec/conf/toolchains.xml ~/.m2/toolchains.xml

接下來修改根目錄下pom.xml文件中toolchains配置,修改對應版本爲剛纔mvn的配置文件中指定一個版本,這裏必須是剛纔配置的jdk版本中有的版本。 

驗證一下maven編譯通過。

運行codeql指令生成數據庫,

cd /Users/zhenghan/Projects/shiro-shiro-root-1.2.4

codeql database create shiro-samples-database -l=java -c="mvn -e clean install -Dmaven.test.skip=true -pl samples -am" --source-root=/Users/zhenghan/Projects/shiro-shiro-root-1.2.4

參考鏈接:

https://www.anquanke.com/post/id/256967
https://blog.csdn.net/gzt19881123/article/details/106487550 
https://blog.csdn.net/qq_38376348/article/details/108962790
https://blog.csdn.net/yiqiu3812/article/details/103298980
https://stackoverflow.com/questions/40354942/maven-build-error-after-setting-toolchain-right
https://blog.csdn.net/qq_20042935/article/details/106540753
https://www.anquanke.com/post/id/255721#h2-9

0x4:基於CodeQL分析Log4j

參考鏈接:

https://www.anquanke.com/post/id/255721
https://www.freebuf.com/articles/web/318141.html
https://mp.weixin.qq.com/s/JYco8DysQNszMohH6zJEGw

0x5:基於CodeQL分析SecExample

參考鏈接:

https://github.com/tangxiaofeng7/SecExample 

 

五、CodeQL和大模型技術的結合機會

參考鏈接:

https://mp.weixin.qq.com/s/xlUWn2oWU51NVkgB157pRw
https://mp.weixin.qq.com/s/Ix2lArBzaCAJr5nyGolCwQ
https://mp.weixin.qq.com/s/leLFECUaNOGbjsN_8mcXrQ
https://mp.weixin.qq.com/s/Masyfq12cjaM4Zn6qxvGoA
https://mp.weixin.qq.com/s/QIKvRzNlAKiqh_UMOMfDdg
https://www.trendmicro.com/ja_jp/devops/23/e/chatgpt-security-vulnerabilities.html
https://blog.csdn.net/ljqclqjc/article/details/133899983

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章