跳到内容

服务器端请求伪造预防速查表

简介

本速查表的目的是提供关于防范 服务器端请求伪造 (SSRF) 攻击的建议。

本速查表将侧重于防御视角,不解释如何执行此类攻击。安全研究员 Orange Tsai 的此次演讲以及此文档提供了执行此类攻击的技术。

背景

SSRF 是一种攻击向量,它滥用应用程序与内部/外部网络或机器本身进行交互。此向量的促成因素之一是对 URL 的不当处理,如下例所示:

  • 外部服务器上的图像(例如 用户输入其头像的图像 URL,供应用程序下载和使用)。
  • 自定义 WebHook(用户必须指定 Webhook 处理程序或回调 URL)。
  • 与另一个服务交互以提供特定功能的内部请求。大多数情况下,用户数据会随之发送以进行处理,如果处理不当,可能会执行特定的注入攻击。

SSRF 常见流程概述

SSRF Common Flow

备注

  • SSRF 不限于 HTTP 协议。通常,第一个请求是 HTTP,但在应用程序本身执行第二个请求的情况下,它可以使用不同的协议(例如 FTP、SMB、SMTP 等)和方案(例如 file://phar://gopher://data://dict:// 等)。
  • 如果应用程序容易受到 XML 外部实体 (XXE) 注入的影响,那么它可以被利用来执行 SSRF 攻击,请查看 XXE 速查表以了解如何防止 XXE 暴露。

案例

根据应用程序的功能和要求,SSRF 可能发生的情况有两种基本类型:

  • 应用程序只能向 已识别和受信任的应用程序 发送请求:当允许列表方法可用时的情况。
  • 应用程序可以向 任何外部 IP 地址或域名 发送请求:当允许列表方法不可用时的情况。

由于这两种情况截然不同,本速查表将分别描述它们的防御措施。

案例 1 - 应用程序只能向已识别和受信任的应用程序发送请求

有时,应用程序需要向另一个应用程序(通常位于另一个网络上)执行请求,以完成特定任务。根据业务场景,此功能需要用户输入才能工作。

示例

以一个接收并使用用户个人信息(例如名字、姓氏、出生日期等)以在内部 HR 系统中创建个人资料的 Web 应用程序为例。根据设计,该 Web 应用程序必须使用 HR 系统理解的协议进行通信以处理数据。基本上,用户无法直接访问 HR 系统,但是,如果负责接收用户信息的 Web 应用程序存在 SSRF 漏洞,用户就可以利用它访问 HR 系统。用户利用该 Web 应用程序作为访问 HR 系统的代理。

允许列表方法是可行的选择,因为由易受攻击的应用程序 (VulnerableApplication) 调用的内部应用程序在技术/业务流程中已明确标识。可以声明,所需的调用将仅在这些已识别和受信任的应用程序之间进行。

可用保护措施

应用层网络层可以采取多种保护措施。为了应用深度防御原则,这两个层都将针对此类攻击进行加固。

应用层

首先想到的一种保护级别是输入验证

基于这一点,随之而来的问题是:如何执行这种输入验证?

正如 Orange Tsai 在他的演讲中所示,根据所使用的编程语言,解析器可能被滥用。一种可能的对策是在使用输入验证时应用允许列表方法,因为大多数情况下,用户期望的信息格式是普遍已知的。

发送到内部应用程序的请求将基于以下信息:

  • 包含业务数据的字符串。
  • IP 地址(IPv4 或 IPv6)。
  • 域名。
  • URL。

注意: 在您的 Web 客户端中禁用对重定向的支持,以防止绕过此文档利用技巧 > 绕过限制 > 输入验证 > 不安全重定向部分描述的输入验证。

字符串

在 SSRF 的上下文中,可以添加验证以确保输入字符串符合预期的业务/技术格式。

如果输入数据格式简单(例如 令牌、邮政编码等),可以使用正则表达式来确保接收到的数据从安全角度来看是有效的。否则,应使用字符串对象可用的库进行验证,因为复杂格式的正则表达式难以维护且极易出错。

假定用户输入与网络无关,并包含用户的个人信息。

示例

//Regex validation for a data having a simple format
if(Pattern.matches("[a-zA-Z0-9\\s\\-]{1,50}", userInput)){
    //Continue the processing because the input data is valid
}else{
    //Stop the processing and reject the request
}
IP 地址

在 SSRF 的上下文中,可以执行两种可能的验证:

  1. 确保提供的数据是有效的 IPv4 或 IPv6 地址。
  2. 确保提供的 IP 地址属于已识别和受信任应用程序的 IP 地址之一。

第一层验证可以使用确保 IP 地址格式安全的库来应用,具体取决于所使用的技术(此处建议使用库选项,以委托 IP 地址格式的管理并利用经过实战检验的验证功能)

已对所建议的库进行了验证,以检查它们是否存在本文文章中描述的绕过方式(十六进制、八进制、双字、URL 和混合编码)的风险。

  • JAVA: 来自 Apache Commons Validator 库的 InetAddressValidator.isValid 方法。
    • 它没有暴露于使用十六进制、八进制、双字、URL 和混合编码的绕过。
  • .NET:来自 SDK 的 IPAddress.TryParse 方法。
    • 暴露于使用十六进制、八进制、双字和混合编码的绕过,但包括 URL 编码。
    • 由于此处使用了允许列表,任何绕过尝试都将在与允许的 IP 地址列表进行比较时被阻止。
  • JavaScript:库 ip-address
    • 它没有暴露于使用十六进制、八进制、双字、URL 和混合编码的绕过。
  • Ruby:来自 SDK 的 IPAddr 类。
    • 它没有暴露于使用十六进制、八进制、双字、URL 和混合编码的绕过。

使用方法/库的输出值作为 IP 地址,与允许列表进行比较。

确保传入 IP 地址的有效性后,应用第二层验证。在确定已识别和受信任应用程序的所有 IP 地址(IPv4 和 IPv6,以避免绕过)后,创建允许列表。将有效 IP 与该列表进行交叉检查,以确保其与内部应用程序的通信(区分大小写的字符串严格比较)。

域名

在尝试验证域名时,进行 DNS 解析以验证域名的存在性是显而易见的。通常,这不是一个坏主意,但它会根据用于域名解析的 DNS 服务器配置,使应用程序面临攻击风险:

  • 它可能会向外部 DNS 解析器泄露信息。
  • 攻击者可以使用它将一个合法的域名绑定到内部 IP 地址。参见此文档利用技巧 > 绕过限制 > 输入验证 > DNS 绑定部分。
  • 攻击者可以使用它向内部 DNS 解析器以及应用程序用于处理 DNS 通信的 API(SDK 或第三方)传递恶意负载,然后,可能会触发这些组件之一中的漏洞。

在 SSRF 的上下文中,有两种验证需要执行:

  1. 确保提供的数据是有效的域名。
  2. 确保提供的域名属于已识别和受信任应用程序的域名之一(此处涉及允许列表)。

类似于 IP 地址验证,第一层验证可以使用确保域名格式安全的库来应用,具体取决于所使用的技术(此处建议使用库选项,以委托域名格式的管理并利用经过实战检验的验证功能)

已对所建议的库进行了验证,以确保所建议的功能不执行任何 DNS 解析查询。

Ruby 中建议正则表达式的执行示例

domain_names = ["owasp.org","owasp-test.org","doc-test.owasp.org","doc.owasp.org",
                "<script>alert(1)</script>","<script>alert(1)</script>.owasp.org"]
domain_names.each { |domain_name|
    if ( domain_name =~ /^(((?!-))(xn--|_{1,1})?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?([a-z0-9][a-z0-9\-]{0,60}|[a-z0-9-]{1,30}\.[a-z]{2,})$/ )
        puts "[i] #{domain_name} is VALID"
    else
        puts "[!] #{domain_name} is INVALID"
    end
}
$ ruby test.rb
[i] owasp.org is VALID
[i] owasp-test.org is VALID
[i] doc-test.owasp.org is VALID
[i] doc.owasp.org is VALID
[!] <script>alert(1)</script> is INVALID
[!] <script>alert(1)</script>.owasp.org is INVALID

确保传入域名的有效性后,应用第二层验证:

  1. 构建一个包含所有已识别和受信任应用程序域名的允许列表。
  2. 验证收到的域名是否属于此允许列表的一部分(区分大小写的字符串严格比较)。

不幸的是,此处应用程序仍然容易受到此文档中提到的DNS 绑定绕过攻击。实际上,当业务代码执行时,将进行 DNS 解析。为了解决这个问题,除了对域名进行验证外,还必须采取以下措施:

  1. 确保属于您组织的域名在 DNS 解析器链中首先由您的内部 DNS 服务器解析。
  2. 监控域名允许列表,以便在任何域名解析为以下情况时进行检测:- 本地 IP 地址(IPv4 + IPv6)。- 您组织的内部 IP(预期在私有 IP 范围内),用于不属于您组织的域名。

以下 Python3 脚本可用作上述监控的起点:

# Dependencies: pip install ipaddress dnspython
import ipaddress
import dns.resolver

# Configure the allowlist to check
DOMAINS_ALLOWLIST = ["owasp.org", "labslinux"]

# Configure the DNS resolver to use for all DNS queries
DNS_RESOLVER = dns.resolver.Resolver()
DNS_RESOLVER.nameservers = ["1.1.1.1"]

def verify_dns_records(domain, records, type):
    """
    Verify if one of the DNS records resolve to a non public IP address.
    Return a boolean indicating if any error has been detected.
    """
    error_detected = False
    if records is not None:
        for record in records:
            value = record.to_text().strip()
            try:
                ip = ipaddress.ip_address(value)
                # See https://docs.pythonlang.cn/3/library/ipaddress.html#ipaddress.IPv4Address.is_global
                if not ip.is_global:
                    print("[!] DNS record type '%s' for domain name '%s' resolve to
                    a non public IP address '%s'!" % (type, domain, value))
                    error_detected = True
            except ValueError:
                error_detected = True
                print("[!] '%s' is not valid IP address!" % value)
    return error_detected

def check():
    """
    Perform the check of the allowlist of domains.
    Return a boolean indicating if any error has been detected.
    """
    error_detected = False
    for domain in DOMAINS_ALLOWLIST:
        # Get the IPs of the current domain
        # See https://en.wikipedia.org/wiki/List_of_DNS_record_types
        try:
            # A = IPv4 address record
            ip_v4_records = DNS_RESOLVER.query(domain, "A")
        except Exception as e:
            ip_v4_records = None
            print("[i] Cannot get A record for domain '%s': %s\n" % (domain,e))
        try:
            # AAAA = IPv6 address record
            ip_v6_records = DNS_RESOLVER.query(domain, "AAAA")
        except Exception as e:
            ip_v6_records = None
            print("[i] Cannot get AAAA record for domain '%s': %s\n" % (domain,e))
        # Verify the IPs obtained
        if verify_dns_records(domain, ip_v4_records, "A")
        or verify_dns_records(domain, ip_v6_records, "AAAA"):
            error_detected = True
    return error_detected

if __name__== "__main__":
    if check():
        exit(1)
    else:
        exit(0)
URL

不要接受用户提供的完整 URL,因为 URL 难以验证,而且根据所使用的技术,解析器可能被滥用,正如 Orange Tsai 的以下演讲所示。

如果确实需要网络相关信息,则只接受有效的 IP 地址或域名。

网络层

网络层安全的目标是防止易受攻击的应用程序 (VulnerableApplication) 向任意应用程序执行调用。该应用程序将只提供允许的路由,以将其网络访问限制为它应该通信的那些应用程序。

防火墙组件,无论是作为特定设备还是使用操作系统中提供的防火墙,都将在此处用于定义合法流量。

在下面的示意图中,防火墙组件被利用来限制应用程序的访问,从而限制了易受 SSRF 攻击的应用程序的影响

Case 1 for Network layer protection about flows that we want to prevent

网络隔离(参见此套实施建议)也可以利用,并且强烈建议直接在网络层面阻止非法调用

案例 2 - 应用程序可以向任何外部 IP 地址或域名发送请求

当用户可以控制指向外部资源的 URL 并且应用程序向此 URL 发送请求时(例如在WebHooks情况下),就会发生这种情况。此处无法使用允许列表,因为 IP/域名列表通常是事先未知的,并且动态变化。

在此场景中,外部指不属于内部网络的任何 IP,并且应该通过公共互联网访问。

因此,来自易受攻击的应用程序 (Vulnerable Application) 的调用:

  • 没有指向公司全球网络内部的某个 IP/域名。
  • 使用在易受攻击的应用程序 (VulnerableApplication) 和预期 IP/域名之间定义的约定,以证明该调用是合法发起的。

在应用层阻止 URL 的挑战

根据上述应用程序的业务要求,允许列表方法不是一个有效的解决方案。尽管知道阻止列表方法不是一道坚不可摧的墙,但在此场景中它是最佳解决方案。它告知应用程序什么操作是应该做的。

这就是为什么在应用层过滤 URL 很困难的原因:

  • 这意味着应用程序必须能够在代码层面检测到所提供的 IP(IPv4 + IPv6)不属于官方私有网络范围,包括localhostIPv4/v6 链路本地地址。并非每个 SDK 都为此类验证提供了内置功能,这使得处理任务留给开发人员理解其所有陷阱和可能的值,从而使其成为一项艰巨的任务。
  • 域名也一样:公司必须维护所有内部域名的列表,并提供一个集中服务,允许应用程序验证所提供的域名是否为内部域名。为此验证,应用程序可以查询内部 DNS 解析器,但此内部 DNS 解析器不得解析外部域名。

可用保护措施

以下各节将沿用以下示例中的相同假设。

应用层

案例 1 类似,假定需要IP 地址域名来创建将发送到目标应用程序 (TargetApplication) 的请求。

对于此案例,案例 1 中针对三种数据类型提出的输入数据的第一层验证将相同,但第二层验证将有所不同。实际上,这里我们必须使用阻止列表方法。

关于请求合法性证明:接收请求的目标应用程序 (TargetedApplication) 必须生成一个随机令牌(例如:20 个字符的字母数字),调用者(通过一个参数在请求体中传递,该参数的名称也由应用程序自身定义,且只允许字符集 [a-z]{1,10})应传递此令牌以执行有效请求。接收端点必须只接受 HTTP POST 请求。

验证流程(如果其中一个验证步骤失败,则请求被拒绝)

  1. 应用程序将接收目标应用程序 (TargetedApplication) 的 IP 地址或域名,并使用本中提到的库/正则表达式对输入数据执行第一层验证。
  2. 第二层验证将使用以下阻止列表方法对目标应用程序 (TargetedApplication) 的 IP 地址或域名进行:- 对于 IP 地址
    • 应用程序将验证它是否为公共 IP(参见下一段中提供的 Python 代码示例中的提示)。
    • 对于域名
      1. 应用程序将通过尝试针对仅解析内部域名的 DNS 解析器解析该域名来验证它是否为公共域名。在这里,它必须返回一个响应,表明它不知道所提供的域名,因为收到的预期值必须是公共域名。
      2. 为了防止此文档中描述的DNS 绑定攻击,应用程序将检索所提供域名背后的所有 IP 地址(获取 IPv4 + IPv6 的A + AAAA 记录),并应用前一点中描述的关于 IP 地址的相同验证。
  3. 应用程序将通过专用输入参数接收用于请求的协议,并针对允许的协议列表(HTTPHTTPS)验证其值。
  4. 应用程序将通过专用输入参数接收要传递给目标应用程序 (TargetedApplication) 的令牌参数名称,该参数只允许字符集 [a-z]{1,10}
  5. 应用程序将通过专用输入参数接收令牌本身,该参数只允许字符集 [a-zA-Z0-9]{20}
  6. 应用程序将接收并(从安全角度)验证执行有效调用所需的任何业务数据。
  7. 应用程序将仅使用已验证的信息构建 HTTP POST 请求并发送(别忘了禁用所用 Web 客户端中对重定向的支持)。
网络层

类似于以下部分

AWS 中的 IMDSv2

在云环境中,SSRF 通常用于从元数据服务(例如 AWS 实例元数据服务、Azure 实例元数据服务、GCP 元数据服务器)访问和窃取凭据和访问令牌。

IMDSv2 是 AWS 的一种额外的深度防御机制,可缓解某些 SSRF 实例。

要利用此保护,请迁移到 IMDSv2 并禁用旧的 IMDSv1。查看 AWS 文档了解更多详情。

Semgrep 规则

Semgrep 是一款用于离线静态分析的命令行工具。使用预构建或自定义规则在您的代码库中强制执行代码和安全标准。查看用于识别/调查 Java 中 SSRF 漏洞的 Semgrep SSRF 规则:https://semgrep.dev/salecharohit:owasp_java_ssrf

参考资料

SSRF 圣经的在线版本(本速查表中使用 PDF 版本)。

关于绕过 SSRF 保护的文章。

关于 SSRF 攻击的文章:第一部分第二部分第三部分

关于 IMDSv2 的文章

用于模式的工具和代码

SSRF 常见流程的 Mermaid 代码(屏幕截图用于捕获插入到此速查表中的 PNG 图像)

sequenceDiagram
    participant Attacker
    participant VulnerableApplication
    participant TargetedApplication
    Attacker->>VulnerableApplication: Crafted HTTP request
    VulnerableApplication->>TargetedApplication: Request (HTTP, FTP...)
    Note left of TargetedApplication: Use payload included<br>into the request to<br>VulnerableApplication
    TargetedApplication->>VulnerableApplication: Response
    VulnerableApplication->>Attacker: Response
    Note left of VulnerableApplication: Include response<br>from the<br>TargetedApplication

Draw.io 模式 XML 代码,用于“案例 1 网络层保护中我们希望阻止的流量”模式(屏幕截图用于捕获插入到此速查表中的 PNG 图像)。