参考

CodeQL入门

CodeQL入坟篇

New dataflow API for writing custom CodeQL queries

CodeQL踩坑日记and规则分享

前言

下面为笔者初学codeql所记的笔记,涉及到的所有ql代码都是用于查询靶场中对应漏洞的

CodeQL 基本语法

QL 语法

用的是这个靶场 —— micro_service_seclab:,同理其实 JoyChou93 师傅之前所设计的靶场,也是可以用来做 CodeQL 练习的。

  • 添加对应 database
1
codeql database create E:\mycode\codeql\CodeQL-Practice --language="java" --source-root=E:\safety\CodeAudit\micro_service_seclab-main --command="mvn clean package -Dmaven.test.skip=true"

--command="mvn clean package -Dmaven.test.skip=true"告诉 CodeQL 如何编译源码

  • mvn clean package:用 Maven 清理并打包项目。
  • -Dmaven.test.skip=true:跳过测试代码的编译和执行(节省时间,避免测试失败阻碍)。

CodeQL 在构建数据库时,需要项目能成功编译,这样它才能捕获所有类、方法、调用关系等语义信息。
这里它会调用 Maven 编译过程来生成字节码并收集编译信息。

CodeQL的核心引擎是不开源的,这个核心引擎的作用之一是帮助我们把micro-service-seclab转换成CodeQL能识别的中间层数据库。

然后我们需要编写QL查询语句来获取我们想要的数据。

img

正如这张图描述的,由于CodeQL开源了所有的规则和规则库部分,所以我们能够做的就是编写符合我们业务逻辑的QL规则,然后使用CodeQL引擎去跑我们的规则,发现靶场的安全漏洞。

我们来简单地介绍一下本案例涉及到的CodeQL的基本语法。

基本语法包含3个部分。

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

结合 ql 的语法,我们尝试获取 micro-service-seclab 项目当中定义的所有方法:

1
2
3
4
import java

from Method method
select method

我们再通过 Method 类内置的一些方法,把结果过滤一下。比如我们获取名字为 getStudent 的方法名称。

1
2
3
4
5
import java

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

image-20250823162140621

1
2
method.getName() // 获取的是当前方法的名称
method.getDeclaringType() / 获取的是当前方法所属class的名称。

谓词

和SQL一样,where部分的查询条件如果过长,会显得很乱。CodeQL提供一种机制可以让你把很长的查询语句封装成函数。

这个函数,就叫谓词。

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

1
2
3
4
5
6
7
8
9
import java

predicate isStudent(Method method) {
exists(|method.hasName("getStudent"))
}

from Method method
where isStudent(method)
select method.getName(), method.getDeclaringType()

语法解释

predicate 表示当前方法没有返回值。
exists 子查询,是CodeQL谓词语法里非常常见的语法结构,它根据内部的子查询返回 true or false,来决定筛选出哪些数据。

image-20250823163035848

设置 Source 和 Sink

什么是source和sink

在代码自动化安全审计的理论当中,有一个最核心的三元组概念,就是(source,sink和sanitizer)。

source 是指漏洞污染链条的输入点。比如获取http请求的参数部分,就是非常明显的Source。

sink 是指漏洞污染链条的执行点,比如SQL注入漏洞,最终执行SQL语句的函数就是sink(这个函数可能叫query或者exeSql,或者其它)。

sanitizer又叫净化函数,是指在整个的漏洞链条当中,如果存在一个方法阻断了整个传递链,那么这个方法就叫sanitizer。

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

img

  • 设置 Source

在 micro_service_seclab 中,对应的 Source 举个例子,SQL 注入的代码

1
2
3
4
@RequestMapping(value = "/one")  
public List<Student> one(@RequestParam(value = "username") String username) {
return indexLogic.getStudent(username);
}

对应 CodeQL 当中的 Source

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

RemoteFlowSource 类(在semmle.code.java.dataflow.FlowSources)中定义)表示可能由远程用户控制的数据流源

这里就是说:把所有远程输入都当作source

这里这段代码的传参比较简单,但其实传参如果复杂,比如是一个类的情况下,也是类似的

在下面的代码中,source就是Student user(user为Student类型,这个不受影响)。

