文章目录
- 前言
- 环境搭建
- 1.1 codeql基础
- 1.2 vscode插件
- 1.3 生成数据库
- 1.4 HelloWorld
- codeql语法
- 2.1 语法结构
- 2.2 常用类库
- 2.3 谓词介绍
- 2.4 污点分析
- 漏洞检测
- 3.1 初步结果
- 3.2 解决误报
- 总结
前言
对于代码审计的工作,最早期的安全人员会以人工审计的方式来审计项目代码,通过查找危险函数并跟进危险函数的参数是否可控,如果可控则说明存在安全漏洞。但是随着项目数量的增加,纯靠人工的方式很难实现所有项目漏洞的覆盖测试。所以近些年出现了不少优秀的自动化代码安全审计产品,比如非常有名的 Checkmarx,Fortify SCA。但是这些软件都是商业的,价格比较贵。
与此同时,Github 为了解决其托管的海量项目的安全性问题,收购了 CodeQL 的创业公司,并宣布开源 CodeQL 的规则部分,这样全世界的安全工程师就可以贡献高效的 QL 审计规则给 Github(GitHub实验室对高质量的 CodeQL 规则发起了奖金计划),帮助它解决托管项目的安全问题。而对于安全工程师,也就多了一个非商业的开源代码自动化审计工具。
简单来说,CodeQL 是一种用于漏洞挖掘的代码审计工具,它可以根据已有的漏洞模型在庞大的代码库中找出相同类型的漏洞的变种,即可以帮助安全研究员或开发人员更快速精准地定位漏洞之处。研究员根据特有的 CodeQL 语法编写 CodeQL 规则,这个规则实质上就是描述已有漏洞的特征,若在目标代码库中存在该特征的代码段就定位出来,方便进一步分析是否为漏洞,极大提高了效率。
CodeQL 目前支持进行漏洞挖掘的语言包括:C/C++、C#、Go、Java、JavaScript、Python、Ruby。而 QL 是一门类似 SQL 的查询语言,通过对源码进行完整编译,并在此过程中把源码文件的所有相关信息(调用关系、语法语义、语法树)存在数据库中,然后编写代码查询该数据库来发现安全漏洞(硬编码 / XSS 等)。
环境搭建
CodeQL 本身包含两部分:
- 解析引擎:解析引擎用来解析我们编写的规则,虽然不开源,但是可以直接在官网下载二进制文件直接使用;
- SDK:SDK完全开源,里面包含大部分现成的漏洞规则,也可以利用其编写自定义规则。
1.1 codeql基础
首先下载解析引擎 CodeQL CLI,它是一个可执行的命令行工具,可以使用 CodeQL CLI运行 CodeQL 分析、创造 CodeQL 数据库、开发和测试自定义 CodeQL 查询。
本人是 Win10 笔记本,下载 codeql-win64.zip 并解压缩,然后在系统环境变量 PATH 中添加:
D:\Security\CodeQL\codeql-win64
最后验证环境变量配置是否成功:
接着下载 CodeQL SDK,或者说是标准的扫描规则 CodeQL libraries and queries。以下仓库包含了标准的 CodeQL 库和查询语句:
https://github.com/github/codeql
下载后是一个 codeql-main.zip 压缩包,解压到与解析引擎同一根目录下:
在此补充说明下:
- micro_service_seclab 文件夹是 Github 某大佬提供的用于 CodeQL 测试的开源漏洞靶场,下载地址:micro_service_seclab;
- 而 Database 文件夹则是我手动创建的,用于存放 CodeQL 编译 micro_service_seclab 项目生成的数据库,具体过程下文会提到。
1.2 vscode插件
VsCode 的扩展里提供了 CodeQL 的插件,可用于开发和调试 CodeQL 规则,在扩展里面搜索 codeql 后直接点击安装即可:
然后在如下图位置点击“拓展设置”:
填入本地下载并解压缩后的解析引擎 CodeQL CLI 的路径:
接着可以(也可以先忽略此步)在 VSCode 打开扫描规则 CodeQL libraries and queries(即上文下载的 codeql-main 文件夹):
注意,由于到这里我们尚未建立存在漏洞的代码项目的数据库,所以还不能直接运行 Run Query进行漏洞扫描。但是至此,CodeQL 的基本开发环境已搭建完毕。
1.3 生成数据库
下文会以 Github 某大佬提供的用于 CodeQL 测试的开源漏洞靶场为例(下载地址:micro_service_seclab),来介绍如何使用 CodeQL 来编写具体的规则分析源码里面的 SQL 注入漏洞点。
由于 CodeQL 的处理对象并不是源码本身,而是中间生成的 AST 结构数据库,所以需要把项目源码转换成 CodeQL 能够识别的 CodeDatabase。
抽象语法树(abstract syntax tree,AST) 是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构。
如下命令可进行 CodeDatabase 的生成工作:
codeql database create Database/micro-service-seclab-database --language="java" --command="mvn clean install --file pom.xml" --source-root=micro_service_seclab
参数解析:
- Database/micro-service-seclab-database:指定了生成的数据库的数据存放路径;
--language="java"
:代表该项目是 Java 语言开发的;--command="mvn clean install --file pom.xml"
:编译命令(因为 Java 是编译语言,所以需要使用--command
命令先对项目进行编译,再进行转换,Python和 PHP 这样的脚本语言不需要此命令),--command
参数如果不指定,则会使用默认的编译命令和参数;--source-root=micro_service_seclab/
:项目源码本地存放路径。
编译如果出错,比如本人遇到如下报错:
可以在 pom.xml 中添加如下代码来避免(参考:https://blog.51cto.com/ios9/3113441):
</project>
……
<build>
<plugins>
<plugin>
……
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<testFailureIgnore>true</testFailureIgnore>
</configuration>
</plugin>
</plugins>
</build>
</project>
最后成功创建本地数据库:
导入数据库
切换到 VSCode 编辑器中,通过如下方式来导入刚刚生成的数据库,选择目录 micro-service-seclab:
1.4 HelloWorld
导入完数据库后,我们便可以正式开始编写 QL 查询规则来对目标代码的 AST 数据库进行特定漏洞规则的匹配查询,但是这需要掌握一定的 QL 语法才行,下面先简单编写 QL 查询语句来输入 “Hello World"。
在 VSCode 打开扫描规则 CodeQL libraries and queries(即上文下载的 codeql-main 文件夹),然后在如下图所示的目录里新建 demo.ql 文件并写入select "Hello World"
, 然后鼠标右键选择CodeQL: Run Query
即可执行。
成功输出 “Hello World”,代表 CodeQL 开发环境和靶场数据库环境均成功准备完毕,可以进行下一步的 CodeQL 污点分析和规则编写(语法)的学习。
codeql语法
在对上文生成的存在大量漏洞的项目代码生成数据库进行 micro-service-seclab 漏洞检查之前,需要先来学习下 CodeQL 的基本语法。
本章节大部分摘自 FreeBuf 博文:CodeQL从入门到放弃,l4yn3大佬文章写得很好很详细,强烈推荐。
上面提到过,CodeQL 的核心引擎是不开源的,这个核心引擎的作用之一是帮助我们把 micro-service-seclab 转换成 CodeQL 能识别的中间层数据库。然后我们需要编写 QL 查询语句来获取我们想要的数据。
正如上图所述,由于 CodeQL 开源了所有的规则和规则库部分,所以我们能够做的就是编写符合我们业务逻辑的 QL 规则,然后使用 CodeQL 引擎去跑我们的规则,从而发现靶场的安全漏洞。
下面来简单地介绍一下本案例涉及到的 CodeQL 基本语法。
2.1 语法结构
CodeQL 的查询语法有点像 SQL,如果你学过基本的 SQL 语句,基本模式应该不会陌生。
import java
from int i
where i = 1
select i
第一行表示我们要引入 CodeQL 的类库,因为我们分析的项目是 java 的,所以在 QL 语句里,import java
必不可少。
- 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
2.2 常用类库
上面我们提到,我们需要把我们的靶场项目,使用 CodeQL 引擎转换成 CodeQL 可以识别的 database(micro-service-seclab-database),这个过程当中,CodeQL 引擎把我们的 java 代码转换成了可识别的 AST 数据库。
AST Code 大体长这个样子(注意需要在 workspace 中将源码文件夹添加进工作区后选择 XXX.java 文件后再在 QL 标签中点击 AST,才能看到对于 java 文件的语法树结构):
我们的类库实际上就是上面 AST 的对应关系。
怎么理解呢?比如说我们想获得所有的类当中的方法,在 AST 里面 Method 代表的就是类当中的方法;比如说我们想过的所有的方法调用, MethodAccess 获取的就是所有的方法调用。
我们经常会用到的 QL 类库大体如下:
结合 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 的名称。
2.3 谓词介绍
和 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,来决定筛选出哪些数据。
2.4 污点分析
个人认为 CodeQL 作为静态代码分析工具,最核心的能力优势的就是可以实现污点分析。什么是污点分析?举个例子就明白了。
eval(md5($_POST['a']));
上述 PHP 代码 eval 函数可以执行系统命令,安全人员会重点关注、审计该类函数的参数是否外部可控,如果外部完全可控那么便是一个代码执行漏洞了。此例中 a 参数显然是外部传递过来的,但是此处并构不成代码执行漏洞,因为在传递给 eval 函数之前,程序调用 md5 函数对外部传入的参数进行了编码转换。
在这个例子中我们就引入了污点分析。污点分析可以抽象成一个三元组<sources,sinks,sanitizers>
的形式:
- source 即漏洞污染链条的输入点:代表直接引入不受信任的数据或者机密数据到系统中(比如获取 http 请求的参数部分,就是非常明显的Source);
- sink 即漏洞污染链条的执行点:代表直接产生安全敏感操作(违反数据完整性)或者泄露隐私数据到外界(违反数据保密性),比如 SQL 注入漏洞,最终执行 SQL 语句的函数就是 sink (这个函数可能叫 query、exeSql 等);
- sanitizer 即无害处理:代表通过数据加密或者移除危害操作等手段使数据传播不再对软件系统的信息安全产生危害(它又称为净化函数,简单说就是在整个的漏洞链条当中,如果存在一个方法阻断了整个传递链,那么这个方法就叫sanitizer)。
上述案例中,$_POST['a']
为 source,eval()
函数为 sink,md5 函数就是无害化处理的 sanitizer。
概括来说,污点分析就是:分析程序中由污点源引入的数据是否能够不经无害处理,而直接传播到污点汇聚点。如果不能,说明系统是信息流安全的;否则,说明系统产生了隐私数据泄露或危险数据操作等安全问题。
下文我们想编写的 QL 规则文件是排查 micro_service_seclab 项目中的 SQL 注入漏洞,下面来看看如何通过 QL 语法正确设置 source、sink 和检测 sanitizer。
1、设置Source
在 CodeQL 中我们通过以下方法来设置source:
override predicate isSource(DataFlow::Node src) {
}
思考一下,在我们的靶场系统 (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 的代码为:
override predicate isSource(DataFlow::Node src) {
src instanceof RemoteFlowSource
}
这是 SDK 自带的规则,里面包含了大多常用的 Source 入口。我们使用的 SpringBoot 也包含在其中,我们可以直接使用。
2、设置Sink
在 CodeQL 中我们通过以下方法设置 Sink:
override predicate isSink(DataFlow::Node 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 变量流入这个方法的时候,才会发生注入漏洞!
3、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,系统就会自动帮我们判断是否存在漏洞了。
漏洞检测
介绍完基本的 QL 语法及污点分析过程后,下面来看看如何通过自定义的 QL 规则,检测出靶场系统的 SQL 注入漏洞。
3.1 初步结果
在 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 就已经涵盖了。
最终第一版写的 QL 漏洞查询规则脚本 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 等信息会写入到审计报告中。
最后来见证效果,通过上述规则脚本,Run Query 最终可拿到注入漏洞的信息。一共报了 5 处漏洞点,并展示了存在漏洞的受控参数从入口点到漏洞触发点的完整传递链路:
批量产洞大概就是这样了吧……凭借如此详尽的数据信息,安全人员可以快速地判断出该结果是否是误报,并确认最终的有效漏洞的污点链路,从而给研发人员提供准确的修复建议。
3.2 解决误报
在上面自动审计出来的SQL注入漏洞当中,发现了一个误报问题。
这个方法的参数类型是List<Long>
,不可能存在注入漏洞。这说明我们的规则里,对于List<Long>
,甚至 List<Integer>
类型都会产生误报,source 误把这种类型的参数涵盖了。
我们需要采取手段消除这种误报,这个手段就是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<Long>
)时,就切断数据流,认为数据流断掉了,不会继续往下检测。
重新执行query,我们发现刚才那条误报已经被成功消除:
关于 CodeQL 更多高级查询语法可参见:https://codeql.github.com/codeql-query-help/java/、CodeQL语法。
总结
可以看到,对于需要进行详细污点分析的漏洞模式,借助 CodeQL 进行漏洞排查是极其有效的。但是个人认为如果只是一些简单的漏洞特征匹配即可、不涉及污点分析的漏洞模式,直接使用 Python 脚本进行静态代码检索匹配反而更为快捷,毕竟 CodeQL 存在两个潜在的大时间成本:编译生成 AST 数据库、编写 QL 规则。
最后总结下使用 CodeQL 进行漏洞挖掘的主要流程:
- 使用 CodeQL 命令,根据目标源代码编译生成 database,该 database 包含整个代码的 AST 树,CodeQL 就是查询该 database 来进行漏洞发现的;
- 根据已知漏洞提炼出漏洞特征,并编写出 CodeQL 规则,该规则用以查询目标 database 找出类似的漏洞。在这一步有两个难点:一个是需要对漏洞进行总结归纳,提炼出漏洞特征,即是什么因素引发了漏洞;另一个是根据漏洞特征编写高质量的 CodeQL 规则,这里需要对 CodeQL 的规则的编写语法有着一定的熟悉;
- 根据 CodeQL 规则运行的结果进行代码审计,分析目标代码是否真正存在漏洞,确认漏洞的成因及触发条件。
本文参考文章:
- CodeQL从入门到放弃;
- CodeQL query help for Java;
- 漏洞发现:代码分析引擎 CodeQL;
- 深入理解CodeQL(Github上对CodeQL全网资源整合的项目);