SSRF 漏洞修复建议

SSRF 漏洞描述

服务端请求伪造 (Server-Side Request Forgery), 可以通过服务端发起请求来获取机器内部无法访问到的数据
SSRF漏洞形成的主要原因是服务端接口中包含了要请求内容的 URL 参数, 并且没有对 URL 参数进行判断

危害: 攻击者可利用该漏洞对企业内网进行大规模扫描, 了解内网结构, 并可能结合内网漏洞直接获取服务器权限

容易出现 SSRF 漏洞的场景

  • 社交分享功能:获取超链接的标题等内容进行显示
  • 图片加载/下载:例如富文本编辑器中的点击下载图片到本地;通过URL地址加载或下载图片
  • 图片/文章收藏功能:主要其会取URL地址中title以及文本的内容作为显示以求一个好的用具体验

SSRF 漏洞的常见的绕过方式

各种IP地址的进制转换

16进制 http://x7f.1:8080 -> http://127.0.0.1:8080

利用 DNS 解析

local.b22.tv 已经解析为 127.0.0.1
xip.io 特殊域名 127.0.0.1.xip.io 解析为 127.0.0.1

利用 3xx Redirection 跳转

https://b22.tv/for_test -> http://127.0.0.1/

SSRF 漏洞的修复方案

  • 解析目标 URL, 获取 scheme、host(推荐使用系统内置函数完成,避免自己使用正则提取)
  • 检查 scheme 是否为合法 (如非特殊需求请只允许 http 和 https)
  • 解析 host 获取 dns 解析后的 IP 地址
  • 检查解析后的 IP 地址是否为外网地址
  • 请求经过解析后的 IP 地址

最后一步需使用解析后的 IP 地址替换 host 请求、禁用 Redirect 跟踪

如果最后一步直接传入URL直接请求,会导致再次进行DNS解析,通过 DNS Rebinding 有概率绕过 IP 地址检查从而访问内网 IP 地址.请求需要禁用 Redirect 跟踪, 如有需要跟踪 Redirect 的需求, 需再次判断 scheme、host 解析、IP 地址.

PHP

<?php

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://b22.tv/for_test');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, CURLOPT_RESOLVE, array("b22.tv:443:104.18.36.72"));

$response = curl_exec($ch);

if (!$response) {
  die('Error: "' . curl_error($ch) . '" - Code: ' . curl_errno($ch));
}

echo 'HTTP Status Code: ' . curl_getinfo($ch, CURLINFO_HTTP_CODE) . PHP_EOL;
echo 'Response Body: ' . $response . PHP_EOL;

curl_close($ch);

Python

import requests
from requests_toolbelt.adapters import host_header_ssl

def send_request():
    s = requests.Session()
    s.mount('https://', host_header_ssl.HostHeaderSSLAdapter())
    response = s.get(
        "https://104.18.36.72/for_test",
        headers={"Host": "b22.tv"},
        allow_redirects=False
    )
    print('Response HTTP Status Code: {status_code}'.format(
        status_code=response.status_code))
    print('Response HTTP Response Body: {content}'.format(
        content=response.content))

if __name__ == "__main__":
    send_request()