为什么要写这篇文章?
自从Github宣布推出CodeQL,国外越来越多安全人员使用这个项目做代码安全评估工作,截止到此刻,CodeQL在Github上已经有超过3100个Star。
但是国内了解CodeQL的安全人员并不多,能google到的关于codeql的中文文章比较少。大部分中文文章,都是介绍CodeQL是什么之后,用简单的代码片段说明CodeQL的某个功能,很少有非常全面的介绍使用CodeQL对一个项目做漏洞分析的文章。这让想学习的读者一头雾水,还是不知道该如何在自己的项目上使用CodeQL。
所以我想写一篇文章,从安装开始,到编写QL规则实现漏洞的自动化扫描,再到解决误报,漏报问题,让读者能够真正的了解该如何使用CodeQL自动化审计自己项目的安全性。
CodeQL是什么?
如果您已经了解CodeQL是什么,可以直接跳过这个章节。
在回答这个问题之前,我们来看一下安全人员做代码审计的进化史。
最早期,安全人员会通过人工审计的方式来审计项目代码,查找危险函数,并跟进危险函数的参数是否可控,如果可控,说明存在安全漏洞。
但是随着项目数量的增加,以上的纯靠人工的方式很难实现所有项目漏洞的覆盖测试。所以出现了一些辅助人工审计的工具,比如前几年比较火的rips,cobra,通过这些工具,可以把危险函数代码代码检索出来,再通过人工审计来判断是否存在安全漏洞。
上面的方式主要还是需要人来判断,工作量还是很大,并且非常依赖安全工程师的个人能力。但是近些年出现了不少优秀的自动化代码安全审计产品,比如非常有名的Checkmarx,Fortify SCA。这些软件可以自动化的帮我们审计出安全漏洞,大大减少了人工工作量,并加快了安全审计速度。但是这些软件都是商业的,价格比较贵,一般企业可能没有这么多预算购买。
与此同时,Github为了解决其托管的海量项目的安全性问题,收购了CodeQL的创业公司,并宣布开源CodeQL的规则部分,这样全世界的安全工程师就可以贡献高效的QL审计规则给Github,帮助它解决托管项目的安全问题。
对于安全工程师,也就多了一个非商业的开源代码自动化审计工具。
CodeQL支持非常多的语言,在官网有如下支持的语言和框架列表
注:CodeQL被禁止用于企业内部的CI/CD流程,我们可以用来做安全研究。同时我还是建议企业购买一款商业的SAST代码审计工具,原因我们最后说。
靶场介绍
我使用SpringBoot
简单的实现了一个靶场。这个靶场里面包含注入漏洞和一些其它的漏洞。我们本篇文章的目的,就是要使用CodeQL
来自动检索出里面的注入漏洞,并且排除误报,解决漏报,还有解决一些其它问题。
OS: Mac
Java JDK: 1.8
Maven: Apache Maven 3.6.3
您可以点击此处下载到这个简单的测试靶场系统(micro-service-lab)。
CodeQL安装
CodeQL本身包含两部分解析引擎+SDK
。
解析引擎用来解析我们编写的规则,虽然不开源,但是我们可以直接在官网下载二进制文件直接使用。
SDK
完全开源,里面包含大部分现成的漏洞规则,我们也可以利用其编写自定义规则。
引擎安装
首先在系统上选定CodeQL的安装位置,我的位置为:Home/CodeQL。
然后我们去地址:https://github.com/github/codeql-cli-binaries/releases 下载已经编译好的codeql执行程序,解压之后把codeql文件夹放入~/CodeQL。
为了方便测试我们需要把ql可执行程序加入到环境变量当中:
export PATH=/Home/CodeQL/codeql:$PATH
然后source一下/etc/profile之后,我们在命令行输入codeql,出现如下内容就表示引擎设置完成。
SDK安装
我们使用Git下载QL语言工具包,也放入~/CodeQL文件夹。
cd ~/CodeQL&git clone https://github.com/Semmle/ql
这样在~/CodeQL目录下就包含了2个文件夹,引擎文件夹(codeql)和SDK文件夹(ql)。
➜ CodeQL ls
codeql ql
VSCode开发插件安装
CodeQL需要使用Visual Studio Code
来开发和调试规则,所以我们需要在VSCode上面安装CodeQL的插件。
我们安装好Visual Studio Code
后,在它的扩展里面搜索codeql
, 点击安装。
然后我们配置一下上面我们安装的codeql引擎
路径。
到此,我们就设置好了CodeQL
的开发环境,是不是很简单?
后面我们将进入CodeQL规则实质性的东西,我们会随着项目进展一起,说明使用Visual Studio Code
的方方面面。
测试"Hello World"
生成Database
为了测试我们刚才的开发环境是否可以正常调试,我们实现一个简单的"Hello World"。
由于CodeQL
的处理对象并不是源码本身,而是中间生成的AST结构数据库,所以我们先需要把我们的项目源码转换成CodeQL
能够识别的CodeDatabase
。
我们使用如下命令进行CodeDatabase
的生成工作。
database create ~/CodeQL/databases/micro-service-seclab-database --language="java" --command="mvn clean install --file pom.xml" --source-root=~/CodeQL/micro-service-seclab/
我们来解释一下上面生成database命令的语句:
codeql database create ~/CodeQL/databases/codeql_demo 当然是指我们要创建的database为~/CodeQL/databases/codeql_demo(注意:要先创建~/CodeQL/databases/目录)。
--language="java" 表示当前程序语言为Java。
--command="mvn clean install --file pom.xml" 编译命令(因为Java是编译语言,所以需要使用–command命令先对项目进行编译,再进行转换,python和php这样的脚本语言不需要此命令)
--source-root=~/CodeQL/micro-service-seclab/ 这个当然指的是项目路径
我们执行以上命令后,首先会对项目进行编译,然后就会提示创建数据库成功,访问~/CodeQL/databases/codeql_demo
存在即可。
导入Database
和SQL语言一样,我们执行QL查询,肯定是要先指定一个数据库才可以。
我们通过如下方式来导入刚刚生成的数据库,选择目录~/CodeQL/micro-service-seclab/
。
编写"Hello World"查询
编写QL查询,我们需要使用Visual Studio Code
打开我们开始下载的SDK
,也就是~/CodeQL/ql
文件夹, 然后在图示的目录里新建demo.ql
文件,然后写入select "Hello World"
, 然后在当前文件上点击鼠标邮件,选择CodeQL: Run Query
即可执行。
如果如上图所示,执行成功之后,看到了右侧的"Hello World",恭喜你,你已经开始了CodeQL
之旅!
CodeQL基本语法
我们上面提到过,CodeQL的核心引擎是不开源的,这个核心引擎的作用之一是帮助我们把micro-service-seclab转换成CodeQL能识别的中间层数据库。
然后我们需要编写QL查询语句来获取我们想要的数据。
正如这张图描述的,由于CodeQL开源了所有的规则和规则库部分,所以我们能够做的就是编写符合我们业务逻辑的QL规则,然后使用CodeQL引擎去跑我们的规则,发现靶场的安全漏洞。
我们来简单地介绍一下本案例涉及到的CodeQL的基本语法。
基本语法包含3个部分。
QL语法
CodeQL的查询语法有点像SQL,如果你学过基本的SQL语句,基本模式应该不会陌生。
import java from int i where i = 1 select i
第一行表示我们要引入CodeQl的类库,因为我们分析的项目是java的,所以在ql语句里,必不可少。
from int i
,表示我们定义一个变量i,它的类型是int,表示我们获取所有的int类型的数据;
where i = 1
, 表示当i等于1的时候,符合条件;
select i
,表示输出i。
一句话总结就是:在所有的整形数字i中,当i==1的时候,我们输出i。打印一下看看:
QL查询的语法结构为:
from [datatype] var where condition(var = something) select var
类库
上面我们提到,我们需要把我们的靶场项目,使用CodeQL引擎转换成CodeQL可以识别的database(micro-service-seclab-database),这个过程当中,CodeQL引擎把我们的java代码转换成了可识别的AST数据库。
AST Code大体长这个样子:
我们的类库实际上就是上面AST的对应关系。
怎么理解呢?比如说我们想获得所有的类当中的方法,在AST里面Method代表的就是类当中的方法;比如说我们想过的所有的方法调用,MethodAccess获取的就是所有的方法调用。
我们经常会用到的ql类库大体如下:
名称 | 解释 |
---|---|
Method | 方法类,Method method表示获取当前项目中所有的方法 |
MethodAccess | 方法调用类,MethodAccess call表示获取当前项目当中的所有方法调用 |
Parameter | 参数类,Parameter表示获取当前项目当中所有的参数 |
结合ql的语法,我们尝试获取micro-service-seclab项目当中定义的所有方法:
import java from Method method select method
我们再通过Method类内置的一些方法,把结果过滤一下。比如我们获取名字为getStudent的方法名称。
import java from Method method where method.hasName("getStudent") select method.getName(), method.getDeclaringType()
method.getName() 获取的是当前方法的名称
method.getDeclaringType() 获取的是当前方法所属class的名称。
谓词
和SQL一样,where部分的查询条件如果过长,会显得很乱。CodeQL提供一种机制可以让你把很长的查询语句封装成函数。
这个函数,就叫谓词。
比如上面的案例,我们可以写成如下,获得的结果跟上面是一样的:
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
在CodeQL中我们通过
override predicate isSource(DataFlow::Node src) {}
方法来设置source
。
思考一下,在我们的靶场系统(micro-service-seclab)中,source是什么?
我们使用的是Spring Boot
框架,那么source就是http参数入口的代码参数,在下面的代码中,source就是username:
@RequestMapping(value = "/one") public List<Student> one(@RequestParam(value = "username") String username) { return indexLogic.getStudent(username); }
在下面的代码中,source就是Student user
(user为Student类型,这个不受影响)。
@PostMapping(value = "/object") public List<Student> objectParam(@RequestBody Student user) { return indexLogic.getStudent(user.getUsername()); }
本例中我们设置Source的代码为:
override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }
这是SDK
自带的规则,里面包含了大多常用的Source入口。我们使用的SpringBoot也包含在其中, 我们可以直接使用。
注: instanceof 语法是CodeQL提供的语法,后面在CodeQL进阶部分我们会讲到。
设置Sink
在CodeQL中我们通过
override predicate isSink(DataFlow::Node sink) { }
方法设置Sink。
在本案例中,我们的sink应该为query
方法(Method)的调用(MethodAccess),所以我们设置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) ) }
注:以上代码使用了exists子查询语法,格式为exists(Obj obj| somthing), 上面查询的意思为:查找一个query()方法的调用点,并把它的第一个参数设置为sink。
在靶场系统(micro-service-seclab
)中,sink就是:
jdbcTemplate.query(sql, ROW_MAPPER);
因为我们测试的注入漏洞,当source变量流入这个方法的时候,才会发生注入漏洞!
Flow数据流
设置好Source和Sink,就相当于搞定了首尾,但是首尾是否能够连通才能决定是否存在漏洞!
一个受污染的变量,能够毫无阻拦的流转到危险函数,就表示存在漏洞!
这个连通工作就是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,系统就会自动帮我们判断是否存在漏洞了。
初步成果
在CodeQL中,我们使用官方提供的TaintTracking::Configuration方法定义source和sink,至于中间是否是通的,这个后面使用CodeQL提供的config.hasFlowPath(source, sink)
来帮我们处理。
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) ) } }
CodeQL语法和Java类似,extends代表集成父类TaintTracking::Configuration。
这个类是官方提供用来做数据流分析的通用类,提供很多数据流分析相关的方法,比如isSource(定义source),isSink(定义sink)
src instanceof RemoteFlowSource 表示src 必须是 RemoteFlowSource类型。在RemoteFlowSource里,官方提供很非常全的source定义,我们本次用到的Springboot的Source就已经涵盖了。
我们最终第一版写的demo.ql如下:
/** * @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"
注:上面的注释和其它语言是不一样的,不能够删除,它是程序的一部分,因为在我们生成测试报告的时候,上面注释当中的name,description等信息会写入到审计报告中。
这样我们最终拿到注入漏洞。
误报解决
我们在上面自动审计出来的SQL注入漏洞当中,发现了一个误报问题。
这个方法的参数类型是List<Long>,不可能存在注入漏洞。
这说明我们的规则里,对于List<Long>,甚至List<Integer>类型都会产生误报,source误把这种类型的参数涵盖了。
我们需要采取手段消除这种误报。
这个手段就是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.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 ) }
以上代码的意思为:如果当前node节点的类型为基础类型,数字类型和泛型数字类型(比如List)时,就切断数据流,认为数据流断掉了,不会继续往下检测。
重新执行query,我们发现,刚才那条误报已经被成功消除啦。
漏报解决
我们发现,如下的SQL注入并没有被CodeQL捕捉到。
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
方法。
实现原理就一句话:
断了就强制给它接上。
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()就变得可控了。这样应该就能识别出这个注入漏洞了。
我们试一下。
/**
* @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
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 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 isSink(DataFlow::Node sink) {
exists(Method method, MethodAccess call |
method.hasName("query")
and
call.getMethod() = method and
sink.asExpr() = call.getArgument(0)
)
}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"
注:以上我们实现了一个isTaintedString
谓词,并使用exists子查询的方式实现了强制把Optional<String> username
关联Optional<String> username.get()
。
最终,我们的这个注入被跑了出来。
这样我们就简单粗暴的,强制把数据流连通了,这个类型的SQL注入也就可以检测出来了。
Lombok问题
Lombok是一个非常有名的Java类库。
它通过简单的注解来帮助我们简化消除一些必须有但显得很臃肿的Java代码的工具。
我们举个例子:
比如如下Java代码:
package com.l4yn3.microserviceseclab.data; public class Student { private int id; private String username; private int sex; private int age; public void setId(int id) { this.id = id; } public int getId() { return id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public void setAge(int age) { this.age = age; } public int getAge() { return age; } public void setSex(int sex) { this.sex = sex; } public int getSex() { return sex; } }
我们需要自己手动编写各个属性的Getter和Setter方法,这是一个非常繁琐的工作。
但是使用Lombok的注解,我们可以省略掉这个工作。
下面的代码跟上面的代码是等价的。
package com.l4yn3.microserviceseclab.data; import lombok.Data; @Data public class Student { private int id; private String username; private int sex; private int age; }
但是由于lombok的实现机制,导致CodeQL无法获取到lombok自动生成的代码,所以就导致使用了lombok的代码即使存在漏洞,也无法被识别的问题。
还好CodeQL官方的issue里面,有人给出了这个问题的解决办法(查看)。
# 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流能够顺利走下去,
从而检索到安全漏洞。
最终代码
经过以上的调优,我们得到了最终的注入漏洞检测规则。
最终demo.ql代码为:
* @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 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 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 isSink(DataFlow::Node sink) { exists(Method method, MethodAccess call | method.hasName("query") and call.getMethod() = method and sink.asExpr() = call.getArgument(0) ) } 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"
持续工程化
到此为止,我们编写了SQL注入的查询语句,消除了误报和漏报问题。当前的规则已经能够适应micro-service-seclab项目啦。
因为我们的micro-service-seclab项目,是按照标准生成的微服务结构,那么我们可以使用这个ql规则去跑其他的项目,来自动化检测其它项目,从而做到自动化检测,提高安全检测效率。
CodeQL除了提供VSCode的检测插件,也提供了大量的命令行,来实现项目的集成检测。
比如:
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)。
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语句。
instanceof是用来优化代码结构非常好的语法糖。
我们都知道,我们可以使用exists(|)这种子查询的方式定义source和sink,但是如果source/sink特别复杂(比如我们为了规则通用,可能要适配springboot, Thrift RPC,Servlet等source),如果我们把这些都在一个子查询内完成,比如 condition 1 or conditon 2 or condition 3, 这样一直下去,我们可能后面都看不懂了,更别说可维护性了。
况且有些情况如果一个子查询无法完成,那么就更没法写了。
instanceof给我们提供了一种机制,我们只需要定义一个abstract class,比如这个案例当中的:
/** 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类型就可以了。
override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }
学过java的人可能会很费解,我们继承了一个abstract抽象类,连个实现方法都没有,怎么就能够达到获取各种source的目的呢?
CodeQL和Java不太一样,只要我们的子类继承了这个RemoteFlowSource类,那么所有子类就会被调用,它所代表的source也会被加载。
我们在 RemoteFlowSource定义下面会看到非常多子类,就是这个道理,它们的结果都会被用and串联加载。
递归问题
递归调用可以帮助我们解决一类问题:就是我们不确定这个方法我们需要调用多少次才能得到我们的结果,这个时候我们就可以用递归调用。
CodeQL里面的递归调用语法是:在谓词方法的后面跟*或者+,来表示调用0次以上和1次以上(和正则类似),0次会打印自己。
我们举一个例子:
在Java语言里,我们可以使用class嵌套class,多个内嵌class的时候,我们需要知道最外层的class是什么怎么办?
比如如下代码:
public class StudentService { class innerOne { public innerOne(){} class innerTwo { public innerTwo(){} public String Nihao() { return "Nihao"; } } public String Hi(){ return "hello"; } } }
我们想要根据innerTwo类定位到最外层的StudentService类,怎么实现?
按照非递归的写法,我们可以这样做:
import java from Class classes where classes.getName().toString() = "innerTwo" select classes.getEnclosingType().getEnclosingType() // getEnclosingtype获取作用域
我们通过连续2次调用getEnclosingType方法是能够拿到最外层的StudentService的。
但是正如我们所说,实际情况是我们并不清楚一共有多少层嵌套,而且多个文件可能每个的嵌套数量都不一样,我们没法用确定的调用次数来解决此问题,这个时候我们就需要使用递归的方式解决。
我们在调用方法后面加*(从本身开始调用)或者+(从上一级开始调用),来解决此问题。
from Class classes where classes.getName().toString() = "innerTwo" select classes.getEnclosingType+() // 获取作用域
我们也可以自己封装方法来递归调用。
import java RefType demo(Class classes) { result = classes.getEnclosingType() } from Class classes where classes.getName().toString() = "innerTwo" select demo*(classes) // 获取作用域
强制类型转换问题
在CodeQL的规则集里,我们会看到很多类型转换的代码,比如:
这里是对getType()的返回结果做强制类型转换。其实CodeQL当中的强制类型转换,理解成filter更贴切一点,它的意思是将前面的结果符合RefType的数据都留下,不符合的都去掉。
以上class 继承了Parameter,那么getType()目的就是获取项目中所有的参数的type信息。
我们用如下QL语句做个测试:
import java from Parameter param select param, param.getType()
以上代码的含义是打印所有方法参数的名称和类型。
我们看到一共有233条结果,并且结果当中含有String,int和其他自定义类型,这是我们没有做任何强制类型转换的结果。
然后我们试着执行:
import java from Parameter param select param, param.getType().(RefType)
强制转换成RefType,意思就是从前面的结果当中过滤出RefType类型的参数。RefType是什么?引用类型,说白了就是去掉int等基础类型之后的数据。
数据只有181条了。
更直观的测试,我们可以过滤保留所有的数值类型。
import java from Parameter param select param, param.getType().(IntegralType)
总结
CodeQL的语法极其强大,希望本文能让你对使用CodeQL进行自动化漏洞检测有一些认识。
前面我们曾经提到,虽然CodeQL
很强大,我们还是希望企业能同时购买一款优秀的商业产品。原因除了CodeQL
的使用协议里禁止在企业的CI/CD里部署外,最重要的原因是:我们应该用两款不同的SAST工具来对比误报和漏报问题,相互补充,相互完善规则,这样才能不断提高准确率,不然你如何来发现漏报问题呢?
如果你想继续研究,我建议你下载本文当中提到的micro-service-seclab
项目,按照本文一步步测试。
当然,详细阅读官方的文档永远是最好的方式。
因为本文内容较长,编写过程中如有错误,可以反馈给我,我的微信号:l4yn3liu,再次表示感谢。
联系方式
微信号: l4yn3liu
参考
https://codeql.github.com/docs/