Featured image of post Codeql使用学习

Codeql使用学习

Codeql使用学习

安装

https://github.com/github/codeql-cli-binaries/releases去这里下载codeql执行程序,然后设置环境变量,指定到codeql文件夹

image-20251217195545208

这样就设置成功了

下载SDK:https://github.com/github/codeql,下载后解压重命名为ql文件夹,与前面codeql同目录

image-20251217195821377

vscode配合codeql

下载插件后,配置codeql文件地址

image-20251217201714304

接下来创建数据库来测试一下

这里选择https://github.com/l4yn3/micro_service_seclab/这个项目来测试

接下来创建数据库,命令一般是

1
codeql database create <数据库名> --language=<语言标识符> --source-root=<源码路径>

如果是maven项目的话,还需要加上command参数

1
--command="mvn clean install"

这里我们直接在下载的这个靶场的目录下创建,无需指定源码路径了

1
codeql database create codeqltest --language=java

执行成功的话会出现codeqltest文件夹,这里java版本要是jdk1.8的

image-20251217210734868

现在打开vscode

image-20251217211036362

添加数据库,这里打开要在codeql同目录下打开,然后上面语言选择java

然后可以在下面的queries里面添加我们想要的ql查询语句进行查询,我们回到sdk的目录,在java/ql/examples下面创建一个demo.ql

image-20251217212031018

语法

codeql语法有点像sql,CodeQL 引擎的作用就是帮我们把源码转换为 CodeQL 能识别的数据库,所以我们能做的就是编写 QL 规则,再通过其引擎来运行我们的规则,这样就可以达到一个自动审计的功能

基本语法

1
2
3
4
5
import java
 
from int i
where i = 1
select i

from int i,表示我们定义一个变量i,它的类型是int,表示我们获取所有的int类型的数据

where i = 1, 表示当i等于1的时候,符合条件

select i,表示输出i

image-20251217212835104

所以codeql的语法结构为

1
2
3
from [datatype] var
where condition(var = something)
select var

类库

我们经常会用到的ql类库大体如下:

名称 解释
Method 方法类,Method method表示获取当前项目中所有的方法
MethodAccess 方法调用类,MethodAccess call表示获取当前项目当中的所有方法调用
Parameter 参数类,Parameter表示获取当前项目当中所有的参数

在现在的版本中MethodAccess 被重名成 MethodCall

我们现在可以来查询一下我们这个项目的所有方法

1
2
3
4
import java
 
from Method kkk
select kkk

image-20251217213942659

可以查询到超级多方法,我们可以过滤出名字为 getStudent 的方法名称

1
2
3
4
5
import java
 
from Method k
where k.hasName("getStudent")
select k.getName(), k.getDeclaringType()

image-20251217214232707

1
2
3
k.hashName() 判断名字是否匹配
k.getName() 获取的是当前方法的名称
k.getDeclaringType() 获取的是当前方法所属class的名称

谓词

如果限制条件比较多,where 语句就会很冗长。CodeQL提供一种机制可以帮助我们把很长的查询语句封装成函数,而这个函数,就叫谓词。

比如上面的案例,我们可以写成如下,获得的结果跟上面是一样的:

1
2
3
4
5
6
7
8
9
import java
 
predicate isStudent(Method k) {
    k.getName()="getStudent"
}
 
from Method k
where isStudent(k)
select k.getName(), k.getDeclaringType()

source和sink

source、sink以及sanitizer这个学过静态分析的不可能不知道,这里不多说了

只有当source和sink同时存在,并且从source到sink的链路是通的,才表示当前漏洞是存在的。

image-20251217221016177

显然这里是一个source

设置source

在CodeQL中我们通过以下方法来设置Source

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

由于上面的source是http请求参数

我们设置的source就为

1
override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }

RemoteFlowSource 是 CodeQL 标准库中预定义的 “远程数据源” 类,比如HTTP 请求参数,用户输入以及其他外部输入等

这是SDK自带的规则,里面包含了大多常用的Source入口。我们使用的SpringBoot也包含在其中,可以直接使用。

注: instanceof 语法是CodeQL提供的语法,这里就是检查获得的 src 是否为 RemoteFlowSource

这里测试输出所有可能的source点,其实就是有http请求参数

1
2
3
4
5
6
7
8
import java
import semmle.code.java.dataflow.FlowSources

predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }

from DataFlow::Node src
where isSource(src)
select src, "是一个Source点"

image-20251217225012897

设置sink

在CodeQL中我们通过以下方法来设置Sink