1
2
3
4
@PostMapping(value = "/object")
public List<Student> objectParam(@RequestBody Student user) {
return indexLogic.getStudent(user.getUsername());
}
  • 设置 Sink

在本案例中,我们的sink应该为query方法(Method)的调用(MethodAccess),所以我们设置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子查询语法,格式为exists(Obj obj| somthing), 上面查询的意思为:查找一个query()方法的调用点,并把它的第一个参数设置为sink。

在靶场系统(micro-service-seclab)中,sink就是:

1
jdbcTemplate.query(sql, ROW_MAPPER);

因为我们测试的注入漏洞,当source变量流入这个方法的时候,才会发生注入漏洞!

Flow数据流

在设置完 Source 和 Sink 之后,我们需要确认 Source 到 Sink 是能够走通的,这一段的连通工作就是 CodeQL 引擎本身来完成的。我们通过 config.hasFlowPath(source, sink) 方法来判断是否连通。

比如如下代码:

1
2
3
4
5
module VulFlow = TaintTracking::Global<VulConfig>;

from VulFlow::PathNode source, VulFlow::PathNode sink
where VulFlow::flowPath(source, sink)
select source, sink, "发现潜在的SQL注入漏洞"

我们传递给 VulFlow::flowPath(source, sink) 我们定义好的source和sink,系统就会自动帮我们判断是否存在漏洞了。

CodeQL 语句优化

初步成果

经过整理之后的 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
/** @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

/** 定义数据流配置 */
module VulConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node src) {
src 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)
)
}
}

/** 用全局 taint tracking 建立路径 */
module VulFlow = TaintTracking::Global<VulConfig>;

from VulFlow::PathNode source, VulFlow::PathNode sink
where VulFlow::flowPath(source, sink)
select source, sink, "发现潜在的SQL注入漏洞"

VulConfig:自定义一个污点追踪配置。

extends TaintTracking::Configuration:继承 CodeQL 提供的污点分析框架。

构造函数里 this = "SqlInjectionConfig":给这个配置起个名字。

image-20250828203902454

误报解决

结果里面存在误报

image-20250828204323993

这个方法的参数类型是List<Long>,不可能存在注入漏洞。

这说明我们的规则里,对于List<Long>,甚至List<Integer>类型都会产生误报,source误把这种类型的参数涵盖了。

我们需要采取手段消除这种误报。

这个手段就是isSanitizer

image

isSanitizer是CodeQL的类TaintTracking::Configuration提供的净化方法。它的函数原型是:

override predicate isSanitizer(DataFlow::Node node) {}

在CodeQL自带的默认规则里,对当前节点是否为基础类型做了判断。

override predicate isSanitizer(DataFlow::Node node) {
node.getType() instanceof PrimitiveType or
node.getType() instanceof BoxedType or
node.getType() instanceof NumberType
}

表示如果当前节点是上面提到的基础类型,那么此污染链将被净化阻断,漏洞将不存在。

由于CodeQL检测SQL注入里的isSanitizer方法,只对基础类型做了判断,并没有对这种复合类型做判断,才引起了这次误报问题。

那我们只需要将这种复合类型加入到isSanitizer方法,即可消除这种误报。

在新版本里面isSanitizer方法已经全部统一到isBarrier方法下了

1
2
3
4
5
6
7
8
9
predicate isBarrier(DataFlow::Node node) {
node.getType() instanceof PrimitiveType or // 基本类型(int, char等)
node.getType() instanceof BoxedType or // 包装类型(Integer, Boolean等)
node.getType() instanceof NumberType or // 数字类型
exists(ParameterizedType pt| // 泛型类型
node.getType() = pt and
pt.getTypeArgument(0) instanceof NumberType // 泛型参数为数字类型
)
}

以上代码的意思为:如果当前node节点的类型为基础类型,数字类型和泛型数字类型(比如List)时,就切断数据流,认为数据流断掉了,不会继续往下检测。
重新执行query,我们发现,刚才那条误报已经被成功消除啦。

image-20250828212424083

漏报解决

我们发现,如下的SQL注入并没有被CodeQL捕捉到。

