参考文章:https://www.freebuf.com/articles/web/283795.html

什么是CodeQL

简单来说,CodeQL就是一个静态分析(SAST)工具,可以在白盒场景通过编写QL制定的规则,自动化的扫描代码。

环境搭建

CodeQL分引擎和SDK两部分,引擎部分不开源,主要负责解析规则。SDK是开源的,包含很多漏洞规则,也可以自己写漏洞规则进行使用。

引擎部分:https://github.com/github/codeql-cli-binaries/releases

SDK部分:https://github.com/github/codeql

引擎部分需要配置一下环境变量

image-20230331162442920

SDK部分直接拉源代码就可以了

接下来拉一个项目,尝试一下CodeQL

这里我拉了这个Java靶场进行测试,拉下来后需要配一下数据库,确保项目可以正常运行。

接下来安装vscodecodeql插件,配置codeql所在的目录

image-20230331162918241

java/ql/examples 目录下创建demo.ql,内容为select "Hello World

,并且右键选择CodeQL: Run Query

image-20230331163230009

到这里环境搭建的工作就完成了。

写起来比较简略,实际上还是踩了不少坑的,不过毕竟用Linux物理机,平常遇到奇奇怪怪的问题太多了,用搜索引擎结合一些文章就解决了

规则编写

ql的语法和sql的语法有一些相似的地方

由于CodeQL实际的查询是对AST的查询,因此QL的类库是与AST对应的。

可以看一下AST的样子

image-20230401112706932

QL语法

1
2
3
from [datatype] var
where condition(var = something)
select var

example:

1
2
3
4
5
import java // 引入CodeQL类库

from int i // 表示所有int类型的数据
where i = 1 // 表示条件:当i等于1时
select i // 输出i

image-20230401113356211

类库

Method:方法类,Method method表示获取当前项目中所有的方法

MethodAccess:方法调用类,MethodAccess call表示获取当前项目当中的所有方法调用

Parameter:参数类,Parameter表示获取当前项目当中所有的参数

example:

1
2
3
4
5
import java

from Method method // 表示所有方法
where method.hasName("getStudent") // 表示条件:方法名为getStudent
select method.getName(), method.getDeclaringType() // 输出方法的名称,和方法所属的类名

image-20230401113424756

谓词

where部分过长时,可以用谓词这个语法,把很长的查询语句封装成函数。

predicate 关键词用于声明谓词

exists 子查询,它根据内部的子查询返回true or false,来决定筛选出哪些数据。

利用上面这两个语法,我们可以把判断方法名称是否为getStudentwhere部分,封装成函数。这个函数就被称为谓词

1
2
3
4
5
6
7
8
9
import java

predicate isStudent(Method method){
exists(|method.hasName("getStudent"))
} // |操作符返回查询结果的数量,大于0则true

from Method method
where isStudent(method)
select method.getName(), method.getDeclaringType().getName()

Source、Sink

SAST的理念中通常会提到这个三元组(source,sink,sanitizer)

source是指漏洞污染链条的输入点。比如获取http请求的参数部分,就是非常明显的Source。

sink是指漏洞污染链条的执行点,比如SQL注入漏洞,最终执行SQL语句的函数就是sink(这个函数可能叫query或者exeSql,或者其它)。

sanitizer又叫净化函数,是指在整个的漏洞链条当中,如果存在一个方法阻断了整个传递链,那么这个方法就叫sanitizer。

如何定义source

source,在我们这个java靶场中,具体来看就是后端接口的参数

1
2
3
4
@RequestMapping(value = "/one")
public List<Student> one(@RequestParam(value = "username") String username) {
return indexLogic.getStudent(username);
}

例如这段代码,source就是username参数

对于source的检测,我们可以用CodeQLSDK提供的检测规则

1
override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }

其中,DataFlow::Node表示一个数据流节点,可以是数据流源、汇或传输节点。RemoteFlowSource是一个表示远程数据流源的CodeQL类。

通俗的说,这里定义了一个叫isSource的谓词,来判断传入的这个节点是不是远程数据流源(RemoteFlowSource)。

如何定义sink

这里我们以找sql注入的漏洞为例,sink就应该是qurey方法

1
2
3
4
5
6
7
8
override predicate isSink(DataFlow::Node sink) {
exists(Method method, MethodAccess call |
method.hasName("query")
and
call.getMethod() = method and
sink.asExpr() = call.getArgument(0)
)
}

这里我们声明谓词isSink,通过exists来判断名为query的方法,并且设置第一个参数为sink

Data Flow

SourceSink的数据流是否能够走通决定了是否有可能存在漏洞,可以用CodeQL的语法config.hasFlowPath(source, sink)来判断。

尝试编写Demo检测SQL注入

前面分别讲了SourceSinkData Flow的定义方法,还需要一些语法将他们串起来,才是一个完成的demo。

sourcesink的定义使用到的方法,需要继承自TaintTracking::Configuration类。

完整Demo:

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
/**
* @id java/examples/demo
* @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"

image-20230401135524256

非常的有意思,不过这里仍然存在问题,就是误报问题。

image-20230401135912897

解决误报问题

这里也就是前面提到的三元组中,sanitizer的问题。

CodeQL默认规则中,没有对List<Long>这样的复合类型做判断,因此需要手动写一个isSantizer的谓词做判断,来解决误报的问题。

这段的意思是,如果当前node节点为基础类型、数字类型、泛型数字类型时,就切断数据流。

1
2
3
4
5
6
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 )
}

解决漏报问题

对于靶场中这段代码,没有捕获到。

1
2
3
4
5
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的默认规则,没有对Optional这种类型做判断,这里可以选择手动添加对于Optional这种类型的检测。

1
2
3
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>" )
}

image-20230401145345909

文章主要是本人的学习笔记,内容大多数都是对其他文章的参考,还是建议看一手文章进行学习

最后的ql文件 https://gist.github.com/ek1ng/f1c10a42a07b467a3989b422137b265a