CodeQL第一步-Grafana任意文件读

概述

介绍及搭建教程https://www.freebuf.com/sectool/269924.html

Golang依赖库https://github.com/github/codeql-go

注意:该依赖库应该独立为一个workspace而不是放进CLI项目中

内置资源

  1. ql/examples/snippets目录提供了一些基础的ql文件,通过阅读与仿写可以了解一些基本的使用,具体有哪些就自己去看吧,反正也不多
  2. lib/security目录下定义了一些常见漏洞的检测规则,通过import semmle.go.security.[module]导入ql文件配合污点分析使用

基础使用

寻找指定方法和调用点

import go
// 方法一
from Function func
where func.hasQualifiedName("os", "Open")
select func, func.getAReference()

// 方法二
	// call.getTarget()相当于Function
	// call.getExpr()、call.asExpr()和call.getTarget().getAReference()同理
from DataFlow::CallNode call
where call.getTarget().hasQualifiedName("os", "Open")
select call.getTarget(), call.getExpr(), call.getTarget().getAReference()

// 方法三
from Function func, DataFlow::CallNode call
where
 func.hasQualifiedName("os", "Open") and
 call = func.getACall()
select func, call

获得方法参数

// 方法一 -- 获得函数定义时的参数(receiver和param)
from Function func
where func.getName() = "getPluginAssets"
select func.getAParameter()

// 方法二 -- 获得函数调用时的参数
from DataFlow::CallNode call
// from CallExpr call也可以
where call.getTarget().hasQualifiedName("os", "Open")
select call.getAnArgument()

查询指定参数的方法

注:查询结果排序区分大小写,按ASCII排序

1
2
3
4
5
6
7
// 通过参数类型查询参数
from Function func, Parameter param, PointerType ptype
where
ptype.getBaseType().hasQualifiedName("github.com/grafana/grafana/pkg/models", "ReqContext") and
param.getType() = ptype and
func.getAParameter() = param
select func

其他

getExpr和asExpr区别

从注释上看:

getExpr:Gets the underlying expression this node corresponds to.(获取此节点对应的基础表达式) asExpr:Gets the expression corresponding to this node, if any.(获取与此节点对应的表达式(如果有))

两个方法分别是获得基础表达式和节点对应表达式,应该是有区别的,但是在函数体中两者却是相同的:

class ExprNode extends InstructionNode {
	 override IR::EvalInstruction insn;
	 Expr expr;
	 ExprNode() { expr = insn.getExpr() }
	 override Expr asExpr() { result = expr }
/** Gets the underlying expression this node corresponds to. */
	 Expr getExpr() { result = expr }
}