1
2
3
4
5
public List<Student> getStudentWithOptional(Optional<String> username) {
String sqlWithOptional = "select * from students where username like '%" + username.get() + "%'";
//String sql = "select * from students where username like ?";
return jdbcTemplate.query(sqlWithOptional, ROW_MAPPER);
}

漏报理论上讲是不能接受的。如果出现误报我们还可以通过人工筛选来解决,但是漏报会导致很多漏洞流经下一个环节到线上,从而产生损失。

那我们如果通过CodeQL来解决漏报问题呢?答案就是通过 isAdditionalTaintStep 方法。

实现原理就一句话:断了就强制给它接上。

img

1
2
3
4
5
6
isAdditionalTaintStep方法是CodeQL的类TaintTracking::Configuration提供的的方法,它的原型是:

override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {}

它的作用是将一个可控节点
A强制传递给另外一个节点B,那么节点B也就成了可控节点。

这里由于 Optional 这种类型的使用没有在 CodeQL 的语法库里,我们需要强制让 username 流转到username.get(),这样 username.get() 就变得可控了。这样应该就能识别出这个注入漏洞了

在新版的ql中使用 isAdditionalFlowStep 而不是污点跟踪谓词 isAdditionalTaintStep

完整代码:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* @id java/examples/demo
* @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

predicate isTaintedString(Expr expSrc, Expr expDest) {
exists(Method method, MethodCall call, MethodCall call1|
expSrc = call1.getArgument(0) and expDest = call and call.getMethod() = method
and method.hasName("get") and method.getDeclaringType().toString() = "Optional<String>"
and call1.getArgument(0).getType().toString() = "Optional<String>"
)
}

/** 定义数据流配置 */
module VulConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node src) {
src 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)
)
}

predicate isBarrier(DataFlow::Node node) {
node.getType() instanceof PrimitiveType or
node.getType() instanceof BoxedType or
node.getType() instanceof NumberType or
exists(ParameterizedType pt| node.getType() = pt and pt.getTypeArgument(0) instanceof NumberType ) // 这里的 ParameterizedType 代表所有泛型,判断泛型当中的传参是否为 Number 型
}

predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
isTaintedString(node1.asExpr(), node2.asExpr())
}
}

/** 用全局 taint tracking 建立路径 */
module VulFlow = TaintTracking::Global<VulConfig>;

import VulFlow::PathGraph

from VulFlow::PathNode source, VulFlow::PathNode sink
where VulFlow::flowPath(source, sink)
select source.getNode(), source, sink, "source"

Lombok 插件漏报

Lombok 的注解并不会直接被 CodeQL 所解析,导致其中的中间链路会“中道崩殂”,我们用以下方法来解决。

解决方法 ①

使用 maven-delombok,在 pom.xml 中添加以下代码,重新编译即可。(推荐)

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
<build>  
<sourceDirectory>target/generated-sources/delombok</sourceDirectory>
<testSourceDirectory>target/generated-test-sources/delombok</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-maven-plugin</artifactId>
<version>1.18.20.0</version>
<executions>
<execution>
<id>delombok</id>
<phase>generate-sources</phase>
<goals>
<goal>delombok</goal>
</goals>
<configuration>
<addOutputDirectory>false</addOutputDirectory>
<sourceDirectory>src/main/java</sourceDirectory>
</configuration>
</execution>
<execution>
<id>test-delombok</id>
<phase>generate-test-sources</phase>
<goals>
<goal>testDelombok</goal>
</goals>
<configuration>
<addOutputDirectory>false</addOutputDirectory>
<sourceDirectory>src/test/java</sourceDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>

</build>

解决办法 ②

CodeQL官方的issue里面,有人给出了这个问题的解决办法 https://github.com/github/codeql/issues/4984

1
2
3
4
5
6
7
8
9
10
11
12
# get a copy of lombok.jar
wget https://projectlombok.org/downloads/lombok.jar -O "lombok.jar"
# run "delombok" on the source files and write the generated files to a folder named "delombok"
java -jar "lombok.jar" delombok -n --onlyChanged . -d "delombok"
# remove "generated by" comments
find "delombok" -name '*.java' -exec sed '/Generated by delombok/d' -i '{}' ';'
# remove any left-over import statements
find "delombok" -name '*.java' -exec sed '/import lombok/d' -i '{}' ';'
# copy delombok'd files over the original ones
cp -r "delombok/." "./"
# remove the "delombok" folder
rm -rf "delombok"

