为什么要写这篇文章?

自从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/

CodeQL自动化代码审计工具相关推荐

  1. php工具能自动出代码的,打造自己的PHP半自动化代码审计工具

    一.PHP扩展进行代码分析(动态分析) 基础环境 apt-get install php5 apt-get install php5-dev apt-get install apache apt-ge ...

  2. php模板读取工具,打造自己的php半自动化代码审计工具

    0x00 PHP扩展进行代码分析(动态分析) 一.基础环境 #!bash apt-get install php5 apt-get install php5-dev apt-get install a ...

  3. PHP代码审计工具——rips

    一.代码审计工具介绍 代码审计工具可以辅助我们进行白盒测试,大大提高漏洞分析和代码挖掘的效率. 在源代码的静态安全审计中,使用自动化工具辅助人工漏洞挖掘,一款好的代码审计软件,可以显著提高审计工作的效 ...

  4. 静态代码审计工具Cobra/Cobra-W/find-sec-bugs

    8.4更新 cobra关键解析流程: ## cli.py ->start scan_engine(target_directory=target_directory, a_sid=a_sid, ...

  5. license 验证服务器唯一机器码_代码审计工具Fortify 17.10及Mac平台license版本

    介绍17.10版本安装指导工具使用云端试用价值 介绍 Fortify SCA是一个静态源代码安全测试工具.它通过内置的五大主要分析引擎对源代码进行静态的分析和检测,分析的过程中与其特有的软件安全漏洞规 ...

  6. 第43篇:国内商用代码审计工具CodePecker啄木鸟的使用教程

     Part1 前言  最近感染了新冠,大病初愈,没有气力写复杂的文章了,就抽空把<代码审计工具系列教程>写完吧,前面几期介绍了商用代码审计工具Fortify.Checkmarx.Cover ...

  7. 常见代码审计工具,代码审计为什么不能只用工具?

    代码审计是一种发现程序漏洞,安全分析为目标的程序源码分析方式.今天主要分享的是几款常用的代码审计工具,以及代码审计工具有哪些优缺点? 代码审计工具 seay代码审计工具,是一款开源的利用C#开发的一款 ...

  8. XSS漏洞自动化攻击工具XSSer

    XSS漏洞自动化攻击工具XSSer XSS是Web应用常见的漏洞.利用该漏洞,安全人员在网站注入恶意脚本,控制用户浏览器,并发起其他渗透操作.XSSer是Kali Linux提供的一款自动化XSS攻击 ...

  9. Python—自动化部署工具:Fabric

    Fabric是python编写的一款自动化部署工具 Fabric依赖paramiko进行SSH交互,某种意义上Fabric是对paramiko的封装,封装完成后,不需要像使用paramiko一样处理S ...

  10. 浅谈PHP自动化代码审计技术

    原文出处: exploit   欢迎分享原创到伯乐头条 0×00 由于博客实在没什么可以更新的了,我就把目前做的事情总结一下,当做一篇博客,主要是谈一谈项目中所运用的一些技术.目前市面上有不少PHP的 ...

最新文章

  1. python有道翻译-Python爬去有道翻译
  2. 落地华东总部、上线创新云、签约AIoT产业基金……京东云南京“新动作”...
  3. [Qt教程] 第31篇 网络(一)Qt网络编程简介
  4. 由防重复点击引发的幂等性问题思考
  5. link2001错误无法解析外部符号metaObject
  6. mysql5.6免安装乱码_mysql5.6乱码问题的几个注意
  7. SpaceVim 1.1.0 发布,模块化 Vim IDE
  8. 沙盒机制和应用程序目录
  9. 搜狗浏览器收藏夹在哪_搜狗浏览器居然流氓到操作我的微博账号
  10. java 对象复制_程序员应该知道java虚拟机的22个重难点(干货)
  11. 软件过程改进的实施建议
  12. js实现斗地主计分器
  13. 电磁流量计的工作原理
  14. 数据库事务(Transaction)详解
  15. mariadb特有函数
  16. 光盘镜像和系统启动盘制作
  17. OSError: [Errno 22] Invalid argument: ‘C:\\Users\\0moyi0\\Desktop\\AD_kk.png‘
  18. LeetCode——1736. 替换隐藏数字得到的最晚时间(Latest Time by Replacing Hidden Digits)——分析及代码(Java)
  19. 莫纳什计算机专业强吗,2020年莫纳什大学计算机科学专业好不好
  20. 一位期货老将的经验之谈(转)

热门文章

  1. 【整理】Ubuntu10.04下安装Altera9.1套件
  2. DAVINCI DM6446 开发攻略——V4L2视频驱动和应用分析 1
  3. 甘肃暴雨强度公式_我国若干城暴雨强度公式列表.doc
  4. Java学习笔记 --- IDEA
  5. 五笔输入法的前世今生
  6. 英语六级高频词汇速记 + 2018-6-2听力 Day02
  7. C语言图书管理系统 文件数据库
  8. xy苹果助手未受信任_【iOS教程】不用电脑 安装苹果软件
  9. 关于绕开百度文库复制限制的那档子事
  10. hosts文件位置和修复hosts文件