唯一的区别就是asExpr是通过覆写基类Node::asExpr方法来实现的,所以到底有什么区别我也不知道(有营销号那味儿了2333

不管了,目前看到的文章中都是用asExpr,那就暂且用这个吧。等等党终会获得胜利(bushi

其他的使用方法见http://f4bb1t.com/post/2020/12/15/codeql-for-golang-practise2/(or直接读examples)

污点分析

概述

文章https://www.modb.pro/db/139793直接借助了官方提供的SqlInjection模块,代码在lib/semmle/go/security/SqlInjection目录下,其内部定义了Configuration类提供source、sink检测以及节点是否可被污染的状态变化,并通过定义isAdditionalTaintStep方法扩展了NoSQL的污染方式

基于此我们可以了解一个污点分析模块基本的写法:

  • 定义一个继承TaintTracking::Configuration的类
  • source、sink判断/isSource、isSink
  • 可选:节点状态判断/isSanitizer、isSanitizerGuard
  • 可选:是否存在其他的污染方式/isAdditionalTaintStep,即中断的数据流能否有方式重新连接

实践/Grafana任意文件读取

数据库懒得编译,就直接用现成的:https://github.com/safe6Sec/codeql-grafana

漏洞成因

  1. plugins api权限为public,任何人都可以查看
  2. 传入参数被直接拼接进了插件路径
  3. 未经处理的路径被Open函数处理,导致任意文件读取

规则编写: sink很明显,就是Open函数:

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

source可以锁定为 router 参数,那么应该与Context类型有关,也就是*models.ReqContext类,但是写不出来emmm...那就照着文章中查询 router 注册的思路走。

定义source如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class GfSource extends DataFlow::Node {
    GfSource() {
        exists(
            Function func, DataFlow::CallNode call |
            call.getTarget().hasQualifiedName(
                "github.com/grafana/grafana/pkg/api/routing.RouteRegister",
                ["Get", "Post", "Delete", "Put", "Patch", "Any"]
            ) and
            func.getAParameter() = this.asParameter() // source设置为参数
        )
    }
}

如果单纯的这样写会查询出大量的source,因此需要添加限制条件来减少误报,通过观察源码发现router注册参数大部分都是string, selectorExprstring, CallExpr(selectorExpr)格式,因此可以将参数格式限制为SelectorExpr,可以看到文中添加了大量的SelectorExpr过滤条件:

selectorExpr为符合f.x格式的表达式

1
2
3
4
...
(call.getAnArgument() =se or call.getAnArgument().getAChildExpr()=se) and
fun.getAReference() = se.getSelector() and
...

因此完整的source为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class GfSource extends DataFlow::Node {
    GfSource() {
        exists(
            Function func, CallExpr call, SelectorExpr se |
            call.getTarget().hasQualifiedName(
                "github.com/grafana/grafana/pkg/api/routing.RouteRegister",
                ["Get", "Post", "Delete", "Put", "Patch", "Any"]
            ) and
            (call.getAnArgument() = se or call.getAnArgument().getAChildExpr() = se) and
            func.getAReference() = se.getSelector() and
            func.getAParameter() = this.asParameter() // source设置为参数
        )
    }
}

Query发现没有结果,因为没有Context传递参数导致在获取路径阶段就截断了,因此需要一些处理来连接数据流:

我一开始的思路是仿照SqlInjection模块的isAdditionalTaintStep方法编写规则,但是那样查询出来的结果太多了,稍微加点限制就查询无果,只好放弃挣扎了

分析一下:

  1. 限制函数为Params
  2. 函数可被污染就说明参数可控,那么就让pred节点作为参数
  3. SimpleAssignStmt结构表示一个赋值表达式,如a+=b,Rhs表示等号右边,通过查看源码可知Params函数调用几乎都是在等号右边,因此可以通过该结构减少误报
  4. 最后将输出节点连接到赋值表达式
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
    exists(
        CallExpr call, SimpleAssignStmt sas |
        call.getTarget().getName() = "Params" and
        call.getAnArgument() = pred.asExpr() and
        sas.getRhs().getAChild() = call.getParent*().getAChild() and
        // 使用getParent*()是因为等号右边不止有光秃秃的Params方法调用,如漏洞点就存在Jion函数拼接操作,需要通过传递闭包getParent*()来获取完整表达式
        // 使用getAChild()则是要获取Params的方法调用,不过测试发现用不用效果差不多,所以也不懂为什么还要加这个
        sas.getRhs() = succ.asExpr()
    )
}

最后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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import go
import DataFlow::PathGraph

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

class GfSource extends DataFlow::Node {
    GfSource() {
        exists(
            Function func, CallExpr call, SelectorExpr se |
            call.getTarget().hasQualifiedName(
                "github.com/grafana/grafana/pkg/api/routing.RouteRegister",
                ["Get", "Post", "Delete", "Put", "Patch", "Any"]
            ) and
            (call.getAnArgument() = se or call.getAnArgument().getAChildExpr() = se) and
            func.getAReference() = se.getSelector() and
            func.getAParameter() = this.asParameter() // source设置为参数
        )
    }
}

class GfConfig extends TaintTracking::Configuration {
    GfConfig() { this = "Grafana file upload" }

    override predicate isSource(DataFlow::Node source) { source instanceof GfSource }
    override predicate isSink(DataFlow::Node sink) { sink instanceof GfSink }
    override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
        exists(
            CallExpr call, SimpleAssignStmt sas |
            call.getTarget().getName() = "Params" and
            call.getAnArgument() = pred.asExpr() and
            // sas.getRhs().getAChild() = call.getParent*().getAChild() and
            sas.getRhs() = call.getParent*() and
            sas.getRhs() = succ.asExpr()
        )
    }
}

from GfConfig gfc, DataFlow::PathNode sink, DataFlow::PathNode source
where gfc.hasFlowPath(source, sink)
select source.getNode(), source, sink, "test"

Reference

https://www.modb.pro/db/139793

https://cloud.tencent.com/developer/article/1750796

https://tttang.com/archive/1353/

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

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

https://kiprey.github.io/2020/12/CodeQL-setup/