没有特别明白这个应该在哪个目录下执行命令。

上面的代码,实现的功能是:去掉代码里的lombok注解,并还原setter和getter方法的java代码,从而使CodeQL的Flow流能够顺利走下去,
从而检索到安全漏洞。

持续工程化

到此为止,我们编写了SQL注入的查询语句,消除了误报和漏报问题。当前的规则已经能够适应micro-service-seclab项目啦。

因为我们的micro-service-seclab项目,是按照标准生成的微服务结构,那么我们可以使用这个ql规则去跑其他的项目,来自动化检测其它项目,从而做到自动化检测,提高安全检测效率。

CodeQL除了提供VSCode的检测插件,也提供了大量的命令行,来实现项目的集成检测。

比如:

1
codeql database create ~/CodeQL/databases/micro-service-seclab  --language="java"  --command="mvn clean install --file pom.xml -Dmaven.test.skip=true" --source-root="~/Code/micro-service-seclab/"

我们通过上面语句自动生成codeql的中间数据库(database)。

1
codeql database analyze /CodeQL/databases/micro-service-seclab /CodeQL/ql/java/ql/examples/demo --format=csv --output=/CodeQL/Result/micro-service-seclab.csv --rerun

我们通过上面的语句可以执行我们写好的QL文件,然后将结果输出到指定csv文件。

利用这两条命令,结合我们自己的程序,我们就能批量的对我们所有的项目做自动化检测了。

CodeQL进阶

上面我们完成了对一个简单的SQL注入漏洞的自动化检测工作。

如果你对上面的语法的一些东西还是有些不解,或者想去阅读SDK规则的代码,可以继续往下看,希望我对一些重点语法的总结
能够帮到你。

用 instanceof 替代复杂查询语句问题

我们在上面的案例当中看到了instanceof, 如果我们去看ql自带的规则库,会发现大量的instanceof语句。

image

instanceof是用来优化代码结构非常好的语法糖。

我们都知道,我们可以使用exists(|)这种子查询的方式定义source和sink,但是如果source/sink特别复杂(比如我们为了规则通用,可能要适配springboot, Thrift RPC,Servlet等source),如果我们把这些都在一个子查询内完成,比如 condition 1 or conditon 2 or condition 3, 这样一直下去,我们可能后面都看不懂了,更别说可维护性了。
况且有些情况如果一个子查询无法完成,那么就更没法写了。

instanceof给我们提供了一种机制,我们只需要定义一个abstract class,比如这个案例当中的:

1
2
3
4
5
/** A data flow source of remote user input. */
abstract class RemoteFlowSource extends DataFlow::Node {
/** Gets a string that describes the type of this remote flow source. */
abstract string getSourceType();
}

然后在isSource方法里进行instanceof,判断src是 RemoteFlowSource类型就可以了。

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

学过java的人可能会很费解,我们继承了一个abstract抽象类,连个实现方法都没有,怎么就能够达到获取各种source的目的呢?

CodeQL和Java不太一样,只要我们的子类继承了这个RemoteFlowSource类,那么所有子类就会被调用,它所代表的source也会被加载。

我们在 RemoteFlowSource定义下面会看到非常多子类,就是这个道理,它们的结果都会被用and串联加载。

image

递归问题

递归调用可以帮助我们解决一类问题:就是我们不确定这个方法我们需要调用多少次才能得到我们的结果,这个时候我们就可以用递归调用。

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

在Java语言里,我们可以使用class嵌套class,多个内嵌class的时候,我们需要知道最外层的class是什么怎么办?
比如如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class StudentService {

class innerOne {
public innerOne(){}

class innerTwo {
public innerTwo(){}

public String Nihao() {
return "Nihao";
}
}
public String Hi(){
return "hello";
}
}

}

我们想要根据innerTwo类定位到最外层的StudentService类,怎么实现?

按照非递归的写法,我们可以这样做:

1
2
3
4
5
import java

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

我们通过连续2次调用getEnclosingType方法是能够拿到最外层的StudentService的。

但是正如我们所说,实际情况是我们并不清楚一共有多少层嵌套,而且多个文件可能每个的嵌套数量都不一样,我们没法用确定的调用次数来解决此问题,这个时候我们就需要使用递归的方式解决。

我们在调用方法后面加*(从本身开始调用)或者+(从上一级开始调用),来解决此问题。

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

imageimage

我们也可以自己封装方法来递归调用

1
2
3
4
5
6
7
8
9
import java

RefType demo(Class classes) {
result = classes.getEnclosingType()
}

from Class classes
where classes.getName().toString() = "innerTwo"
select demo*(classes) // 获取作用域

image

强制类型转换问题

在CodeQL的规则集里,我们会看到很多类型转换的代码,比如:

image

这里是对getType()的返回结果做强制类型转换。其实CodeQL当中的强制类型转换,理解成filter更贴切一点,它的意思是将前面的结果符合RefType的数据都留下,不符合的都去掉。

以上class 继承了Parameter,那么getType()目的就是获取项目中所有的参数的type信息。

我们用如下QL语句做个测试:

1
2
3
4
import java

from Parameter param
select param, param.getType()

以上代码的含义是打印所有方法参数的名称和类型。

image

我们看到一共有233条结果,并且结果当中含有String,int和其他自定义类型,这是我们没有做任何强制类型转换的结果。
然后我们试着执行:

1
2
3
4
import java

from Parameter param
select param, param.getType().(RefType)

强制转换成RefType,意思就是从前面的结果当中过滤出RefType类型的参数。RefType是什么?引用类型,说白了就是去掉int等基础类型之后的数据。

image

数据只有181条了。

更直观的测试,我们可以过滤保留所有的数值类型。

1
2
3
4
import java

from Parameter param
select param, param.getType().(IntegralType)

image

内存爆炸解决办法

如果你现在是 **新版本 CodeQL CLI (>=2.9.x)**:

  • 数据库创建时:

    1
    2
    export SEMMLE_JAVA_EXTRACTOR_JVM_ARGS="-Xmx4g"
    codeql database create db --language=java -s . -M 4000
  • 查询时用:

    1
    codeql database analyze db query.ql --ram=4000

如果你还是老版本:

  • 数据库创建时只能用:

    1
    codeql database create db --language=java -s . -J=-Xmx4g

FastJson

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
/**
* @id java/examples/fastjson
* @name Fastjson
* @description fastjson
* @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

module FastjsonVulconfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node src) {
src instanceof RemoteFlowSource
}

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

module fastjsonFlow = TaintTracking::Global<FastjsonVulconfig>;

import fastjsonFlow::PathGraph

from fastjsonFlow::PathNode source, fastjsonFlow::PathNode sink
where fastjsonFlow::flowPath(source, sink)
select source.getNode(), source, sink, "source"

Rce

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
/**
* @id java/examples/rce
* @name Rce
* @description rce
* @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

module RceVulconfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node src) {
src instanceof RemoteFlowSource
}

predicate isSink(DataFlow::Node sink) {
sink.asExpr() instanceof ArgumentToExec //ArgumentToExec表示传递给命令执行的参数
}
}

module rceFlow = TaintTracking::Global<RceVulconfig>;

import rceFlow::PathGraph

from rceFlow::PathNode source, rceFlow::PathNode sink
where rceFlow::flowPath(source, sink)
select source.getNode(), source, sink, "source"

XXE

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
/**
* @id java/examples/vuldemo/xxe
* @name xxe
* @description xxe-vul
* @kind path-problem
* @problem.severity warning
*/

import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.ExternalFlow
import DataFlow::PathGraph

