简介
CodeQL 是一个语义代码分析引擎,它可以扫描发现代码库中的漏洞;通过对项目源码(C/C++、C#、golang、java、JavaScript、typescript、python)进行完整编译,并在此过程中把项目源码文件的所有相关信息(调用关系、语法语义、语法树)存在数据库中,然后编写QL代码或使用自带的QL规则查询该数据库来发现安全漏洞。
简而言之:codeql是一个可以对代码进行分析的引擎, 安全人员可以用它作为挖洞的辅助或者直接进行挖掘漏洞,节省进行重复操作的精力
环境配置
CodeQL本身包含两部分:解析引擎+SDK。
- 解析引擎用来解析我们编写的规则,虽然不开源,但是我们可以直接在官网下载二进制文件直接使用;
- SDK完全开源,里面包含大部分现成的漏洞规则,我们也可以利用其编写自定义规则。
安装
codeql-cli(解析引擎):二进制可执行文件,下载即可
ql库(SDK):已经写好的可以查询安全漏洞的代码,克隆到本地即可
mkdir ~/codeql && cd ~/codeql
wget https://github.com/github/codeql-cli-binaries/releases/download/v2.8.4/codeql-osx64.zip && unzip codeql-osx64.zip
git clone https://github.com/github/codeql.git ql
两个目录必须放置到同级,因为codeql-cli把同层目录作为查找路径,如下图:
为了方便使用可以给codeql-cli
添加到环境变量中
# codeql
export PATH="/Users/d4m1ts/codeql/codeql:$PATH"
vscode插件
CodeQL可以使用VS Code
插件来开发和调试规则,简化我们的操作,非常方便,直接在扩展商店安装即可。
最后配置一下codeql的路径即可
基础使用
整个工作流程主要分为两步:
- 提取创建数据库
- 编写QL语句进行查询
准备分析的源码
需要先准备扫描分析的源码,可以用micro_service_seclab
这个github项目(注意:需要切换到master
分支,默认为main
分支,存在异常)
git clone https://github.com/l4yn3/micro_service_seclab.git
创建源码数据库
由于CodeQL
的处理对象并不是源码本身,而是中间生成的抽象语法树(AST)结构数据库,所以我们先需要把我们的项目源码转换成CodeQL
能够识别的CodeDatabase
。
codeql database create ~/codeql/micro-service-seclab-database --language=java --command="mvn clean install --file pom.xml" --source-root=./micro_service_seclab
参数含义可以通过codeql database create -h
进行查看,基本上都能猜出来是啥意思
导入源码数据库到VS Code
和数据库一样,要指定一个数据库,才知道从哪里取数据进行分析,这里我们用刚才生成的micro-service-seclab-database
点击Set Current Database
,前面出现 √ 说明加载成功
编写QL规则查询
在~/codeql/ql/java/ql/examples/test.ql
中编写测试代码,因为examples
目录下有qlpack.yml
就不需要再新建了。
[!tip]
codeQL规则有包结构/目录结构要求(
qlpack.yml
定义一个package),才能正常编译、执行。参考:https://codeql.github.com/docs/codeql-cli/using-custom-queries-with-the-codeql-cli/
编写后右键,然后点击Run Query
即可出现运行结果。
寻找没有使用的参数
import java
from Parameter p
where not exists(p.getAnAccess())
select p
输出报告+所有漏洞扫描
在生成数据库的时候,我们用到了codeql-cli
,中间我们用vscode编写ql规则和验证的时候,其实也是用到了codeql命令行操作,只不过vscode代替我们执行了命令。
如果想要输出报告,可以用如下的命令:
codeql analyze命令可以执行单个ql文件,目录下所有ql文件,和查询suite(.qls)
codeql database analyze ~/codeql/micro-service-seclab-database ~/codeql/ql/java/ql/examples/test.ql --format=csv --output=result.csv --rerun
如果要执行所有的漏洞扫描,可以使用如下命令
codeql database analyze ~/codeql/micro-service-seclab-database ~/codeql/ql/java/ql/src/codeql-suites/java-security-extended.qls --format=csv --output=result.csv --rerun
CodeQL基本语法
因为CodeQL的解析引擎帮我们给需要审计的项目解析成了AST结构的数据库,所以我们只需要编写相关的QL规则,然后引擎会根据代码去找到满足条件的点,我们再分析即可。
从上面也可以看出来,QL规则语法和SQL差不多
语法结构
QL查询的语法结构为:
from [datatype] var
where condition(var = something)
select var
一个简单的例子如下:
import java
from int i
where i = 1
select i
- 第一行表示我们要引入CodeQL的类库,因为我们分析的项目是java的,所以在ql语句里,必不可少
- 第三行表示定义一个int型变量i,表示我们获取所有的int类型的数据
- 第四行为判定条件
- 第五行为输出i
简单来说:在所有的整形数字i中,当i==1的时候,就输出i
类库
刚才说了解析引擎给代码解析为了AST
数据库,AST Code如下:
前面的[]
中的内容,就可以理解为类库,我们可以通过它来获取所有的内容,比如Method
代表的就是所有类中的方法,Parameter
就代表方法中的参数
我们经常会用到的ql类库大体如下:
名称 | 解释 |
---|---|
Method | 方法类,Method method表示获取当前项目中所有的方法 |
MethodAccess | 方法调用类,MethodAccess call表示获取当前项目当中的所有方法调用 |
Parameter | 参数类,Parameter表示获取当前项目当中所有的参数 |
实例一:获取所有的方法
import java
from Method i
select i
实例二:过滤掉部分方法
通过添加判断条件,过滤掉部分不满足条件的方法;
获取名字为getStudent的方法的名称、参数和所属类
import java
from Method i
where i.hasName("getStudent")
select i.getName(),i.getAParameter(),i.getDeclaringType()
谓词
where部分的查询条件如果过长,会显得很乱。CodeQL提供一种机制可以让你把很长的查询语句封装成函数;这个函数,就叫谓词
以上面的实例二为例:
import java
predicate testFunc(Method method) {
exists( | method.hasName("getStudent") | method.getDeclaringType().toString()="IndexDb" )
}
from Method i
where testFunc(i)
select i.getName(),i.getAParameter(),i.getDeclaringType()
predicate
表示当前方法没有返回值exists
子查询,是CodeQL谓词语法里非常常见的语法结构,它根据内部的子查询返回true
orfalse
,来决定筛选出哪些数据;|
前后存在上下文关系,并列关系可以用and
或者or
。
设置Source和Sink(污点追踪)
在代码自动化安全审计的理论当中,有一个最核心的三元组概念,就是(
source
,sink
和sanitizer
)。source
:是指漏洞污染链条的输入点;比如获取http请求的参数部分,就是非常明显的Source。sink
:是指漏洞污染链条的执行点;比如SQL注入漏洞,最终执行SQL语句的函数就是sink(这个函数可能叫query或者exeSql,或者其它)。sanitizer
:又叫净化函数,是指在整个的漏洞链条当中,如果存在一个方法阻断了整个传递链,那么这个方法就叫sanitizer
。
只有当source和sink同时存在,并且从source到sink的链路是通的,才表示当前漏洞是存在的。
设置source
设置头
source就是输入点,如下面的username
codeql通过如下代码来设置source,这是SDK
自带的规则,里面包含了大多常用的Source入口。SpringBoot也包含在其中, 可以直接使用。
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
instanceof
语法是CodeQL提供的语法- 重写的是
TaintTracking::Configuration
中的isSource
设置sink
设置尾
在CodeQL中我们通过如下函数设置Sink。
override predicate isSink(DataFlow::Node sink) {
}
比如我们想编写SQL注入的sink,那就应该是query
方法(Method)的调用(MethodAccess),所以实现的代码如下:
override predicate isSink(DataFlow::Node sink) {
exists(Method method, MethodAccess call |
method.hasName("query")
and
call.getMethod() = method and
sink.asExpr() = call.getArgument(0)
)
}
代码解析:查找一个query()
方法的调用点,并把它的第一个参数设置为sink
也可以用SDK内置的已经写好的sink规则
override predicate isSink(DataFlow::Node sink) { sink instanceof QueryInjectionSink }
- 重写的是
TaintTracking::Configuration
中的isSink
Flow数据流
设置好Source
和Sink
,相当于搞定了首尾,但是首尾连通才能存在漏洞;也就是说一个受污染的变量,能够毫无阻拦的流转到危险函数,那么就可以确定漏洞存在。
这个连通工作是由CodeQL来完成的,我们调用内置的config.hasFlowPath(source, sink)
方法来判断是否连通,其中source
和sink
需要自己定义
from DataFlow::PathNode source, DataFlow::PathNode sink, XPathInjectionConfiguration c
where c.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "$@ flows to here and is used in an XPath expression.",
source.getNode(), "User-provided value"
初尝试
上面基本上有了整个流程,但是我们要怎么给他衔接起来呢?可以找已经写好的例子对照参考
比如ql/java/ql/src/Security/CWE/CWE-643/XPathInjection.ql
/**
* @name XPath injection
* @description Building an XPath expression from user-controlled sources is vulnerable to insertion of
* malicious code by the user.
* @kind path-problem
* @problem.severity error
* @security-severity 9.8
* @precision high
* @id java/xml/xpath-injection
* @tags security
* external/cwe/cwe-643
*/
import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
import semmle.code.java.security.XPath
import DataFlow::PathGraph
class XPathInjectionConfiguration extends TaintTracking::Configuration {
XPathInjectionConfiguration() { this = "XPathInjection" }
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
override predicate isSink(DataFlow::Node sink) { sink instanceof XPathInjectionSink }
}
from DataFlow::PathNode source, DataFlow::PathNode sink, XPathInjectionConfiguration c
where c.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "$@ flows to here and is used in an XPath expression.",
source.getNode(), "User-provided value"
改造后
/**
* @name SQL injection
* @description SQL注入
* @kind path-problem
* @problem.severity error
* @security-severity 9.8
* @precision high
* @id java/test/sql-injection
*/
import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
import semmle.code.java.security.QueryInjection
import DataFlow::PathGraph
class SQLInjectionConfiguration extends TaintTracking::Configuration {
SQLInjectionConfiguration() { this = "SQLInjection" }
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
override predicate isSink(DataFlow::Node sink) { sink instanceof QueryInjectionSink }
}
from DataFlow::PathNode source, DataFlow::PathNode sink, SQLInjectionConfiguration c
where c.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "$@ flows to here and is sql injection vuln",source.getNode(), "vuln"
[!note]
- 上面的注释和其它语言是不一样的,不能够删除,它是程序的一部分,因为在我们生成测试报告的时候,上面注释当中的
name
,description
等信息会写入到审计报告中。- select输出的结果一定要满足预定的规则,如果有异常可以根据报错来修改,不然无法导出结果
运行后得到的结果
误报解决(净化函数)
上面的第五行结果,跟进一下代码,传入参数的List是Long型的,所以不可能存在SQL注入
所以这就属于误报,那么我们就需要消除误报,消除的方法就是利用isSanitizer
函数,重写TaintTracking::Configuration
中的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 )
}
结果成功少了第五行
lombok
开发过java的应该都清楚,由于java的封装特性,每一个变量都要写setter
和setter
很麻烦,所以就有了lombok
,引入以来后通过@Data
注解就可以自动实现getter
和setter
(不是自动补全代码的方式实现)
如下:
// 原代码
public class Student {
private int id;
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
}
等价于
// 使用lombok
import lombok.Data;
@Data
public class Student {
private int id;
}
但是codeql不能识别lombok
的getter
和setter
,所以可能存在问题也发现不了,因此用codeql分析代码的时候,如果存在lombok
,那么可以通过如下的方法快速还原setter
和getter
方法,来自github 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"
进阶
主要是一些说明和补充
instanceof
和java类似,比如sink instanceof QueryInjectionSink
表示判断sink
是QueryInjectionSink
类型
我们要实现这种机制,只需要创建一个abstract抽象类
,如下图,不过这里和java的抽象类有区别,只要我们的子类继承了这个类,那么所有子类都会被调用
递归
CodeQL里面的递归调用语法是:在谓词方法的后面跟*
或者+
,来表示调用0次以上和1次以上(和正则类似),0次会打印自己。
import java
RefType demo(Class classes) {
result = classes.getEnclosingType()
}
from Class classes
where classes.getName().toString() = "innerTwo"
select demo*(classes) /* 获取作用域 */
类型过滤
CodeQL通过.(type)
进行类型过滤,可以理解成filter,它的意思是将前面的结果符合Type
的数据保留
如.(RefType)
,就是保留RefType
类的内容
如:
关于RefType
是啥可以通过如下代码来对比,看看过滤了什么:
import java
from Parameter p
select p, p.getType()
过滤后,结果中所有的int
等基础数据都没了
import java
from Parameter p
select p, p.getType().(RefType)
再来一个保留数字的
import java
from Parameter p
select p, p.getType().(IntegralType)
扫描github开源项目
https://lgtm.com/search?q=xiaomi
编写规则
https://lgtm.com/query/lang:java/