1
override predicate isSink(DataFlow::Node sink) {}

在实际中,我们最后都是触发到某个恶意方法,如 getter,setter,所以 sink 应该是个方法,假设我们这里的sink 点是个query方法(Method)的调用(MethodCall),所以我们设置Sink为:

1
2
3
4
5
6
7
8
override predicate isSink(DataFlow::Node sink) {
exists(Method method, MethodCall call |
      method.hasName("query")
      and
      call.getMethod() = method and
      sink.asExpr() = call.getArgument(0)
	  )
}

这里我们使用了exists子查询,这个是CodeQL谓词语法里非常常见的语法结构,它根据内部的子查询返回true or false,来决定筛选出哪些数据。

sink.asExpr() = call.getArgument(0):将 sink 节点转换为表达式,并检查它是否等于 call 的第一个参数

故上面sink语句的作用是查找一个query()方法的调用点,并把它的第一个参数设置为sink

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import java
import semmle.code.java.dataflow.FlowSources

predicate isSink(DataFlow::Node sink) {
  exists(Method method, MethodCall call |
    method.hasName("query") and
    call.getMethod() = method and
    sink.asExpr() = call.getArgument(0)
  )
}

from DataFlow::Node sink
where isSink(sink)
select sink, "传入 query() 方法的第一个参数是敏感点"

image-20251217225308882

点进第一个实际上就确实是sql注入漏洞点

1
jdbcTemplate.query(sql, ROW_MAPPER);

flow数据流

确定了上面的source和sink,现在要找中间的链子了

这个连通工作就是CodeQL引擎本身来完成的。我们通过使用config.hasFlowPath(source, sink)方法来判断是否连通。

1
2
3
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,系统就会自动帮我们判断是否存在漏洞了。

source.getNode():获取源节点的底层语法树节点(AST Node),显示漏洞源头在代码中的具体位置

实操

现在来试着探测sql注入的可行性,这里l3yx师傅在freebuff上那种写法在新版已经被弃用了,在新版 CodeQL中,数据流分析(DataFlow)和污点跟踪(TaintTracking)的 API 发生了重大变化。旧的 TaintTracking::Configuration 类已被弃用,取而代之的是模块化(Modular) 的写法。

然后我让gemini生成一个新版可用的ql代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
 * @id java/examples/vuldemo
 * @name Sql-Injection
 * @description Sql-Injection
 * @kind path-problem
 * @problem.severity warning
 */

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

// 1. 定义配置模块,实现 DataFlow::ConfigSig 签名
module VulConfig implements DataFlow::ConfigSig {
    
    // 定义 Source (污染源)
    predicate isSource(DataFlow::Node source) {
        source instanceof RemoteFlowSource
    }

    // 定义 Sink (汇聚点)
    predicate isSink(DataFlow::Node sink) {
        exists(MethodCall call |
            // 查找方法名为 "query" 的调用
            call.getMethod().hasName("query") and
            // sink 是该调用的第一个参数
            sink.asExpr() = call.getArgument(0)
        )
    }
}

// 2. 使用 TaintTracking::Global 模板生成具体的分析模块
module VulFlow = TaintTracking::Global<VulConfig>;

// 3. 从生成的模块中导入 PathGraph (解决 import DataFlow::PathGraph 报错的问题)
import VulFlow::PathGraph

from VulFlow::PathNode source, VulFlow::PathNode sink
where VulFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "Detected SQL Injection from $@", source.getNode(), "source"

image-20251218201722852

然后左上角点成alert就能看到完整的gadget

image-20251218201756697

这里解释一下后面的代码

1
2
3
4
5
6
// 3. 从生成的模块中导入 PathGraph (解决 import DataFlow::PathGraph 报错的问题)
import VulFlow::PathGraph

from VulFlow::PathNode source, VulFlow::PathNode sink
where VulFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "Detected SQL Injection from $@", source.getNode(), "source"
  • DataFlow::Node:代表代码中的一个点(比如一个变量、一个参数)。它关注的是代码位置
  • PathNode:代表数据流路径图中的一个节点。它包裹了 DataFlow::Node,但它专为路径追踪设计。
  • VulFlow::flowPath(source, sink)判断两个 PathNode(起点和终点)之间是否存在一条完整的、可达的污点传播路径。相比与flow函数,flow函数只告诉你 node1 能流到 node2,是个布尔值(True/False),不包含中间经过了谁。
  • 最后PathGraph用来在结果中生成edges这个谓词,PathGraph 模块自动把 VulFlow 计算出的所有数据流关系转换成图的边(edges)。必须引用这个,不然最后看不到gadget

