https://www.freebuf.com/articles/web/283795.html (主要参考)

https://github.com/Firebasky/CodeqlLearn

粗糙的数据库生成

这里生成数据库的方法我用的是这个项目

https://github.com/ice-doom/codeql_compile

根据java源码构建数据库的方法

1
codeql database create "D:\google download\cc_database" --language="java" --source-root="D:\google download\micro_service_seclab-main"  --overwrite

这里我用的例子是jar包

1
2
3
python .\codeql_compile.py -a D:\codeql_compile\ezjava.jar  -d D:\codeql_compile\ezjava\BOOT-INF\lib

codeql database create D:\codeql_compile\demo-database --language="java" --source-root=D:\codeql_compile\ezjava.jar_save_1703684740 --command="run.cmd"

一共两步就成功生成了数据库

然后导入数据库

image-20231228213705367

先创建个文件夹 然后导入ql文件

image-20231228213941385

qlpack.yml

1
2
3
name: example-query
version: 0.0.0
libraryPathDependencies: codeql-java

然后写ql文件就行了

QL语言编写

基础查询

先是查询数据库中所有的类方法

这里的话是使用Method这个来进行查询

1
2
3
4
import java

from Method method
select method

image-20231228214509935

查出数据库中所有函数方法


指定查询某个方法

1
2
3
4
5
6
import java

from Method method
where method.hasName("resolveClass")
select method

image-20231228214630394

如果要把其是什么类查询出来的化 就再加上一句话

1
2
3
4
5
import java

from Method method
where method.hasName("resolveClass")
select method,method.getDeclaringType()

image-20231228214746369


查询父类中子类的某个方法

1
2
3
4
5
import java

from Method method
where method.hasName("resolveClass") and method.getDeclaringType().getASupertype().hasQualifiedName("java.io", "ObjectInputStream")
select method, method.getDeclaringType()

image-20231228215047751

这里的话是查询ObjectInputStream这个父类中子类的resolveClass方法 这里的话就会有个疑问就是 他只能查询到隔一级的子类 隔两级的子类的resolveClass方法是查不到的

查询某个类中的某个方法被谁调用

Call和Callable

Callable表示可调用的方法或构造器的集合。

Call表示调用Callable的这个过程(方法调用,构造器调用等等)

过滤 方法调用

MethodAccess

一般是先查method,与MethodAccess.getMethod() 进行比较。

1
2
3
4
5
import java

from MethodAccess call, Method method
where method.hasName("resolveClass") and method.getDeclaringType().getAnAncestor().hasQualifiedName("java.io", "ObjectInputStream") and call.getMethod() = method
select call

这个是查找resolveClass这个方法被调用情况 我这样查是能查到 但是只能查父类下面差一级的子类

谓词(可以理解为函数)

和SQL一样,where部分的查询条件如果过长,会显得很乱。CodeQL提供一种机制可以让你把很长的查询语句封装成函数。

这个函数,就叫谓词。

1
2
3
4
5
6
7
8
9
import java

predicate isStudent(Method method) {
exists(|method.hasName("getStudent"))
}

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

image-20231229145822767

语法解释

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的链路是通的,才表示当前漏洞是存在的。

image

设置source

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

我们使用的是Spring Boot框架,那么source就是http参数入口的代码参数,在下面的代码中,source就是username:

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

在下面的代码中,source就是Student user(user为Student类型,这个不受影响)。

1
2
3
4
@PostMapping(value = "/object")
public List<Student> objectParam(@RequestBody Student user) {
return indexLogic.getStudent(user.getUsername());
}

在下面的代码中,source就是Student user(user为Student类型,这个不受影响)。

1
2
3
4
@PostMapping(value = "/object")
public List<Student> objectParam(@RequestBody Student user) {
return indexLogic.getStudent(user.getUsername());
}

Source设置的代码为

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

