当前位置: 首页 > article >正文

SSRF漏洞

什么是 SSRF(服务器端请求伪造)?

定义:

服务器端请求伪造(Server-Side Request Forgery, SSRF)是一种由攻击者控制服务器发送恶意请求的安全漏洞。攻击者可以利用 SSRF 在受害服务器内部发起网络请求,进而访问未授权的资源或服务。
在这里插入图片描述

漏洞产生原因

  1. 应用程序允许用户输入外部 URL 或 IP 地址,然后由服务器来处理或访问这些地址。
  2. 应用程序未对用户输入的 URL 进行严格验证和过滤。
  3. 服务器通常具有比外部用户更高的权限,攻击者可以通过伪造请求利用服务器与内部网络或其他受保护的系统通信。

漏洞产生的危害

常见的 SSRF 攻击目标

  1. 内网探测:攻击者利用服务器访问企业内网的资源,探测内网服务,如数据库、API等。
  2. 攻击其他服务:攻击者可以通过 SSRF 向目标服务发送恶意请求,执行命令或攻击其他系统。
  3. 滥用云服务:攻击者可利用 SSRF 滥用云服务的元数据 API,窃取敏感信息,如 AWS EC2 实例中的 IAM 凭证。

实际案例

  1. Capital One 数据泄露(2019年)

    背景: Capital One 是美国一家大型金融机构。2019 年发生了一起大规模的数据泄露事件,攻击者利用 SSRF 漏洞获取了该公司托管在 AWS S3 服务中的敏感数据。

    漏洞详情: 攻击者通过 SSRF 漏洞访问了 AWS EC2 实例元数据服务,从中获取到了 IAM 角色的临时访问凭证。这些凭证允许攻击者访问 S3 存储桶,从而导致了 1 亿多
    美国客户和 600 万加拿大客户的个人信息泄露。

    损失: Capital One 因此面临数千万美元的罚款,并需要处理大量的法律诉讼及声誉损失。

  2. Jenkins SSRF 漏洞(2017年)

    背景: Jenkins 是一个流行的开源自动化服务器,常用于 CI/CD(持续集成和持续部署)。在 2017 年,Jenkins 被发现存在一个 SSRF 漏洞,影响了多个版本。

    漏洞详情: 攻击者可以利用 Jenkins 中的 SSRF 漏洞发送恶意请求,访问内部资源,甚至是其他内网服务。这个漏洞影响了多个企业使用 Jenkins 的环境。

    影响: Jenkins 广泛应用于企业开发环境中,攻击者可以利用此漏洞获取内网中其他系统的访问权限,给大量企业的网络安全带来潜在的威胁。

SSRF 漏洞的破坏力巨大,通过 SSRF 攻击,攻击者可以访问内部网络或云元数据,进而窃取敏感数据,甚至控制关键系统。

如何防御 SSRF 漏洞?

输入校验与过滤:

通常而言,根据当前 HTTP 接口的具体功能,可以分为下面的两个场景:

  1. 应用服务所接收的 url 是固定的域名列表或者域名范围是可控的(已知的多个二级域名/多级域名),就应该创建白名单来校验域名。

    • 严格验证输入:应限制用户输入的 URL,仅允许访问受信任的外部地址。可以使用白名单方式,限制 URL 必须符合特定的正则表达式或域名。
    • 禁止私有地址访问:使用 IP 黑名单,阻止私有 IP 地址段的访问,如 127.0.0.1、10.x.x.x、192.168.x.x、172.16.x.x等。
  2. 如果接收的url的域名是不可控的(未知的),则可以考虑用一个沙箱环境来进行数据请求,实现与内网的分离。

代码示例

包含SSRF漏洞的代码(Java)

package com.example.ssrf_demo;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.stream.Stream;

@RestController
public class SSRF {
    // 未对用户输入的url做任何限制,用户可以输入任意的url
    // 例如,攻击者可以访问内网域名:http://oa.in.example.com
    @GetMapping("/ssrf1")
    public String ssrf1(@RequestParam(required = true) String url) throws IOException {
        try (CloseableHttpClient client = HttpClients.createDefault()) {
            HttpGet request = new HttpGet(url);
            HttpResponse response = client.execute(request);
            return EntityUtils.toString(response.getEntity());
        }
    }

    // 有过滤,但是只限制了URL中必须包含example.com
    // 假设内网域名是 oa.in.example.com,包含了example.com,那么攻击者依旧可以访问内网。
    @GetMapping("/ssrf2")
    public ResponseEntity<String> ssrf2(@RequestParam(required = true) String url) throws IOException {

        if (!url.contains("example.com")) {
            return new ResponseEntity<>("Url Not Allow", HttpStatus.BAD_REQUEST);
        }

        try (CloseableHttpClient client = HttpClients.createDefault()) {
            HttpGet request = new HttpGet(url);
            HttpResponse response = client.execute(request);
            return new ResponseEntity<>(EntityUtils.toString(response.getEntity()), HttpStatus.OK);
        }
    }

    // 通过黑名单和白名单组合的方式来限制
    // 攻击者可以通过域名解析不区分大小写来绕过,例如:oa.IN.example.com
    @GetMapping("/ssrf3")
    public ResponseEntity<String> ssrf3(@RequestParam(required = true) String url) throws IOException {

        if (url.contains("in.example.com")) {
            return new ResponseEntity<>("Url Not Allow", HttpStatus.BAD_REQUEST);
        }

        if (!url.contains("example.com")) {
            return new ResponseEntity<>("Url Not Allow", HttpStatus.BAD_REQUEST);
        }

        try (CloseableHttpClient client = HttpClients.createDefault()) {
            HttpGet request = new HttpGet(url);
            HttpResponse response = client.execute(request);
            return new ResponseEntity<>(EntityUtils.toString(response.getEntity()), HttpStatus.OK);
        }
    }

    // 黑名单+白名单,同时限制大小写
    // 攻击者可以通过 URL 跳转来绕过限制,例如注册一个假的域名 www.eexample.com,然后通过重定向的方式绕过黑白名单限制。
    // 当url是 http://www.eexample.com/ssrf, 返回302重定向,设置重定向的地址为 oa.in.example.com
    @GetMapping("/ssrf4")
    public ResponseEntity<String> ssrf4(@RequestParam(required = true) String url) throws IOException {

        if (url.toLowerCase().contains("in.example.com")) {
            return new ResponseEntity<>("Url Not Allow", HttpStatus.BAD_REQUEST);
        }

        if (!url.contains("example.com")) {
            return new ResponseEntity<>("Url Not Allow", HttpStatus.BAD_REQUEST);
        }

        try (CloseableHttpClient client = HttpClients.createDefault()) {
            HttpGet request = new HttpGet(url);
            HttpResponse response = client.execute(request);
            return new ResponseEntity<>(EntityUtils.toString(response.getEntity()), HttpStatus.OK);
        }
    }
}

正确修复示例(适用于应用服务所接收的 url 是固定的域名列表或者域名范围是可控的)

// 正确的修复示例
@GetMapping("/fix_ssrf")
public ResponseEntity<String> fixSSRF(@RequestParam(required = true) String url) throws IOException {
    // 1. 获取域名,协议,端口号
    // 使用对应的工具库来获取,这里使用Java的URI库,并将域名和协议转为小写。
    URI new_url = URI.create(url);
    String host = new_url.getHost().toLowerCase();
    String scheme = new_url.getScheme().toLowerCase();
    int port = new_url.getPort();

    System.out.println(host);
    System.out.println(scheme);
    System.out.println(port);
    // 2. 检查 host,协议,端口号是否合法(Apache HTTPClinet 使用了java.net.URI包里的 URI 解析器,该解析器会检查 URI 的语法是否合法,这一步可以跳过)
    // 其它语言,其它库的 URI 解析器,是否会检测 URI 语法是否合法是不确定的(已知Python的urllib库不会检测,curl也不保证),因此这一步作为通用的一步存在。
    Pattern rule = Pattern.compile("[0-9a-z][0-9a-z.-]+[0-9a-z]");      // 域名正则表达式,支持Unicode域名(xn--开头的域名)
    Matcher match = rule.matcher(host);
    if (!match.find()) {
        return new ResponseEntity<>("Url Not Allow", HttpStatus.BAD_REQUEST);
    }
    String[] scheme_white_list = {"http", "https"};  // scheme 白名单
    boolean is_white = Arrays.stream(scheme_white_list).anyMatch(scheme::equals);
    if (!is_white) {
        return new ResponseEntity<>("Url Not Allow", HttpStatus.BAD_REQUEST);
    }

    int[] port_white_list = {-1, 443, 80};      // 端口白名单
    is_white = Arrays.stream(port_white_list).anyMatch(p -> p == port);
    if (!is_white) {
        return new ResponseEntity<>("Url Not Allow", HttpStatus.BAD_REQUEST);
    }

    // 3. 黑名单匹配
    // 黑名单匹配,如果以内网域名结尾,则不允许。
    String[] domain_black_list = {".in.example.com", ".beta.example.com", ".test.example.com", ".gray.example.com"};
    boolean is_black = Arrays.stream(domain_black_list).anyMatch(host::endsWith);
    if (is_black) {
        return new ResponseEntity<>("Url Not Allow", HttpStatus.BAD_REQUEST);
    }

    // 4. 白名单匹配
    // 白名单匹配,如果不是允许的域名/子域名,则不允许发起请求。

    // 精准匹配域名
    String[] exact_match_domain_list = {"mp.weixin.qq.com", "www.didiglobal.com", "m.ctrip.com"};
    boolean is_exact_match = Arrays.asList(exact_match_domain_list).contains(host);

    // 匹配子域名
    // 下面的子域名将只允许 xxx.example.com 的域名进行,不允许 example.com 这个二级域名本身,如果需要这个二级域名本身,需要将 example.com 加入到精准匹配的域名中
    String[] subdomain_list = {".example.com"};
    boolean is_subdomain = Arrays.stream(subdomain_list).anyMatch(host::endsWith);

    // 如果即无法精确匹配,也不是子域名,那么不允许。
    if (!is_subdomain && !is_exact_match) {
        return new ResponseEntity<>("Url Not Allow", HttpStatus.BAD_REQUEST);
    }

    // 5. 禁止自动跟随HTTP重定向
    // 这里使用的库是Apache HttpClient,它默认会自动跟随HTTP重定向,需要禁止它自动跟随HTTP重定向
    try (CloseableHttpClient client = HttpClients.custom()
        .disableRedirectHandling()  // 禁止自动跟随HTTP重定向
        .build()) {

        HttpGet request = new HttpGet(url);
        HttpResponse response = client.execute(request);
        return new ResponseEntity<>(EntityUtils.toString(response.getEntity(), "UTF-8"), HttpStatus.OK);
    }
}

总结为5个步骤:

1.获取域名,协议,端口,转为小写
2.检测域名,协议,端口是否合法
3.黑名单匹配
4.白名单匹配
5.禁止自动跟随HTTP重定向

Q&A(也许这里可以解答一些你的疑问)

Q1:为什么要将域名转为小写?

A1:因为域名的解析是不区分大小写的,为了防止攻击者通过大小写的手法,绕过黑名单。例如,你可以通过www.baidu.com和WWW.BAIDU.COM访问百度。

Q2:为什么要进行黑名单匹配,只匹配白名单不行吗?

A2:通常情况下是可以只使用白名单的。但需要注意的是,上面例子中的内网域名是.in.example.com,当你仅使用白名单限制.example.com的时候,该内网域名可以匹配到白名单,从而绕过限制。这是由于内网域名的设计所导致的。如果内网域名设计为examplein.com,examplegray.com等,那么你只需要使用白名单即可。

Q3:为什么白名单匹配中区分为了精准匹配域名和子域名?

A3:因为软件开发的亘古难题是对抗软件系统的复杂性。为了保持简单性,将其分为精准匹配域名和子域名匹配两部分。如果需求已知精确的域名,那么做精准匹配是最合适的;如果需求是匹配很多的子域名,无法一个一个列举出来,那么可以做子域名匹配。

Q4:为什么要禁止自动跟随重定向?

A4:因为攻击者可以通过 url 跳转的方式来进行绕过。例如:我们的网站 www.example.com 有一个 URL 跳转的 HTTP API(https://www.example.com/jump?url=https://oa.in.example.com),该 API 将重定向 URL 为https://oa.in.example.com,而有的HTTP客户端库会自动跟随重定向,这样就可以绕过限制,从而访问到内网资源。有关URL跳转漏洞,可以阅读参考链接中的URL跳转漏洞。

Q5:为什么建议使用工具库来获取域名,写正则表达式捕获域名不可以吗?

A5:因为攻击者可以通过@, #, 伪造域名等方式绕过,例如:https://www.example.com@attack.com/xxx,如果正则表达式写的不够严谨,将导致提取到的host为 www.example.com@attack.com, 那么将会实际访问 attack.com, 而不是 www.example.com;另外一点是,使用正则表达式分组模式提取域名的时候,容易写出包含大量回溯的代码,而Java, Javascript,python等语言的标准库中提供的正则表达式库都是易受 ReDOS 攻击的。有关ReDOS,可以阅读参考链接中的ReDOS漏洞。

Q6:为什么同时匹配域名 example.com 本身和它的子域名 .example.com 时,不能直接写为匹配子域名 example.com?

A6:域名系统开放给域名申请者的是二级域名的命名权,而不是一级域名的命名权。域名申请者只能在.com, .net, .cn等顶级域名(一级域名)中做选择,然后申请命名自己的二级域名。对于二级域名而言,如果直接匹配 example.com,那么将会导致 eexample.com, aexample.com等这样的伪造域名可以成功匹配。

对于三级,四级等域名而言,其实就不存在这个问题了,你可以直接匹配 www.example.com,因为只要你的 DNS 解析没有被污染,那么你的 DNS 解析对于不存在的子域名无法解析成功,更不可能访问到伪造的域名。但是为了保持软件开发的简单性,一致性,如果你需要同时匹配域名 demo.example.com 本身和它的子域名 .demo.example.com 时,需要在精准匹配列表和子域名匹配列表中分别进行匹配。对于DNS解析感兴趣的同学可以阅读参考链接中的DNS入门知识。

Q7:如果使用的是原始套接字或者自定义协议,如何修复?

A7:如果是使用的原始套接字或者自定义的应用层协议,那么建议使用API Gateway作为中间层,接收客户端请求并转发给后端服务,让 API 网关帮你完成协议转换。这样避免了自定义协议可能存在的安全问题暴露到公网。

Q8:为什么要检测端口是否是允许的端口?

A8:因为有时候企业的网络环境是复杂的,在内网环境下,可能存在某个域名除80,443之外,其它的端口也可以访问的情况,因此要对端口做检测。

其它建议

目前有部分的场景,更推荐使用 API Gateway 作为中间层,接收客户端请求并转发给后端服务;自己开发和维护成本较高,而且容易变成单点故障,需要做好高可用的设计。

以下是API 网关的优缺点和适用场景:

优点:

能提供安全、流量控制、认证、限流等功能。
可以对后端服务进行聚合,提供一个统一的入口。
易于扩展和维护,支持多种协议(HTTP/HTTPS、WebSocket)。

缺点:

增加了系统的复杂度。
需要额外的基础设施和配置。
适用场景:微服务架构、大型分布式系统、需要认证和流量控制的场景。

推荐工具:Kong, Nginx, AWS API Gateway, Apache APISIX

参考链接

1.URL跳转漏洞

2.DNS入门知识

3.ReDOS漏洞


http://www.kler.cn/news/341857.html

相关文章:

  • Java经典面试题-多线程打印
  • js短路求值
  • 网络安全社区和论坛
  • Java入门:10.Java中的包
  • 使用Java调用OpenAI API并解析响应:详细教程
  • 【含文档】基于Springboot+Android的校园论坛系统(含源码+数据库+lw)
  • LeetCode讲解篇之1043. 分隔数组以得到最大和
  • 服装生产管理的现代化:SpringBoot框架
  • 《C++职场中设计模式的学习与应用:开启高效编程之旅》
  • Leetcode.20 有效的括号
  • OpenStreetMap介绍
  • 研发中台拆分之路:深度剖析、心得总结与经验分享
  • Linux_进程概念详解
  • MySql外键约束
  • 舞韵流转:SpringBoot实现古典舞在线交流新体验
  • Pytest测试用例生命周期管理-Fixture
  • VBA即用型代码手册:将工作表复制到已关闭的工作簿
  • YOLO11改进|SPPF篇|引入YOLOv9提出的SPPELAN模块
  • uni-app之旅-day04-商品列表
  • 旅游管理智能化转型:SpringBoot系统设计与实现