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

 

posted @ 2023-12-01 11:13  郑瀚Andrew  阅读(2545)  评论(0编辑  收藏  举报