CodeQL 入门
Drunkbaby Lv6

CodeQL · 真入门

0x01 前言

在自己第一遍学完 CodeQL 之后还是感觉比较生疏,于是想找点靶场练手,于是就有了这篇文章。

0x02 CodeQL 基本语法

QL 语法

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

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

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

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

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

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

基本语法包含3个部分。

名称解释
Method方法类,Method method 表示获取当前项目中所有的方法
MethodAccess方法调用类,MethodAccess 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()
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,来决定筛选出哪些数据。

设置 Source 和 Sink

什么是source和sink

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

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

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

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

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

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

0x03 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.FlowSources
import semmle.code.java.security.QueryInjection
import DataFlow::PathGraph

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

override predicate isSource(DataFlow::Node src) {
src 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 config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"

CodeQL 在定义类上的语法和 Java 类似,其中 extends 的父类 TaintTracking::Configuration 是官方提供用来做数据流分析的通用类,提供很多数据流分析相关的方法,比如isSource(定义source),isSink(定义sink)

src instanceof RemoteFlowSource 表示src 必须是 RemoteFlowSource 类型。在RemoteFlowSource里,官方提供很非常全的source定义,我们本次用到的Springboot的Source就已经涵盖了。

  • 注:上面的注释和其它语言是不一样的,不能够删除,它是程序的一部分,因为在我们生成测试报告的时候,上面注释当中的name,description等信息会写入到审计报告中。

误报解决

扫描结果当中存在一项误报

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

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

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

这个手段就是 isSanitizer

1
2
3
4
5
6
7
8
9
10
11
12
13
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 方法,即可消除这种误报。

1
2
3
4
5
6
override predicate isSanitizer(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 型
}

以上代码的意思为:如果当前node节点的类型为基础类型,数字类型和泛型数字类型(比如List)时,就切断数据流,认为数据流断掉了,不会继续往下检测。

重新执行query,我们发现,刚才那条误报已经被成功消除啦。

漏报解决

我们发现,如下的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 方法。

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

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() 就变得可控了。这样应该就能识别出这个注入漏洞了。

完整代码

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

// 解决 SQL 注入 QL 语句的漏保

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

predicate isTaintedString(Expr expSrc, Expr expDest) {
exists(Method method, MethodAccess call, MethodAccess 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>"
)
}

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

override predicate isSource(DataFlow::Node src) {
src 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) // sink.asExpr() 是一个方法,用于将一个 sink 转换成一个表达式。这个方法通常用于在查询中使用 sink,因为查询需要将 sink 转换成表达式才能进行分析。
)
}

override predicate isSanitizer(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
)
}

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

}

from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(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 E:\Coding\CodeQL\CodeQL-Practice\database --language="java" --source-root=E:\Coding\CodeQL\micro_service_seclab --command="mvn clean package -Dmaven.test.skip=true"

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

这里是很容易踩坑的,因为前面的语句有个问题就是 select 的返回值并非是 string,所以就会报错,报错信息如下

1
Error was: Expected result pattern(s) are not present for problem query: Expected exactly one pattern. 

参考 https://xz.aliyun.com/t/10852#toc-7

0x04 CodeQL 进阶

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

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

instanceof 这个关键字是用来判断当前的对象,和后面的是否为同一类型。

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

这种方式的提出其实是用来优化之前使用 exists(|) 匹配对应 Source/Sink,如果我们需要写非常多的 exists(|),这会使得整个项目维护起来非常困难,于是就出现了这一种语法糖 instanceof

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

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 也会被加载(让我想起了超级版 Fastjson,这有没有存在 CodeQL 反制一说

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

递归问题

简单来说,有如此一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.l4yn3.microserviceseclab.service;

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
4
5
import java

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

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

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)

强制类型转换问题(过滤需要类型)

强制类型转换这个名字有点拗口,且不太好理解,这里我更愿意把它理解成一种 filter

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

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

1
2
3
4
import java

from Parameter param
select param, param.getType()

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

如果其中我们想要单独某个类型的方法参数,这里就需要用到强制类型转换,或者说用到 filter

使用 RefType() 来测试

1
2
3
4
import java

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

强制转换成 RefType,意思就是从前面的结果当中过滤出 RefType 类型的参数。

RefType 是什么?引用类型,说白了就是去掉int等基础类型之后的数据。

同理这里肯定并不限于 RefType,也可以是其他的。比如这里保留所有的数值类型参数

1
2
3
4
import java

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

0x05 关于其他漏洞点的 CodeQL 语句尝试

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

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

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

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

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

from FastjsonVulConfig fastjsonVul, DataFlow::PathNode source, DataFlow::PathNode sink
where fastjsonVul.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
23
24
25
26
/**
* @id java/examples/vuldemo
* @name processBuilder-vul
* @description processBuilder-vul
* @kind path-problem
* @problem.severity warning
*/

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

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

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

override predicate isSink(DataFlow::Node sink) {
sink.asExpr() instanceof ArgumentToExec
}
}
from RceVulConfig rceVulConfig, DataFlow::PathNode source, DataFlow::PathNode sink
where rceVulConfig.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"