在CodeQL中存在两种数据流:

  • 本地数据流:本地数据流是指单个方法或可调用函数内的数据流。本地数据流通常比全局数据流更简单、更快速、更精确,并且足以应对许多查询。
  • 全局数据流:全局数据流跟踪整个程序的数据流,因此比局部数据流更强大。然而,全局数据流的精确度低于局部数据流,并且分析通常需要更多的时间和内存。

这里我们当然先学习全局数据流,本地数据流相对而言效果不是太好,我们需要通过实现签名DataFlow::ConfigSig和应用模块来使用全局数据流库DataFlow::Global<ConfigSig>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import java
import semmle.code.java.dataflow.DataFlow

module MyFlowConfiguration implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node source) {
    ...
  }

  predicate isSink(DataFlow::Node sink) {
    ...
  }
}

module MyFlow = DataFlow::Global<MyFlowConfiguration>;
  • isSource– 定义数据流出的位置。
  • isSink– 定义数据流向何处。
  • isBarrier– 可选,定义数据流被阻止的位置。
  • isAdditionalFlowStep– 可选,添加额外的流程步骤。

如果想使用污点追踪,需要使用TaintTracking:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import java
import semmle.code.java.dataflow.TaintTracking

module MyFlowConfiguration implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node source) {
    ...
  }

  predicate isSink(DataFlow::Node sink) {
    ...
  }
}

module MyFlow = TaintTracking::Global<MyFlowConfiguration>;

最后使用使用谓词执行数据流分析:flow(DataFlow::Node source, DataFlow::Node sink)

1
2
3
from DataFlow::Node source, DataFlow::Node sink
where MyFlow::flow(source, sink)
select source, "Data flow to $@.", sink, sink.toString()

总结上面的,得到完整代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.FlowSources

module SourceToSinkConfig implements DataFlow::ConfigSig {
  predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }

  predicate isSink(DataFlow::Node sink) {
    exists(Method method, MethodCall call |
      method.hasName("query") and
      call.getMethod() = method and
      sink.asExpr() = call.getArgument(0)
    )
  }
}

module SourceToSink = TaintTracking::Global<SourceToSinkConfig>;

from DataFlow::Node source, DataFlow::Node sink
where SourceToSink::flow(source, sink)
select source, "flow to", sink

image-20251218195737221

点前面两个确实是对的,但是第三个出现了误报了

image-20251218200224706

现在来解决上面的误报问题,因为我们最开始写的那个也会误报这个List<Long>的参数,因为我们的查询没有设置参数类型,这里用isSanitizer来实现排除误报,要是判断到传入类型是基础类型、数字类型、泛型数字类型时,就切断数据流

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 predicate isBarrier(DataFlow::Node sanitizer) {
    sanitizer.getType() instanceof PrimitiveType
    or
    sanitizer.getType() instanceof BoxedType
    or
    sanitizer.getType() instanceof NumberType
    or
    exists(ParameterizedType pt |
      sanitizer.getType() = pt and pt.getTypeArgument(0) instanceof NumberType // 这里的 ParameterizedType 代表所有泛型,判断泛型当中的传参是否为 Number 
    )
  }

直接添加上去,查询发现确实去掉了前下的误报的链子

image-20251218203957858

小技巧

递归

CodeQL里面的递归调用语法是:在谓词方法的后面跟*或者+,来表示调用0次以上和1次以上(和正则类似),0次会打印自己。

例如:

1
2
3
4
5
import java
 
from Class classes
where classes.getName().toString() = "innerTwo"
select classes.getEnclosingType().getEnclosingType().getEnclosingType()   // getEnclosingtype获取作用域

我们想要调用classes的上层作用域,一直写这玩意很麻烦,我们在调用方法后面加*(从自身开始调用)或者+(从上一级开始调用),来解决此问题。

1
2
3
from Class classes
where classes.getName().toString() = "innerTwo"
select classes.getEnclosingType+()   // 获取作用域

+ 是从上一级开始调用

* 是从自身开始调用

强制类型转换

在 CodeQL 中可以用 getType() 来对返回结果做强制类型转换

查询下当前数据库中所有的参数及其类型

1
2
3
4
import java
 
from Parameter param
select param, param.getType()

后面加上指定的参数类型就会查询对应剩下的类型

1
2
3
4
import java
 
from Parameter param
select param, param.getType().(IntegeralType)
使用 Hugo 构建
主题 StackJimmy 设计