这是SDK自带的规则,里面包含了大多常用的Source入口。我们使用的SpringBoot也包含在其中, 我们可以直接使用。

instanceof是codeql自带的语法

当然了上述语句并不是 完整可使用的语句 这是把我们等会完全的语句拿出部分进行解释讲解

设置sink

1
2
3
override predicate isSink(DataFlow::Node sink) {

}

在本案例中,我们的sink应该为query方法(Method)的调用(MethodAccess),所以我们设置Sink为:

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)
)
}

在这个语句中 call方法就是我们上文提到的 就是可以查询某个方法被谁调用了

注:以上代码使用了exists子查询语法,格式为exists(Obj obj| somthing), 上面查询的意思为:查找一个query()方法的调用点,并把它的第一个参数设置为sink (加黑的这句话就是上述代码中最后一段的解释)

在靶场系统(micro-service-seclab)中,sink就是:

1
2
3
jdbcTemplate.query(sql, ROW_MAPPER);

//提前说明一下

因为我们测试的注入漏洞,当source变量流入这个方法的时候,才会发生注入漏洞!

当然了上述语句并不是 完整可使用的语句 这是把我们等会完全的语句拿出部分进行解释讲解

Flow数据流

设置好Source和Sink,就相当于搞定了首尾,但是首尾是否能够连通才能决定是否存在漏洞!

一个受污染的变量,能够毫无阻拦的流转到危险函数,就表示存在漏洞!

这个连通工作就是CodeQL引擎本身来完成的。我们通过使用config.hasFlowPath(source, sink)方法来判断是否连通。

比如如下代码:

1
2
3
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,系统就会自动帮我们判断是否存在漏洞了。

Source和sink配合查询结果

在CodeQL中,我们使用官方提供的TaintTracking::Configuration方法定义source和sink,至于中间是否是通的,这个后面使用CodeQL提供的config.hasFlowPath(source, sink)来帮我们处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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就已经涵盖了。

最终demo.ql

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/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等信息会写入到审计报告中。

(这个不能删 如果删了话再执行 就会生成不了下图中的alerts 不能清楚的看到source和sink之间的node了)

image-20231229152906287

上述查询误报解决

image-20231229153553428

在上述跑出的链子中 跑出sink是这个东西 但是呢这个参数是long类型的 不可能存在sql注入

这说明我们的规则里,对于List,甚至List类型都会产生误报,source误把这种类型的参数涵盖了。

我们需要采取手段消除这种误报。

这个手段就是isSanitizer

img

1
2
3
4
5
6
7
8
9
10
11
12
13
isSanitizer是CodeQL的类TaintTracking::Configuration提供的净化方法。它的函数原型是:

override predicate isSanitizer(DataFlow::Node node) {}
覆盖谓词 isSanitizer(DataFlow::Node 节点) {}

在CodeQL自带的默认规则里,对当前节点是否为基础类型做了判断。

override predicate isSanitizer(DataFlow::Node node) {
覆盖谓词 isSanitizer(DataFlow::Node 节点) {
node.getType() instanceof PrimitiveType or
node.getType() instanceof BoxedType or
node.getType() instanceof NumberType
}

由于CodeQL检测SQL注入里的isSanitizer方法,只对基础类型做了判断,并没有对这种复合类型做判断,才引起了这次误报问题。

那我们只需要将这种复合类型加入到isSanitizer方法,即可消除这种误报。

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 )
}

以上代码的意思为:如果当前node节点的类型为基础类型,数字类型和泛型数字类型(比如List)时,就切断数据流,认为数据流断掉了,不会继续往下检测。
重新执行query,我们发现,刚才那条误报已经被成功消除啦。

image-20231229164818007

泛型就是指的是List 这种 ParameterizedType这个指的就是泛型 pt.getTypeArgument(0) instanceof NumberType 这个指的就是泛型的第一个参数是不是Num类型

这里执行的话是会返回True的 因为Node节点是会被匹配到的

漏报解决