SSRF(重点关注)

这里的内容主要是参考于这篇文章 https://forum.butian.net/share/2117

还是觉得关于 SSRF 的 ql 规则这块儿,应该再记录一下,其实在之前看命令执行的 sink 的时候就没追踪到,但是那个时候并没有深入去看。

最开始我的 ql 语句是这样的(很嫩的 ql 语句)

1
2
3
4
5
6
7
8
9
10
11
12
override predicate isSource(DataFlow::Node src) {
src instanceof RemoteFlowSource
}

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

追踪 url.openConnection(),但这很明显是追踪不到的,因为 url.openConnection() 是不传参的。那么这一条链路用图来表示的话,断在了这里

那么中间断的地方我们要想办法接上,这就回到了前文提到过的 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
/**
* @id java/examples/vuldemo
* @name processBuilder-vul
* @description processBuilder-vul
* @kind path-problem
* @problem.severity warning
*/

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

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

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

override predicate isSink(DataFlow::Node sink) {
sink instanceof RequestForgerySink
}
}
from SSRFVulConfig ssrfVulConfig, DataFlow::PathNode source, DataFlow::PathNode sink
where ssrfVulConfig.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"

运行结果

此处 import 了一个新的文件 semmle.code.java.security.RequestForgeryConfig,这里匹配了对应的规则,和之前的命令执行接口是一样的。可以深入看一下对应的实现。

isSource

1
2
3
4
5
6
7
override predicate isSource(DataFlow::Node source) {
source instanceof RemoteFlowSource and
// Exclude results of remote HTTP requests: fetching something else based on that result
// is no worse than following a redirect returned by the remote server, and typically
// we're requesting a resource via https which we trust to only send us to safe URLs.
not source.asExpr().(MethodAccess).getCallee() instanceof UrlConnectionGetInputStreamMethod
}

RequestForgeryConfig.qll 规则中的 Source 匹配 RemoteFlowSource,且限定了 java.net.URLConnection.getInputStream() 的输入不为漏洞。

这里的原因是此处的 getInputStream() 的输入不一定是可控的。

isSink

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

跟进 RequestForgerySink

1
2
3
4
5
6
7
8
9
10
/** A data flow sink for server-side request forgery (SSRF) vulnerabilities. */
abstract class RequestForgerySink extends DataFlow::Node { }

private class UrlOpenSinkAsRequestForgerySink extends RequestForgerySink {
UrlOpenSinkAsRequestForgerySink() { sinkNode(this, "open-url") }
}

private class JdbcUrlSinkAsRequestForgerySink extends RequestForgerySink {
JdbcUrlSinkAsRequestForgerySink() { sinkNode(this, "jdbc-url") }
}

两个类都在构造函数里面调用了 sinkNode(),跟进去,对应的文件是 java.dataflow.ExternalFlow.qll
关注到它的注释内容大致意思是在说

仅供内部使用。这是一个实验API
提供用于处理指定的流模型的类和谓词
数据扩展和CSV格式

那么这里的内容,一定是从某个 CSV 文件里面去读取的,这就被定义为 ModelCsv 我们可以简化代码定义 sink、source、flow step,并通过kind来使用它。

  • 它的格式有这么几种
1
2
3
4
5
6
7
8
9
10
11
Source: (SourceModelCsv)
package; type; subtypes; name; signature; ext; output; kind; provenance

Sink: (SinkModelCsv)
package; type; subtypes; name; signature; ext; input; kind; provenance

Summaries: (SummaryModelCsv)
package; type; subtypes; name; signature; ext; input; output; kind; provenance

Neutrals:
package; type; name; signature; provenan

