codeql学习指南
如何安装
基本查询
查询结构
查询基本包含3部分,作用与SQL查询的FROM、WHERE、SELECT部分类似。
查询部分 | 目的 | 细节 |
---|---|---|
import java |
为 Java 和 Kotlin 导入标准 CodeQL 库。 | 每个查询都以一个或多个语句开始import 。 |
from MethodAccess ma |
定义查询的变量。声明的形式为: <type> <variable name> |
我们用:MethodAccess 调用表达式的变量 |
where ma.getMethod().hasName("equals") and ma.getArgument(0).(StringLiteral).getValue() = "" |
定义变量的条件。 | ma.getMethod().hasName("equals") 仅限ma 于调用方法 call equals 。ma.getArgument(0).(StringLiteral).getValue() = "" 说参数必须是文字的"" 。 |
select ma, "This comparison to empty string is inefficient, use isEmpty() instead." |
定义每次匹配报告的内容。select 用于查找不良编码实践实例的查询语句始终采用以下形式: select <program element>, "<alert message>" |
.equals 使用解释问题的字符串报告生成的表达式。 |
查询空字符时,效率较低,替换s.equals("")
为s.isEmpty()
都会更有效率。
//java |
Java相关语法
库类总结
标准 Java 库中最重要的类可以分为五个主要类别:
- 表示程序元素的类(例如类和方法)
- 表示 AST 节点的类(例如语句和表达式)
- 表示元数据的类(例如注释和评论)
- 用于计算指标的类(例如圈复杂度和耦合)
- 用于导航程序调用图的类
代码关键元素
这些类表示命名的程序元素:包 ( Package
)、编译单元 ( CompilationUnit
)、类型 ( Type
)、方法 ( Method
)、构造函数 ( Constructor
) 和变量 ( Variable
)。
它们的共同超类是Element
,它提供通用成员谓词来确定程序元素的名称并检查两个元素是否相互嵌套。
引用可能是方法或构造函数的元素通常很方便;类Callable
是Method
和的公共超类Constructor
,可用于此目的。
类型
类Type
有许多子类来表示不同种类的类型:
PrimitiveType
表示原始类型,即boolean
,byte
,char
,double
,float
,int
,long
,之一short
;QL 也将void
and<nulltype>
(文字的类型null
)归类为基本类型。- RefType表示引用(即非原始)类型;它又有几个子类:
Class
代表一个 Java 类。Interface
表示一个 Java 接口。EnumType
表示 Javaenum
类型。Array
表示 Java 数组类型。
查找程序中的所有变量
import java |
例如,以下查询查找int
程序中类型的所有变量:
import java |
运行之后我们发现存在大量的结果,因为大多项目都包含许多类型的变量int
import java |
运行后发现确实找到了String类型的变量
codeql也提供了非原始类型,RefType
import java |
引用类型也根据它们的声明范围进行分类:
TopLevelType
表示在编译单元的顶层声明的引用类型。NestedType
是在另一个类型中声明的类型。
String不是基本类型,而是一个class对象,我们可以用这样的方法来查找类型为string的变量
例如,此查询查找名称与其编译单元名称不同的所有顶级类型:
import java |
您通常会在存储库的源代码中看到这种模式,在源代码引用的文件中有更多实例。
同时在Java中还有在类中声明类的,可以用以下语查询
import java |
java中还有范型类,我们用以下语法来查询参数实例化了java.util.Map,ParameterizedType是所有参数的类型
import java |
在程序中找到了实例化了map的参数可以用下面语法来查询
import java |
getSourceDeclaration 可以获取相应泛型类型
变量
在上面,我们用Variable 来查询程序中所有的类,有时需要更具体的,所以codeql提供了3个子类
Field
代表一个 Java 字段。LocalVariableDecl
表示局部变量。Parameter
表示方法或构造函数的参数。
试一下Field
LocalVariableDecl
Parameter
抽象语法树
抽象语法树AST是一个程序的抽象表示模式,各种程序的语句都可以被抽象成节点,比如跳转节点,return节点,switch节点等等,通过节点还可以生成程序流程图等等,在数据流分析中十分重要。
比如跳转节点
import java |
元数据
这里关注下注解,我们在测试springboot项目比较注意这些
比如找到方法上的所有注解
import java |
函数调用
我们可以用Call来找到所有表达式中的调用部分
import java |
找到所有调用了println方法的Call
import java |
数据流
什么是数据流呢,我们在代码审计中要先找到危险函数,如readfile等。readfile入参为param,如果一个外部输入参数如:$GET[‘file’],通过一些列赋值传递给了param,这之间的通道便被称为数据流。$GET[‘file’]此类称为source,readfile此类危险函数称为sink。中间的通道被称为数据流flow,这之间还有净化函数Sanitizer,就是安全函数,在审计中比如黑白名单、类型强转等等。
codeQL中的数据流
本地数据流
本地数据流很好理解,就是在单个方法中的数据流。优点是快速准确。
在codeQl中为:
DataFlow::localFlow(DataFlow::parameterNode(source), DataFlow::exprNode(sink)) |
在我们用到的靶场中
package org.joychou.controller; |
很明显,危险函数是new ProcessBuilder(arrCmd)
我们使用下面语法来找到这个调用
import java |
找到了危险函数的第一个危险参数commands
我们很明显看出,危险参数由command传入,那么codeQL怎么识别呢?
这就要用到DataFlow库了。
引用方式为
import semmle.code.java.dataflow.DataFlow |
使用方法为:
DataFlow::localFlow(DataFlow::parameterNode(source), DataFlow::exprNode(sink) |
我们编写codeql代码如下:
import java |
我们可以找到所有流入危险函数的值
名称 | 解释 |
---|---|
Method | 方法类,Method method表示获取当前项目中所有的方法 |
MethodAccess | 方法调用类,MethodAccess call表示获取当前项目当中的所有方法调用 |
Parameter | 参数类,Parameter表示获取当前项目当中所有的参数 |
结合ql的语法,我们尝试获取项目中定义的所有方法
import java |
我们在通过Method内置的一些方法,把结果过滤一下。比如获取名字为sayHello的方法名称
import java |
谓词
和SQL一样,where部分的查询条件如果过长,会显得很乱。CodeQL提供一种机制可以让你把很长的查询语句封装成函数。
这个函数,就叫谓词。
比如上面的案例,我们可以写成如下,获得的结果跟上面是一样的:
import java |
语法解释
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
在CodeQL中我们通过
override predicate isSource(DataFlow::Node src) {} |
方法来设置source
。
思考一下,在我们的靶场系统中,source是什么?
我们使用的是Spring Boot
框架,那么source就是http参数入口的代码参数,在下面的代码中,source就是username:
@RequestMapping("/jdbc/vuln") |
在下面的代码中,source就是cmd
@GetMapping("/ProcessBuilder") |
我们设置Source的代码为:
override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } |
这是SDK
自带的规则,里面包含了大多常用的Source入口。我们使用的SpringBoot也包含在其中, 我们可以直接使用。
设置Sink
在CodeQL中我们通过
override predicate isSink(DataFlow::Node sink) { |
方法设置Sink。
在本案例中,我们的sink应该为executeQuery
方法(Method)的调用(MethodAccess),所以我们设置Sink为:
override predicate isSink(DataFlow::Node sink) { |
注:以上代码使用了exists子查询语法,格式为exists(Obj obj| somthing), 上面查询的意思为:查找一个query()方法的调用点,并把它的第一个参数设置为sink。
在靶场系统中,sink就是:
statement.executeQuery(sql); |
因为我们测试的注入漏洞,当source变量流入这个方法的时候,才会发生注入漏洞!
Flow数据流
设置好Source和Sink,就相当于搞定了首尾,但是首尾是否能够连通才能决定是否存在漏洞!
一个受污染的变量,能够毫无阻拦的流转到危险函数,就表示存在漏洞!
这个连通工作就是CodeQL引擎本身来完成的。我们通过使用config.hasFlowPath(source, sink)
方法来判断是否连通。
比如如下代码:
from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink |
我们传递给config.hasFlowPath(source, sink)
我们定义好的source和sink,系统就会自动帮我们判断是否存在漏洞了。
在CodeQL中,我们使用官方提供的TaintTracking::Configuration方法定义source和sink,至于中间是否是通的,这个后面使用CodeQL提供的config.hasFlowPath(source, sink)
来帮我们处理。
class VulConfig extends TaintTracking::Configuration { |
CodeQL语法和Java类似,extends代表集成父类TaintTracking::Configuration。
这个类是官方提供用来做数据流分析的通用类,提供很多数据流分析相关的方法,比如isSource(定义source),isSink(定义sink)
src instanceof RemoteFlowSource 表示src 必须是 RemoteFlowSource类型。在RemoteFlowSource里,官方提供很非常全的source定义,我们本次用到的Springboot的Source就已经涵盖了。
最终代码
/** |
使用靶场系统(micro-service-seclab)进行测试
误报处理
我们发现存在一处误报的现象
这个方法的参数类型是List
这说明我们的规则里,对于List
我们需要采取手段消除这种误报。
这个手段就是isSanitizer
。
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方法,即可消除这种误报。
override predicate isSanitizer(DataFlow::Node node) { |
以上代码的意思为:如果当前node节点的类型为基础类型,数字类型和泛型数字类型(比如List)时,就切断数据流,认为数据流断掉了,不会继续往下检测。
重新执行query,我们发现,刚才那条误报已经被成功消除啦。
漏报解决
我们发现,如下的SQL注入并没有被CodeQL捕捉到。
public List<Student> getStudentWithOptional(Optional<String> username) { |
漏报理论上讲是不能接受的。如果出现误报我们还可以通过人工筛选来解决,但是漏报会导致很多漏洞流经下一个环节到线上,从而产生损失。
那我们如果通过CodeQL来解决漏报问题呢?答案就是通过isAdditionalTaintStep
方法。
实现原理就一句话:
断了就强制给它接上。
isAdditionalTaintStep方法是CodeQL的类
TaintTracking::Configuration
提供的的方法,它的原型是:override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {}
它的作用是将一个可控节点
A强制传递给另外一个节点B,那么节点B也就成了可控节点。
多次测试之后,我认定是因为username.get()这一步断掉了。大概是因为Optional这种类型的使用没有在CodeQL的语法库里。
那么这里我们强制让username流转到username.get(),这样username.get()就变得可控了。这样应该就能识别出这个注入漏洞了。
我们试一下。
发现成功捕捉到了相关漏洞
利用codeql规则集进行扫描
codeql database analyze --threads 16 java-sec-code-db codeql-main/java/ql/src/codeql-suites/java-security-and-quality.qls --format=sarifv2.1.0 --output=result.sarifv |
保存成sarifv格式,同时也可以保存csv格式
Shutting down query evaluator. |

如何编写codeql规则集
https://tonghuaroot.com/2021/09/18/Write-test-files-for-CodeQL-custom-rules/
自动化codeql探索
https://github.com/ZhuriLab/Yi