class XXEVulConfig extends TaintTracking::Configuration {
XXEVulConfig(){
this = "XXEVulConfig"
}

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

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

from XXEVulConfig xxeVulConfig, DataFlow::PathNode source, DataFlow::PathNode sink
where xxeVulConfig.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"

下面是其他文章中个人决定有用的点

自写规则模板

写脚本熟练之后,就经常用这个来写规则了,开箱即用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

/**
* @kind path-problem
*/
import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.FlowSources
module MyConfiguration implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
predicate isSink(DataFlow::Node sink) {
exists(MethodCall methodCall |
methodCall.getMethod().getName() = "methodName" and
methodCall.getQualifier().getType().getName() = "ClassName" and
sink.asExpr() = methodCall.getAnArgument()
)
}
}
module Flow = DataFlow::Global<MyConfiguration>;
import Flow::PathGraph
from Flow::PathNode source, Flow::PathNode sink
where Flow::flowPath(source, sink)
select sink.getNode(), source, sink,"xxxInfo RCE"

Shiro 秘钥硬编码查询(支持新版语法)

语法已废弃,新版中默认已支持该规则。

shiro在早期版本中的用来序列化反序列化的key可以被定义,查询系统中是否存在默认密钥定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.FlowSources
import DataFlow::PathGraph
//查询是否存在 Shiro 默认秘钥
class ShiroDefaultKey extends TaintTracking::Configuration {
ShiroDefaultKey() { this = "ShiroDefaultKey" }
override predicate isSource(DataFlow::Node src) { src.asExpr() instanceof StringLiteral }
override predicate isSink(DataFlow::Node src) {
exists(MethodCall methodCall |
methodCall.getMethod().getName() = "setCipherKey" and
methodCall.getQualifier().getType().getName() = "CookieRememberMeManager" and
src.asExpr() = methodCall.getAnArgument()
)
}
}
from DataFlow::PathNode source, DataFlow::PathNode sink, ShiroDefaultKey conf, MethodCall methodCall
where conf.hasFlowPath(source, sink) and sink.getNode().asExpr() = methodCall.getAnArgument()
// select source, sink, methodCall, methodCall.getEnclosingCallable()
select source.toString(),source.getNode().getEnclosingCallable(),source.getNode().getEnclosingCallable().getFile().getAbsolutePath(),
sink.toString(),sink.getNode().getEnclosingCallable(), sink.getNode().getEnclosingCallable().getFile().getAbsolutePath(), "Shiro Default Key"

脚本更新为新版支持语法,减少waring报错。

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

/**
* @name Apache Shiro Default Key
* @description Apache Shiro Default Key
* @kind path-problem
* @problem.severity error
* @security-severity 8.8
* @precision high
* @id java/rce-shiro-default-key
* @tags devrules
*/
import java
import Flow::PathGraph
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
module RCEConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { source = source }
predicate isSink(DataFlow::Node sink) {
exists(MethodCall methodCall, Class c |
methodCall.getMethod().getName() = "setCipherKey" and
methodCall.getQualifier().getType() = c and
c.hasQualifiedName("org.apache.shiro.web.mgt", "CookieRememberMeManager") and
sink.asExpr() = methodCall.getAnArgument()
)
}
}
module Flow = DataFlow::Global<RCEConfig>;
from Flow::PathNode source, Flow::PathNode sink
where Flow::flowPath(source, sink)
select sink.getNode(), source, sink, "Shiro Default Key"

ScriptEngine RCE规则查询(支持新版语法)

Java默认的JavaScript解析器,可以解析Java代码造成任意代码执行。

漏洞代码:

1
2
3
4
ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine scriptEngine = scriptEngineManager.getEngineByExtension("js");
String command = "new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc']).start()";
scriptEngine.eval(command);

使用新版规则进行查询,减少控制台Warning警告。

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
/**
* @name Script Engine RCE
* @description Script Engine RCE
* @kind path-problem
* @problem.severity error
* @security-severity 8.8
* @precision high
* @id java/rce-script-engine
* @tags devrules
*/
import java
import ScriptEngineFlow::PathGraph
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.FlowSources
module ScriptEngineConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
predicate isSink(DataFlow::Node sink) {
exists(MethodCall methodCall, Class c |
methodCall.getMethod().getName() = "eval" and
methodCall.getQualifier().getType().getName() = "ScriptEngine" and
sink.asExpr() = methodCall.getAnArgument()
)
}
}
module ScriptEngineFlow = DataFlow::Global<ScriptEngineConfig>;
from ScriptEngineFlow::PathNode source, ScriptEngineFlow::PathNode sink
where ScriptEngineFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "Script Engine RCE"