每一个参数都代表一个含义,整体来说如下

package:包名
type:选择包中的某个类型
subtypes:布尔类型,指示是否跳转到子类
name:方法名
signature:签名
ext:类似于附加类
input:输入的位置
kind:当前 sink 的类型
provenance:来源验证

这么看的话比较抽象,下文会详细讲解 SSRF 漏洞中所对应的规则。

目前 CodeQL 官方还并未发布 SinkModelCsv 的一些官方规则,原因是此功能尚不稳定。使用需要开发者/安全人员自己定义类,此类需继承 xxxModelCsv 即可应用。

sinkModelCsv 谓词数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private predicate sinkModelCsv(string row) {
row =
[
// Open URL
"java.net;URL;false;openConnection;;;Argument[-1];open-url",
"java.net;URL;false;openStream;;;Argument[-1];open-url",
"java.net.http;HttpRequest;false;newBuilder;;;Argument[0];open-url",
"java.net.http;HttpRequest$Builder;false;uri;;;Argument[0];open-url",
"java.net;URLClassLoader;false;URLClassLoader;(URL[]);;Argument[0];open-url",
"java.net;URLClassLoader;false;URLClassLoader;(URL[],ClassLoader);;Argument[0];open-url",
"java.net;URLClassLoader;false;URLClassLoader;(URL[],ClassLoader,URLStreamHandlerFactory);;Argument[0];open-url",
"java.net;URLClassLoader;false;URLClassLoader;(String,URL[],ClassLoader);;Argument[1];open-url",
"java.net;URLClassLoader;false;URLClassLoader;(String,URL[],ClassLoader,URLStreamHandlerFactory);;Argument[1];open-url",
"java.net;URLClassLoader;false;newInstance;;;Argument[0];open-url",
// Bean validation
"javax.validation;ConstraintValidatorContext;true;buildConstraintViolationWithTemplate;;;Argument[0];bean-validation",
// Set hostname
"javax.net.ssl;HttpsURLConnection;true;setDefaultHostnameVerifier;;;Argument[0];set-hostname-verifier",
"javax.net.ssl;HttpsURLConnection;true;setHostnameVerifier;;;Argument[0];set-hostname-verifier"
]
}

如果直接跑 sinkNode() 代码,结果如下

也就是通过上述的 sinkModuleCsv,其匹配所有 open-url 类型的数据类型。

  • 但是在新版本的 CodeQL 当中是找不到这一个规则的,在搜索了一堆资料后发现在新版本中是这样的。

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
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
any(RequestForgeryAdditionalTaintStep r).propagatesTaint(pred, succ)
}

// 跟进 RequestForgeryAdditionalTaintStep

class RequestForgeryAdditionalTaintStep extends Unit {
/**
* Holds if the step from `pred` to `succ` should be considered a taint
* step for server-side request forgery.
*/
abstract predicate propagatesTaint(DataFlow::Node pred, DataFlow::Node succ);
}

private class DefaultRequestForgeryAdditionalTaintStep extends RequestForgeryAdditionalTaintStep {
override predicate propagatesTaint(DataFlow::Node pred, DataFlow::Node succ) {
// propagate to a URI when its host is assigned to
exists(UriCreation c | c.getHostArg() = pred.asExpr() | succ.asExpr() = c)
or
// propagate to a URL when its host is assigned to
exists(UrlConstructorCall c | c.getHostArg() = pred.asExpr() | succ.asExpr() = c)
}
}

private class TypePropertiesRequestForgeryAdditionalTaintStep extends RequestForgeryAdditionalTaintStep
{
override predicate propagatesTaint(DataFlow::Node pred, DataFlow::Node succ) {
exists(MethodAccess ma |
// Properties props = new Properties();
// props.setProperty("jdbcUrl", tainted);
// Propagate tainted value to the qualifier `props`
ma.getMethod() instanceof PropertiesSetPropertyMethod and
ma.getArgument(0).(CompileTimeConstantExpr).getStringValue() = "jdbcUrl" and
pred.asExpr() = ma.getArgument(1) and
succ.asExpr() = ma.getQualifier()
)
}
}

看注释是比较明确的,也就是通过 isAdditionalFlowStep(),将 predsucc 两个点连起来,这里的连接方式是通过污点传递来实现的。具体的匹配方式很容易理解,跟进一下 UriCreationUrlConstructorCall 即可

 评论