为什么要写这篇文章?
自从Github宣布推出CodeQL,国外越来越多安全人员使用这个项目做代码安全评估工作,截止到此刻,CodeQL在Github上已经有超过3100个Star。
但是国内了解CodeQL的安全人员并不多,能google到的关于codeql的中文文章比较少。大部分中文文章,都是介绍CodeQL是什么之后,用简单的代码片段说明CodeQL的某个功能,很少有非常全面的介绍使用CodeQL对一个项目做漏洞分析的文章。这让想学习的读者一头雾水,还是不知道该如何在自己的项目上使用CodeQL。
所以我想写一篇文章,从安装开始,到编写QL规则实现漏洞的自动化扫描,再到解决误报,漏报问题,让读者能够真正的了解该如何使用CodeQL自动化审计自己项目的安全性。
靶场介绍
我使用SpringBoot
简单的实现了一个靶场。这个靶场里面包含注入漏洞和一些其它的漏洞。我们本篇文章的目的,就是要使用CodeQL
来自动检索出里面的注入漏洞,并且排除误报,解决漏报,还有解决一些其它问题。
OS: Mac
Java JDK: 1.8
Maven: Apache Maven 3.6.3
您可以点击此处下载到这个简单的测试靶场系统(micro-service-lab)。
CodeQL是什么?
如果您已经了解CodeQL是什么,可以直接跳过这个章节。
在回答这个问题之前,我们来看一下安全人员做代码审计的进化史。
在代码审计的早期,安全研究人员会是通过人工审计的方式来做作项目的代码审计,主要是通过查找用户可控的参数和危险函数,然后跟进危险函数的参数是否可控,如果用户可控的参数没有做处理,那么项目就可能存在安全漏洞。
但是随着业务快速的发展,项目迭代周期变短以及代码数量的增加,如果是纯靠人工的方式很难实现所有项目漏洞的覆盖测试。所以出现了一些辅助人工审计的工具,比如前几年比较火的rips,cobra,通过这些工具,可以把危险函数代码代码检索出来,再通过人工审计来判断是否存在安全漏洞。
上面的方式主要还是需要人工来介入进来进行判断,工作量还是很大,并且非常依赖安全工程师的个人能力。但是近些年出现了不少优秀的自动化代码安全审计产品,比如非常有名的Checkmarx,Fortify SCA。这些软件可以自动化的帮我们审计出安全漏洞,大大减少了人工工作量,并加快了安全审计速度。但是这些软件都是商业的,价格比较贵,一般企业可能没有这么多预算购买。
与此同时,Github为了解决其托管的海量项目的安全性问题,收购了CodeQL的创业公司,并宣布开源CodeQL的规则部分,这样全世界的安全工程师就可以贡献高效的QL审计规则给Github,帮助它解决托管项目的安全问题。
对于安全工程师,也就多了一个非商业的开源代码自动化审计工具。
CodeQL支持非常多的语言,在官网有如下支持的语言和框架列表
CodeQL for research | GitHub Security Lab
注:CodeQL被禁止用于企业内部的CI/CD流程,我们可以用来做安全研究。同时我还是建议企业购买一款商业的SAST代码审计工具,原因我们最后说。
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
的开发环境,是不是很简单?
后面我们将进入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的基本语法:
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的对应关系。
怎么理解呢?比如说我们想获得所有的类当中的方法,在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的链路是通的,才表示当前漏洞是存在的。
...........