Apache Commons Text 远程代码执行漏洞(支持新版语法)

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
/**
* @name Apache Commons Text Rce
* @description Apache Commons Text Rce
* @kind path-problem
* @problem.severity error
* @security-severity 8.8
* @precision high
* @id java/rce-apache-commons-text
* @tags devrules
*/
import java
import CommonsTextRCEFlow::PathGraph
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
module CommonsTextRCEConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
predicate isSink(DataFlow::Node sink) {
exists(MethodCall methodCall , Class c|
methodCall.getMethod().getName().substring(0, 7) = "replace" and
methodCall.getQualifier().getType() = c and
c.hasQualifiedName("org.apache.commons.text", "StringSubstitutor") and
sink.asExpr() = methodCall.getAnArgument()
)
}
}
module CommonsTextRCEFlow = DataFlow::Global<CommonsTextRCEConfig>;
from CommonsTextRCEFlow::PathNode source, CommonsTextRCEFlow::PathNode sink
where CommonsTextRCEFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "Apache Commons Text Rce"

SpEL表达式注入漏洞(支持新版语法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.FlowSources
module SpELConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
predicate isSink(DataFlow::Node sink) {
exists(MethodCall methodCall |
methodCall.getMethod().getName() = "parseExpression" and
methodCall.getQualifier().getType().getName() = "SpelExpressionParser" and
sink.asExpr() = methodCall.getAnArgument()
)
}
}
module SpELFlow = DataFlow::Global<SpELConfig>;
from DataFlow::Node source, DataFlow::Node sink
where SpELFlow::flow(source, sink)
select source,sink

小技巧

有时候获取到的可能并非源码,而是jar包,但是jar包反编译后的源码再次重编译会因为种种原因编译错误,在一次和一位高手沟通的过程中我了解到也可以不通过编译来构建codeql数据库

模拟大部分情况下获取jar包的情况,先打包后反编译

1
java -jar java-decompiler.jar D:\Downloads\micro_service_seclab\target\micro-service-seclab-0.0.1-SNAPSHOT.jar D:\Downloads\micro_service_seclab\decompile

反编译后得到一个jar包,不过解压后会发现里面的文件不再是class而是java

img

创建数据库

1
codeql database create D:\Downloads\micro_service_seclab\database --language java -s D:\Downloads\micro_service_seclab\decompile\micro-service-seclab-0.0.1-SNAPSHOT --overwrite --build-mode none

先记一下,还没试过

像你说的 Hessian 反序列化漏洞 这类场景,漏洞点往往存在于 业务代码调用外部依赖库(Hessian 框架) 的过程中,所以数据库必须包含:

  1. 项目自身的源码
  2. 项目依赖的三方库(jar 包)

否则 CodeQL 的数据流图不完整,就没法追踪到 HessianInput.readObject() 之类的危险调用。


正确的数据库创建流程(Java 项目)

1. 用构建工具生成数据库

  • Maven 项目

    1
    2
    3
    codeql database create mydb \
    --language=java \
    --command="mvn clean compile -DskipTests"
  • Gradle 项目

    1
    2
    3
    codeql database create mydb \
    --language=java \
    --command="./gradlew build -x test"

原理:
CodeQL 的 Java extractor 会拦截 javac,在编译过程中把源码和依赖关系收集到数据库里。
这样不仅有项目源码,还会自动把 依赖 jar 包里的类签名 收录进来(用于调用关系)。


2. 如果项目没有标准构建命令

有些项目是手工编译或者没用 Maven/Gradle。
这时你需要显式告诉 CodeQL 源码 + classpath

1
2
3
4
codeql database create mydb \
--language=java \
--source-root . \
--command="javac -cp 'lib/*' -d build/classes $(find src -name '*.java')"
  • lib/*:依赖 jar 包目录(例如包含 hessian.jar)
  • src:源码目录

这样 extractor 在分析时就能看见 Hessian 的 API 并构建调用图。


3. 确认数据库里包含依赖

创建完成后,你可以检查数据库里是否包含依赖:

1
codeql database interpret-results mydb

或者在 .db 目录下找到 dbscheme,确认是否解析了 Hessian 的类。