攻击_SSRF
前言
Github:https://github.com/HealerJean
一、SSRF 是什么
SSRF(Server-Side Request Forgery)是一种服务器端请求伪造的攻击方式。攻击者通过诱导服务器发起对外部或内部目标的请求,从而绕过网络隔离机制,访问原本无法从外部访问的资源或服务。
1、核心原理:
- 当
Web应用程序提供从指定URL获取资源的功能(如图片加载、文件下载、API 调用等)时,若未对用户提供的URL做严格的校验与限制,攻击者可以构造恶意URL,使服务器向任意地址发起请求。 SSRF通常被用于:- 访问内网资源(如
127.0.0.1、192.168.x.x) - 探测或攻击内部服务(如
Redis、MySQL、SSRF反弹 Shell) - 利用伪协议(如
file://、dict://、gopher://)读取本地文件或执行命令 - 绕过防火墙访问外网限制服务
- 访问内网资源(如
二、SSRF 的危害
| 攻击目标 | 潜在风险 |
|---|---|
| 内网服务 | Redis 未授权访问、SSRF 反弹 Shell |
| 本地文件 | 利用 file:// 读取敏感文件 |
| 外部服务 | 利用服务器身份发起攻击 |
| 云环境 | 获取云平台元数据(如 AWS Metadata API) |
三、SSRF 的防御措施
1、输入验证和过滤
- 白名单机制:只允许访问特定域名或 IP 地址。
- 协议限制:仅允许
http、https等安全协议。 - 禁止伪协议:如
file://、dict://、gopher://等。
2、禁止访问内网地址
- IP 地址黑名单:拒绝
127.0.0.1、192.168.0.0/16、10.0.0.0/8、172.16.0.0/12等私有 IP。 DNS解析控制:防止localhost、metadata.google.internal等解析为内网地址。
3、端口限制
- 限制访问端口为常见的 Web 端口,如
80、443。 - 防止访问
22、23、25、53、8080等非 Web 端口。
4、使用安全组件
- 使用安全的
HTTP客户端库,如ApacheHttpClient、OkHttp。 - 使用
DNS解析时,避免使用系统默认解析器,防止DNS Rebinding攻击。
5、强制访问控制
- 对请求目标进行身份认证和权限控制。
- 使用代理服务器或中间服务来隔离请求。
6、日志和监控
- 记录所有
SSRF请求行为,便于审计和追踪。 - 设置异常请求的告警机制。
四、优化建议
输入验证和过滤:对从用户输入中获取的URL进行严格的验证和过滤,确保只接受合法的URL。使用白名单过滤机制,限制URL只能访问特定的域名或IP地址,从而防止攻击者构造恶意请求。
限制协议和端口:限制服务器端应用程序只能发起特定协议(如HTTP和HTTPS)和特定端口范围内的请求。同样,使用白名单机制,只允许特定的协议和端口,以减少潜在的攻击面。
内网访问限制:确保服务器端应用程序只能发起外部网络的请求,禁止访问内部网络。利用网络隔离技术,将服务器部署在DMZ(Demilitarized Zone)区域,只允许与外部网络通信,以防止SSRF攻击利用内部网络资源。
更新相关组件和框架:及时更新服务器端应用程序使用的相关组件和框架,以修复已知的SSRF漏洞。保持系统和应用程序的最新版本,可以减少被利用的风险。
强制访问控制:使用身份验证和授权机制,限制用户访问特定的功能和资源。这有助于防止未经授权的请求被发送,从而减少SSRF攻击的可能性。
package com.healerjean.proj.utils.http;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Set;
/**
* HTTP 工具类:包含安全 URL 校验(防 SSRF)和响应写入功能
*
* @author zhangyujin
* @date 2025/3/17
*/
@Slf4j
public class HttpUtils {
/**
* 默认允许的协议(用于 SSRF 防护)
*/
private static final Set<String> ALLOWED_PROTOCOLS = Sets.newHashSet("http", "https");
/**
* 默认允许的端口(用于 SSRF 防护)
*/
private static final Set<Integer> ALLOWED_PORTS = Sets.newHashSet(80, 443);
/**
* 全面校验 URL 安全性(用于防止 SSRF 攻击)
* <p>
* 校验项包括:
* 1. 协议是否为 http/https
* 2. 域名是否在白名单内
* 3. 是否尝试访问私有 IP(回环、内网等)
* 4. 端口是否为 80/443
*
* @param url 待校验的 URL 字符串
* @param allowedDomains 允许的域名白名单(支持 .domain.com 通配)
* @return true 表示安全,false 表示存在风险
*/
public static boolean sslfUrl(String url, List<String> allowedDomains) {
if (url == null || url.trim().isEmpty()) {
log.warn("sslfUrl - 输入 URL 为空");
return false;
}
URI uri;
try {
// 使用 URI 而非 URL,避免意外 DNS 查询或连接
uri = new URI(url);
} catch (URISyntaxException e) {
log.warn("sslfUrl - URL 格式非法: {}", url, e);
return false;
}
// 1. 协议检查
String scheme = uri.getScheme();
if (scheme == null || !ALLOWED_PROTOCOLS.contains(scheme.toLowerCase())) {
log.warn("sslfUrl - 协议不合法: {}", url);
return false;
}
// 2. Host 检查
String host = uri.getHost();
if (host == null || host.isEmpty()) {
log.warn("sslfUrl - URL 缺少 host: {}", url);
return false;
}
host = host.toLowerCase();
if (!isAllowedDomain(host, allowedDomains)) {
log.warn("sslfUrl - 域名不在白名单内: {}", url);
return false;
}
// 3. 私有地址检查(防止 SSRF)
try {
InetAddress address = InetAddress.getByName(host);
if (address.isAnyLocalAddress() ||
address.isLoopbackAddress() ||
address.isLinkLocalAddress() ||
address.isSiteLocalAddress()) {
log.warn("sslfUrl - 禁止访问私有或回环地址: {}", url);
return false;
}
} catch (UnknownHostException e) {
log.warn("sslfUrl - DNS 解析失败: {}", url, e);
return false;
}
// 4. 端口检查
int port = uri.getPort();
if (port == -1) {
// 默认端口:https -> 443, http -> 80
port = "https".equalsIgnoreCase(scheme) ? 443 : 80;
}
if (!ALLOWED_PORTS.contains(port)) {
log.warn("sslfUrl - 端口不合法: {} (port={})", url, port);
return false;
}
return true;
}
/**
* 判断 host 是否匹配域名白名单规则
*
* @param host 目标主机名(已转为小写)
* @param allowedDomains 白名单列表(如 [".bail.com", "api.trusted.com"])
* @return 是否匹配
*/
private static boolean isAllowedDomain(String host, List<String> allowedDomains) {
for (String pattern : allowedDomains) {
if (pattern == null || pattern.trim().isEmpty()) {
continue;
}
String cleanPattern = pattern.trim().toLowerCase();
if (cleanPattern.startsWith(".")) {
// 通配模式:.example.com
String domain = cleanPattern.substring(1);
if (domain.isEmpty()) {
continue; // 跳过无效模式如 "."
}
// 匹配主域(example.com)或子域(xxx.example.com)
if (host.equals(domain) || host.endsWith("." + domain)) {
return true;
}
} else {
// 精确匹配模式:example.com
if (host.equals(cleanPattern)) {
return true;
}
}
}
return false;
}
}


