前言

代码审计总是离不开一些神器,笔者常用 Codeql 这款工具辅助挖洞。当我每写一个规则都需要对其它项目手动运行检查一遍,效率很低,再加上 lgtm 的关闭,此项目诞生了 — 弈(Yi)

CVE-2021-43798

这里以 Graana 的任意文件读取漏洞举例说明使用方法(初学 Codeql,如有错误之处,轻点喷)

该漏洞版本为 8.0.0 - 8.3.0 , 修复版本为 8.3.1, 8.2.7, 8.1.8, 8.0.7

1
2
git clone https://github.com/grafana/grafana
git checkout v8.2.6

这个漏洞发生在 /public/plugins/:pluginId/* api 中,当输入的 pluginId存在时,会匹配*内容,使用filepath.Clean清理路径中的多余字符后,直接拼接到pluginFilePath,然后使用os.open(pluginFilePath)打开该文件,最终回显到页面。而且plugins api权限为public,是未授权的,任何人都可以查看。

image-20221212154042195

fmt.Println(filepath.Clean(“./…/../../../../../../etc/passwd”))

输出:../../../../../etc/passwd 只清除了前面的./…/../

Codeql分析

生成 codeql 数据库

1
codeql database create /Users/yhy/CodeQL/database/go/grafana/v8.2.6 -s ./ --language=go

sink

os.open(),这个显而易见

1
2
3
4
5
6
7
8
9
10
11
12
import go
import DataFlow::PathGraph

class Sink extends DataFlow::Node {
Sink() {
exists(
DataFlow::CallNode call |
call.getTarget().hasQualifiedName("os", "Open") |
call.getArgument(0) = this // 标记 sink 为 os.Open 第一个参数
)
}
}

image-20221212161954374

source

我将 Source 点定在了macaron.Params()函数

网上的文章都是以github.com/grafana/grafana/pkg/api/routing.RouteRegister作为起始点,这就导致一个问题,写完的规则只对grafana项目起作用,不通用。

仔细研究会发现,RouteRegister的实现是以gopkg.in/macaron.v1框架为基础的,但是官方的go/ql/lib/semmle/go/frameworks/Macaron.qll,只是实现了一个重定向相关的检测规则,emmm,只能自己动手写了。

写着写着忽然发现,怎么也获取不到macaron.Params, 去看 macaron 源码才发现,这个函数就根本没有,是Grafana自己实现的。我们来分析一下这个 Params 函数

image-20221212214825612

其实就是获取net/httpRequest.Context的值,而参数r,又是通过pkg/macaron/context.go

image-20221212215137241

也就是 macaron 框架中的Context结构体中的Req成员,这个Req就是我们要找的Source点。

修改go/ql/lib/semmle/go/frameworks/Macaron.qll文件,加入如下代码

1
2
3
4
5
6
7
 private class UserControlledRequestField extends UntrustedFlowSource::Range, DataFlow::FieldReadNode {
UserControlledRequestField() {
exists(string fieldName | this.getField().hasQualifiedName("gopkg.in/macaron.v1", "Context", fieldName) |
fieldName = "Req"
)
}
}

单独执行,可以找到污染点

image-20221212220032453

直接将 sink、source 拼接跑,并没有出结果,因此需要一些处理来连接数据流

isAdditionalTaintStep

这里 tyskill 师傅说的很详细,引用一下

  1. 限制函数为Params

  2. 函数可被污染就说明参数可控,那么就让pred节点作为参数

  3. SimpleAssignStmt结构表示一个赋值表达式,如a+=b,Rhs表示等号右边,通过查看源码可知Params函数调用几乎都是在等号右边,因此可以通过该结构减少误报

  4. 最后将输出节点连接到赋值表达式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
    exists(
    CallExpr call, SimpleAssignStmt sas |
    // call.getTarget().getName() = "Params" and
    // 限制为 Params 函数会产生局限性,去除
    call.getAnArgument() = pred.asExpr() and
    sas.getRhs().getAChild() = call.getParent*().getAChild() and
    // 使用getParent*()是因为等号右边不止有光秃秃的Params方法调用,如漏洞点就存在Jion函数拼接操作,需要通过传递闭包getParent*()来获取完整表达式
    // 使用getAChild()则是要获取Params的方法调用,不过测试发现用不用效果差不多,所以也不懂为什么还要加这个
    sas.getRhs() = succ.asExpr()
    )
    }

    运行,成功发现该漏洞

image-20221212221508877

加入工具监控、扫描

因为项目中调用 Codeql 将扫描结果保存为文件,这里需在文件头添加一些描述,完整代码如下

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
47
48
/**
* @name read file
* @description read file
* @kind path-problem
* @problem.severity error
* @security-severity 6.1
* @sub-severity high
* @id yhy0/read-file
* @tags security
* @precision high
*/
import go
import DataFlow::PathGraph

class ReadFileSink extends DataFlow::Node {
ReadFileSink() {
exists(
DataFlow::CallNode call |
call.getTarget().hasQualifiedName("os", "Open") |
call.getArgument(0) = this // 标记 sink 为 os.Open 第一个参数
)
}
}

class ReadFileConfig extends TaintTracking::Configuration {
ReadFileConfig() { this = "read file" }
// 这里的 source 实现 UntrustedFlowSource ,方便其他框架通用, 对于Grafana ,我们已经修改了go/ql/lib/semmle/go/frameworks/Macaron.qll文件
override predicate isSource(DataFlow::Node source) { source instanceof UntrustedFlowSource }
override predicate isSink(DataFlow::Node sink) { sink instanceof ReadFileSink }
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(
CallExpr call, SimpleAssignStmt sas |
// call.getTarget().getName() = "Params" and
// 限制为 Params 函数会产生局限性,去除
call.getAnArgument() = pred.asExpr() and
sas.getRhs().getAChild() = call.getParent*().getAChild() and
// 使用getParent*()是因为等号右边不止有光秃秃的Params方法调用,如漏洞点就存在Jion函数拼接操作,需要通过传递闭包getParent*()来获取完整表达式
// 使用getAChild()则是要获取Params的方法调用,不过测试发现用不用效果差不多,所以也不懂为什么还要加这个
sas.getRhs() = succ.asExpr()
)
}
}

from ReadFileConfig rfc, DataFlow::PathNode sink, DataFlow::PathNode source
where rfc.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "read file find on $@.", source.getNode(), "user-provided value"


将此 ql 文件路径加入配置文件config.yaml中, - go/ql/src/myRules/ReadFile.ql ,之后程序会自动对监控的项目运行此条规则,等待捡洞即可(笔者已经捡到了^_^)

效果图

image-20221213143603327

image-20221215162315622

项目的具体介绍请看 https://github.com/ZhuriLab/Yi

参考

https://tyskill.github.io/posts/codeql-grafana/#%E5%AE%9E%E8%B7%B5grafana%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E8%AF%BB%E5%8F%96

https://xz.aliyun.com/t/10648

https://codeql.github.com/