什么是 SSRF
大家使用的服务中或多或少是不是都有以下的功能:
- 通过 URL 地址分享内容
- 通过 URL 地址把原地址的网页内容调优使其适合手机屏幕浏览,即所谓的转码功能
- 通过 URL 地址翻译对应文本的内容,即类似 Google 的翻译网页功能
- 通过 URL 地址加载或下载图片,即类似图片抓取功能
- 以及图片、文章抓取收藏功能
简单的来说就是通过 URL 抓取其它服务器上数据然后做对应的操作的功能。以 ThinkJS 代码为例,我们的实现方法大概如下:
const request = require('request-promise-native');
module.exports = class extends think.Controller {
async indexAction() {
const { url } = this.get();
const ret = await request.get(url);
// 这里是处理抓取数据的逻辑
// ...
this.ctx.body = ret;
}
}
本来是个不错的功能,但是当用户输入一个服务器可访问的内网地址,这个情况下它就会把内网的内容抓取出来展现给外网的用户。大多数公司会在内网中放置一些与公司相关的资料和关键数据,如果应用程序对用户提供的URL和远端服务器返回的信息没有进行合适的验证和过滤,就可能存在这种服务端请求伪造的缺陷,即 Server-Side Request Forgery,简称 SSRF。
SSRF 的危害
简单来说如果你的这个功能存在 SSRF 漏洞的话,相当于在攻击者和内网之间牵了根线,透过该通能攻击者可以间接访问到内网。攻击者可以利用 SSRF 实现的攻击主要有 5 种:
- 可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的 Banner 信息
- 攻击运行在内网或本地的应用程序(比如溢出)
- 对内网 Web 应用进行指纹识别,通过访问默认文件实现
- 攻击内外网的 Web 应用,主要是使用 GET 参数就可以实现的攻击
- 利用 file 协议读取本地文件
其中最后一条的实现方式是用户输入 file://
本地文件协议地址,如果不做判断,程序很可能就会把本地文件读取出来返回给用户,例如 file:///etc/password
服务器系统密码。
防御方法
首先我们需要禁用掉不需要的协议,仅允许 HTTP(s) 请求,防止最后一条使用 file://
等其它协议引起的问题,然后我们需要对输出内容进行判断,例如我应该输出一张图片,如果抓取返回回来的是一段文本我们就不应该返回。以及如果抓取远端地址导致报错返回的情况,我们需要统一处理返回给用户的内容,而不是直接将远端服务器的内容返回给用户,这样让攻击者了解到了更多远端服务器的信息。
除了输出内容的处理,我们还要对输入地址进行限制,过滤内网 IP,限制访问内网行为。以之前的示例代码为例,正常我们会增加如下处理:
const ip = require('ip');
const dns = require('dns');
const { parse } = require('url');
const lookupAsync = think.promisify(dns.lookup, dns);
module.exports = class extends think.Logic {
async indexAction() {
const { url } = this.get();
const { protocol, hostname } = parse(url);
// 判断协议
if( !/https?:/i.test(protocol) ) {
return this.fail();
}
// 判断内网IP
cost host = await lookupAsync(hostname);
if( ip.isPrivate(host) ) {
return this.fail();
}
}
}
短链接绕过
大部分情况下这样处理是没有问题的,不过攻击者可不是一般人。这里存在一个两个可以绕过的方式,首先是短链接,短链接是先到短链接服务的地址之后再302跳转到真实服务器上,如果攻击者对内网地址进行短链处理之后以上代码会判断短链服务的 IP 为合法 IP 而通过校验。
针对这种绕过方式,我们有两种方法来阻止:
- 直接根据请求返回的响应头中的 HOST 来做内网 IP 判断
- 由于跳转后的地址也还是需要 DNS 解析的,所以只要在每次域名请求 DNS 解析处都做内网 IP 判断的逻辑即可
DNS 重新绑定绕过
另外一种绕过方式是利用 DNS 重绑定攻击。
DNS如何重新绑定的工作
攻击者注册一个域名(如attacker.com),并在攻击者控制下将其代理给DNS服务器。 服务器配置为很短响应时间的TTL记录,防止响应被缓存。 当受害者浏览到恶意域时,攻击者的DNS服务器首先用托管恶意客户端代码的服务器的IP地址作出响应。 例如,他们可以将受害者的浏览器指向包含旨在在受害者计算机上执行的恶意JavaScript或Flash脚本的网站。
恶意客户端代码会对原始域名(例如attacker.com)进行额外访问。 这些都是由同源政策所允许的。 但是,当受害者的浏览器运行该脚本时,它会为该域创建一个新的DNS请求,并且攻击者会使用新的IP地址进行回复。 例如,他们可以使用内部IP地址或互联网上某个目标的IP地址进行回复。
via: 《DNS 重新绑定攻击》
简单来说就是利用 DNS 服务器来使得每次解析返回不同的 IP,当在校验 IP 的时候 DNS 解析返回合法的值,等后续重新请求内容的时候 DNS 解析返回内网 IP。这种利用了多次 DNS 解析的攻击方式就是 DNS 重新绑定攻击。
由于 DNS 重新绑定攻击是利用了多次解析,所以我们最好将校验和抓取两次 DNS 解析合并成一次,这里我们也有两种方法来阻止:
- 将第一次 DNS 解析得到的 IP 直接用于第二次请求的 DNS 解析,去除第二次解析的问题
- 在抓取请求发起的时候直接判断解析的 IP,如果不符合的话直接拒绝连接
针对以上解决方法,有开发者直接封装了 ssrf-agent 模块,使用的时候只要将其传入即可实现一次解析,多次判断的功能,下面是简单的使用示例:
const ssrfAgent = require('ssrf-agent');
const request = require('request-promise-native');
module.exports = class extends think.Controller {
async indexAction() {
const { url } = this.get();
try {
const ret = await request(url, {agent: ssrfAgent(url)});
this.ctx.body = ret;
} catch(e) {
return this.fail();
}
}
}
后记
SSRF 可以说是经久不衰的漏洞攻击了,早些年百度、人人、360搜索等都有过相应的案例。一般以下场景会可能存在 SSRF 问题,我们需要多加注意:
- 能够对外发起网络请求的地方,就可能存在 SSRF 漏洞
- 从远程服务器请求资源(Upload from URL,Import & Export RSS Feed)
- 数据库内置功能(Oracle、MongoDB、MSSQL、Postgres、CouchDB)
- Webmail 收取其他邮箱邮件(POP3、IMAP、SMTP)
- 文件处理、编码处理、属性信息处理(ffmpeg、ImageMagic、DOCX、PDF、XML)
参考资料: