前言
在学习CodeQL之前,接触的静态分析工具如Tabby, Java-Analyzer等工具有很多局限性,比如只能分析Java代码,后两者更大的优势是寻找Java反序列化链进行黑名单与WAF绕过,而CodeQL可以对不同的编程语言如Java, Go等进行静态分析,代码式的查询语句使其更容易编写检测常规漏洞如SQL注入的规则,在SAST工具盛极一时的当下,学习CodeQL就显得很有必要了。
环境搭建
在Java示范靶场中使用如下命令建立分析数据库
1
| ./codeql database create codeqltest --language=java --command="mvn clean package -DskipTests"
|

在VSCode中打开ql文件夹,然后引入刚刚建立的分析数据库codeqltest

新建example.ql,使用查询语句打印输出Hello world

CodeQL 语法&规则
Method:方法类,Method method 表示获取当前项目中的所有方法
exists(A | B):存在函数,是否存在A使B成立
1 2 3 4 5
| import java
from Method method where method.hasName("getStudent") select method.getName(), method.getDeclaringType()
|

根据输出结果可知,IndexDb和IndexLogic类中各有一个getStudent方法


谓词 - predicate
用于解决where查询条件过长导致逻辑不清晰的问题
1 2 3 4 5 6 7 8 9 10
| import java
predicate isStudent(Method method) { exists(|method.hasName("getStudent")) }
from Method method where isStudent(method) select method.getName(), method.getDeclaringType()
|
可以理解为自定义了一个函数isStudent作为查询条件

静态分析
source - 用户可控输入点
sink - 危险函数
sanitizer - 过滤函数,指在整个的漏洞链条当中,如果存在一个方法阻断了整个传递链,那么这个方法就叫sanitizer
以检测SQL注入漏洞为例
首先获取所有的source点,可以直接使用CodeQL SDK提供的RemoteFlowSource
1 2 3 4 5 6 7 8 9
| import java import semmle.code.java.dataflow.FlowSources
predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }
from DataFlow::Node src where isSource(src) select "Source: ", src
|

找到Source之后,接下来就是确定sink,这个案例中,可以通过查找query方法来判断SQL注入,可以将所有传给名为query的方法的第一个参数为sink点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import java import semmle.code.java.dataflow.FlowSources
predicate isSink(DataFlow::Node sink) { exists(Method method, MethodCall call | method.hasName("query") and call.getMethod() = method and sink.asExpr() = call.getArgument(0) ) }
from DataFlow::Node src where isSink(src) select "Sink: ", src
|

确定source和sink后,最后需要输出 source --> sink的形式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import java import semmle.code.java.dataflow.FlowSources import semmle.code.java.dataflow.DataFlow
module SQLINJ_Source_To_sink_Config implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
predicate isSink(DataFlow::Node sink) { exists(Method method, MethodCall call | method.hasName("query") and call.getMethod() = method and sink.asExpr() = call.getArgument(0) ) } }
module SQLINJ_Source_To_sink = TaintTracking::Global<SQLINJ_Source_To_sink_Config>;
from DataFlow::Node source, DataFlow::Node sink where SQLINJ_Source_To_sink::flow(source, sink) select source, "-->", sink
|

效果尚可

误报过滤
但是一般的source --> sink会不可避免地产生误报,如下图所示,虽然用户可控输入被直接拼接至SQL查询语句中,但是代码中强制要求输入参数类型为Long,无法造成SQL注入,产生误报

此时就需要设计一个sanitizer来减少误报,对传入参数的类型进行判断,不能是基本数据类型PrimitiveType,BoxedType、数字类型NumverType以及泛型数字类型如此处的List<Long>
修正后的QL查询代码如下所示,添加了isBarrier方法进行过滤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import java import semmle.code.java.dataflow.FlowSources import semmle.code.java.dataflow.DataFlow
module SQLINJ_Source_To_sink_Config implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
predicate isSink(DataFlow::Node sink) { exists(Method method, MethodCall call | method.hasName("query") and call.getMethod() = method and sink.asExpr() = call.getArgument(0) ) }
predicate isBarrier(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) } }
module SQLINJ_Source_To_sink = TaintTracking::Global<SQLINJ_Source_To_sink_Config>;
from DataFlow::Node source, DataFlow::Node sink where SQLINJ_Source_To_sink::flow(source, sink) select source, "-->", sink
|
过滤误报后成功检测出五处SQL注入漏洞
