这个漏洞这几天大家提得也比较多,我就有了对此做一下漏洞分析的念头。
由于这几天没怎么上Twitter和关注群聊,所以从得知消息到昨天7号四点多复现成功后,我算是比较末尾那一批,因为五点多的时候各大src 也已经开始通报漏洞了。
漏洞复现:
我7号那天是直接去官网下安装包,无脑下一步;完成后默认是 3000 端口,默认凭证 admin/admin
我这里版本是8.3.0
发送 payloads,可以看到读取内容成功:
漏洞分析:
我一开始是直接用 github 的在线 vscode 审计 Grafana 的源码,看了一会后觉得很纳闷,最后发现16小时前已经被修复了...
16小时前已修复:
可以看到 8.3.1 版本目前共有 1个更改的文件 、3 个添加 和2 个删除。改动位置为 pkg/api/plugins.go ,分别在 284、288、289 行添加了代码,将原 287、288行的代码删除。
那么我们就把 8.3.0 的源码下载到本地,并找到 pkg/api/plugins.go 开始审计
找到改动处,可以看到内容属于 getPluginAssets 这个结构体。
大家看看这个结构体第三行的路径注释,是不是与我文中的 payloads ( https://xxxxxx.xx/public/plugins/grafana-clock-panel/../../../../../../etc/passwd ) 很相似 ? 没错,这里就是我们请求该路径后,会触发的地方之一。
我们来看看这个结构体做了什么:
往下
结构体具体代码:
`// getPluginAssets returns public plugin assets (images, JS, etc.)``//``// /public/plugins/:pluginId/*``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)``}`
看到这里,我们再回看一下这个结构体的第三行注释 /public/plugins/:pluginId/*
和我们的payloads (https://xxxxxx.xx/public/plugins/grafana-clock-panel/../../../../../../etc/passwd )
代码中的pluginld,就代表我们 payloads 里的插件名grafana-clock-panel。 那么再构造 payload 就很显然了,我们找到一个默认存在或者大概率存在的 plugin 就可以触发此结构体,从而完成未授权读取任意文件。
Grafana默认存在的插件:
`alertmanager``cloud-monitoring``cloudwatch``dashboard``elasticsearch``grafana``grafana-azure-monitor-datasource``graphite``influxdb``jaeger``loki``mixed``mssql``mysql``opentsdb``postgres``prometheus``tempo``testdata``zipkin`
更新至最新版本
目前普遍的利用方式是将插件名作为字典来对目标进行爆破,比较有价值的是读取.db文件。