这个结果的返回时不全的 有些链子没有给我们返回来 例如下例语句

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);
}

这里修复方法就是强制给其接上

image

1
2
3
4
5
6
7
isAdditionalTaintStep方法是CodeQL的类TaintTracking::Configuration提供的的方法,它的原型是:

override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {}
覆盖谓词 isAdditionalTaintStep(DataFlow::Node 节点1, DataFlow::Node 节点2) {}

它的作用是将一个可控节点
A强制传递给另外一个节点B,那么节点B也就成了可控节点。

多次测试之后,我认定是因为username.get()这一步断掉了。大概是因为Optional这种类型的使用没有在CodeQL的语法库里。

那么这里我们强制让username流转到username.get(),这样username.get()就变得可控了。这样应该就能识别出这个注入漏洞了。

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
37
38
39
40
41
42
43
44
45
46
/**
* @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

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>" )
}

class VulConfig extends TaintTracking::Configuration {
VulConfig() { this = "SqlInjectionConfig" }

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

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 )
}

override predicate isSink(DataFlow::Node sink) {
exists(Method method, MethodAccess call |
method.hasName("query")
and
call.getMethod() = method and
sink.asExpr() = call.getArgument(0)
)
}
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
isTaintedString(node1.asExpr(), node2.asExpr())
}
}


from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"

注:以上我们实现了一个isTaintedString谓词,并使用exists子查询的方式实现了强制把Optional<String> username关联Optional<String> username.get()
最终,我们的这个注入被跑了出来。

image-20231229170116821

其实这个node跟下断点调试一样 一直跟着这个username来走 很详细

CodeQL进阶查询

递归问题

递归调用可以帮助我们解决一类问题:就是我们不确定这个方法我们需要调用多少次才能得到我们的结果,这个时候我们就可以用递归调用。

CodeQL里面的递归调用语法是:在谓词方法的后面跟*或者+,来表示调用0次以上和1次以上(和正则类似),0次会打印自己。
我们举一个例子:

在Java语言里,我们可以使用class嵌套class,多个内嵌class的时候,我们需要知道最外层的class是什么怎么办?
比如如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class StudentService {

class innerOne {
public innerOne(){}

class innerTwo {
public innerTwo(){}

public String Nihao() {
return "Nihao";
}
}
public String Hi(){
return "hello";
}
}

}

按照非递归的方法

1
2
3
4
5
import java

from Class classes
where classes.getName().toString() = "innerTwo"
select classes.getEnclosingType().getEnclosingType() // getEnclosingtype获取作用域

使用递归的方法

我们在调用方法后面加*(从本身开始调用)或者+(从上一级开始调用),来解决此问题。

(就是如开头所说 在谓词方法后面加上就行)

1
2
3
from Class classes
where classes.getName().toString() = "innerTwo"
select classes.getEnclosingType+() // 获取作用域

我们也可以自己封装方法来递归调用。

1
2
3
4
5
6
7
8
9
import java

RefType demo(Class classes) {
result = classes.getEnclosingType()
}

from Class classes
where classes.getName().toString() = "innerTwo"
select demo*(classes) // 获取作用域

强制类型转换问题

1
2
3
4
import java

from Parameter param
select param, param.getType()

getType()目的就是获取项目中所有的参数的type信息。

以上代码的含义是打印所有方法参数的名称和类型。

image-20231229173522680

如果我们进行强制类型转化呢 (简单的说就是把不符合的类型给过滤掉 留下符合的类型)

1
2
3
4
import java

from Parameter param
select param, param.getType().(RefType)

强制转换成RefType,意思就是从前面的结果当中过滤出RefType类型的参数。RefType是什么?引用类型,说白了就是去掉int等基础类型之后的数据。

image-20231229173732428

相比之前确实变少了

1
2
3
4
import java

from Parameter param
select param, param.getType().(IntegralType)

这是保留所有数字型的参数

image-20231229173907492