V4.0 安全扩展:29个新模块 + 60项API端点 + 164项新测试全部通过
新增模块(5组29个): 反作弊扩展(6个): - AntiKillAuraService: 杀戮光环检测(攻击频率/目标切换/旋转角度) - AntiReachService: 超距攻击检测(攻击距离/位置回溯验证) - AntiSpeedService: 水平速度检测(移动速度/GroundSpoof异常) - AntiJesusService: 水上行走检测(水面/岩浆面移动验证) - AntiNoFallService: 无摔落伤害检测(落地检测/伤害验证) - AntiScaffoldService: 自动搭路检测(方块放置频率/角度/速度) 玩家行为安全(6个): - AntiNukerService: 快速破坏检测(挖矿速度/范围/模式识别) - AntiAutoClickerService: 自动点击检测(CPS统计/点击间隔熵值分析) - AntiChestStealService: 自动偷箱检测(开箱/取物速度模式识别) - AntiAutoFishService: 自动钓鱼检测(钓鱼行为时序/反应速度) - AntiInventoryManipulationService: 背包操作检测(非法丢弃/槽位异常) - AntiBaritoneService: AI寻路机器人检测(路径平滑度/行为重复率) 服务器保护(6个): - PacketFloodProtectionService: 数据包洪水防护(窗口/递增惩罚机制) - AntiSignExploitService: 告示牌漏洞防护(超长JSON/NBT/命令注入) - AntiBookBanService: 书与笔漏洞防护(超大页码/翻译键滥用/命令注入) - AntiResourcePackExploitService: 资源包漏洞防护(内网SSRF/SHA-1黑名单) - AntiTabCompleteCrashService: Tab补全崩溃防护(深度嵌套/洪水/无效UTF-8) - AntiOfflineModeSpoofService: 离线UUID欺诈防护(格式校验/IP-UUID关联) 访问控制(6个): - OPPrivilegeMonitorService: OP权限监控(权限变更/敏感命令审计) - ConfigTamperDetectionService: 配置文件篡改检测(SHA-256基线监控) - BackdoorPluginScannerService: 后门插件扫描(恶意类名/远程执行检测) - AntiVPNProxyService: VPN代理检测(托管ASN/IP范围匹配) - AntiAltAccountService: 小号检测(IP关联/行为相似度/封禁逃逸) - AntiNameSpoofService: 名称冒充检测(Unicode同形字/空白字符/编辑距离) 聊天社交安全(5个): - ChatFloodProtectionService: 聊天洪水防护(双窗口频率/相似度/递增静音) - AntiAdvertisementService: 广告检测(IP/域名/QQ群/Discord邀请正则) - AntiPhishingLinkService: 钓鱼链接检测(短链接/可疑域名/显示文本不匹配) - AntiCommandAbuseService: 命令滥用检测(洪水/插件探测/Shell注入) - PlayerPrivacyService: 玩家隐私保护(IP脱敏/GDPR合规/数据保留策略) 配置与API: - ServerGuardConfig新增29个boolean开关(默认true),全部独立可控 - application.yml新增29行YAML配置(含中文注释说明功能) - DashboardController整合29个新服务,新增60个REST API端点(总计108个端点) - 所有模块使用双构造函数(无参+@Autowired)、静态结果类工厂方法 测试:新增5个测试类164项测试,全部通过(全项目278项/17测试类/159源文件) 修复: - AntiBookBanService: helper方法改为返回int得分贡献,修复translate/score/clickEvent检测不生效 - AntiResourcePackExploitService: 修复Set.of()中.vbs重复导致ExceptionInInitializer - AntiTabCompleteCrashService: 深度嵌套得分提高至75(触发blocked阈值) - AntiOfflineModeSpoofService: 快速UUID切换得分提高至75(触发blocked阈值) - PacketFloodProtectionService: 重构检测流程,移除提前返回导致的检测屏蔽
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
package com.aluer.security;
|
||||
|
||||
import com.aluer.config.ServerGuardConfig;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.*;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* 资源包漏洞防护服务 — V4.0 服务器保护模块
|
||||
*
|
||||
* 检测原理:
|
||||
* Minecraft允许服务器向客户端推送资源包(Resource Pack)URL,客户端会自动下载
|
||||
* 并加载。如果URL指向恶意资源,可能导致客户端安全风险。本服务验证资源包URL的
|
||||
* 合法性,检测SSRF攻击、恶意文件类型、过大文件、以及已知的恶意SHA-1哈希。
|
||||
*
|
||||
* 攻击场景:
|
||||
* - SSRF攻击:URL指向内网地址(file:///etc/passwd, localhost等)
|
||||
* - 超大文件:资源包>100MB导致客户端下载时内存溢出
|
||||
* - 恶意文件类型:.exe/.jar/.sh等可执行文件伪装为资源包
|
||||
* - 已知恶意哈希:使用已被确认的恶意资源包SHA-1
|
||||
* - HTTP重定向攻击:URL重定向到危险目标
|
||||
*
|
||||
* 配置开关:serverguard.security.super-evolution.anti-resource-pack-exploit
|
||||
*/
|
||||
@Service
|
||||
public class AntiResourcePackExploitService {
|
||||
|
||||
private final ServerGuardConfig config;
|
||||
|
||||
private final Map<String, List<PackEvent>> packHistory = new ConcurrentHashMap<>();
|
||||
private final Map<String, PackUrlInfo> urlCache = new ConcurrentHashMap<>();
|
||||
/** SHA-1哈希黑名单(已知恶意资源包) */
|
||||
private final Set<String> sha1Blacklist = ConcurrentHashMap.newKeySet();
|
||||
private final Set<String> urlBlacklist = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private final AtomicLong totalPacksChecked = new AtomicLong(0);
|
||||
private final AtomicLong blockedPacks = new AtomicLong(0);
|
||||
|
||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
/** 资源包允许的最大文件大小(字节) */
|
||||
private static final long MAX_PACK_SIZE_BYTES = 100 * 1024 * 1024; // 100MB
|
||||
/** URL缓存超时(毫秒) */
|
||||
private static final long URL_CACHE_TTL_MS = 300_000; // 5分钟
|
||||
|
||||
/** 内网IP前缀 */
|
||||
private static final Set<String> INTERNAL_IPv4_PREFIXES = Set.of(
|
||||
"10.", "172.16.", "172.17.", "172.18.", "172.19.", "172.20.",
|
||||
"172.21.", "172.22.", "172.23.", "172.24.", "172.25.", "172.26.",
|
||||
"172.27.", "172.28.", "172.29.", "172.30.", "172.31.", "192.168."
|
||||
);
|
||||
|
||||
/**
|
||||
* 危险文件扩展名(资源包必须是.zip格式,以下扩展名可能包含可执行代码).
|
||||
* 注意:Set.of() 不允许重复元素,每个扩展名必须唯一.
|
||||
*/
|
||||
private static final Set<String> DANGEROUS_EXTENSIONS = Set.of(
|
||||
".exe", ".bat", ".cmd", ".com", ".msi", ".ps1", ".scr",
|
||||
".jar", ".class", ".war", ".ear",
|
||||
".sh", ".bash", ".zsh", ".run", ".bin",
|
||||
".dll", ".so", ".dylib", ".sys",
|
||||
".deb", ".rpm", ".apk", ".ipa",
|
||||
".js", ".vbs", ".jse", ".wsf", ".wsh", ".hta"
|
||||
);
|
||||
|
||||
/** 被禁止的协议 */
|
||||
private static final Set<String> BLOCKED_SCHEMES = Set.of(
|
||||
"file", "ftp", "gopher", "dict", "jar", "netdoc", "tftp"
|
||||
);
|
||||
|
||||
public AntiResourcePackExploitService() {
|
||||
this(new ServerGuardConfig());
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public AntiResourcePackExploitService(ServerGuardConfig config) {
|
||||
this.config = config;
|
||||
// 初始化已知恶意哈希(示例值,生产环境应从威胁情报源同步)
|
||||
initSha1Blacklist();
|
||||
scheduler.scheduleAtFixedRate(this::cleanupOldData, 60, 300, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查资源包URL和SHA-1哈希是否安全。
|
||||
*
|
||||
* @param url 资源包下载URL
|
||||
* @param sha1 资源包SHA-1哈希(必需,Minecraft强制要求)
|
||||
* @param sourcePlayer 发起推送的玩家(OP或控制台)
|
||||
* @param packVersion 资源包格式版本
|
||||
* @return 检查结果,包含是否阻止及原因
|
||||
*/
|
||||
public PackCheckResult check(String url, String sha1, String sourcePlayer, int packVersion) {
|
||||
// 配置开关检查
|
||||
if (!config.getSecurity().getSuperEvolution().isAntiResourcePackExploit()) {
|
||||
return PackCheckResult.clean();
|
||||
}
|
||||
|
||||
totalPacksChecked.incrementAndGet();
|
||||
List<String> reasons = new ArrayList<>();
|
||||
|
||||
// 记录事件
|
||||
PackEvent event = new PackEvent(Instant.now(), url, sha1, sourcePlayer, packVersion);
|
||||
packHistory.computeIfAbsent(sourcePlayer, k -> Collections.synchronizedList(new ArrayList<>())).add(event);
|
||||
|
||||
// ---- 检测1:URL格式验证 ----
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
reasons.add("EMPTY_URL");
|
||||
blockedPacks.incrementAndGet();
|
||||
return PackCheckResult.blocked(reasons);
|
||||
}
|
||||
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(url.trim());
|
||||
} catch (URISyntaxException e) {
|
||||
reasons.add("INVALID_URI: " + e.getMessage());
|
||||
blockedPacks.incrementAndGet();
|
||||
return PackCheckResult.blocked(reasons);
|
||||
}
|
||||
|
||||
// ---- 检测2:协议验证(必须http/https) ----
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null) {
|
||||
reasons.add("MISSING_SCHEME: URL has no protocol");
|
||||
} else {
|
||||
String schemeLower = scheme.toLowerCase();
|
||||
if (BLOCKED_SCHEMES.contains(schemeLower)) {
|
||||
reasons.add("BLOCKED_SCHEME: " + scheme + " protocol is not allowed");
|
||||
} else if (!schemeLower.equals("http") && !schemeLower.equals("https")) {
|
||||
reasons.add("INVALID_SCHEME: " + scheme + " only http/https allowed");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 检测3:内网地址检测(SSRF风险) ----
|
||||
// 攻击者可以通过资源包URL探测内网服务或读取本地文件
|
||||
String host = uri.getHost();
|
||||
if (host != null) {
|
||||
String hostLower = host.toLowerCase();
|
||||
|
||||
// 检测本地回环地址
|
||||
if ("localhost".equals(hostLower) || "127.0.0.1".equals(hostLower)
|
||||
|| "::1".equals(hostLower) || "[::1]".equals(hostLower)
|
||||
|| "0.0.0.0".equals(hostLower)) {
|
||||
reasons.add("SSRF_LOOPBACK: " + host + " is a loopback address");
|
||||
}
|
||||
|
||||
// 检测内网IP段
|
||||
for (String prefix : INTERNAL_IPv4_PREFIXES) {
|
||||
if (hostLower.startsWith(prefix)) {
|
||||
reasons.add("SSRF_PRIVATE_NET: " + host + " is a private network address");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 检测云元数据地址
|
||||
if (hostLower.contains("metadata") || hostLower.equals("169.254.169.254")) {
|
||||
reasons.add("SSRF_CLOUD_METADATA: " + host + " is a cloud metadata endpoint");
|
||||
}
|
||||
|
||||
// 尝试DNS解析检查(防止DNS重绑定攻击)
|
||||
try {
|
||||
InetAddress resolved = InetAddress.getByName(host);
|
||||
String resolvedIP = resolved.getHostAddress();
|
||||
if (resolvedIP.startsWith("10.") || resolvedIP.startsWith("192.168.")
|
||||
|| resolvedIP.equals("127.0.0.1") || resolvedIP.startsWith("172.16.")) {
|
||||
reasons.add("SSRF_DNS_RESOLVE: " + host + " resolves to internal IP " + resolvedIP);
|
||||
}
|
||||
} catch (UnknownHostException ignored) {
|
||||
// 无法解析的域名不一定危险,但标记为可疑
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 检测4:URL黑名单匹配 ----
|
||||
if (urlBlacklist.contains(url.trim().toLowerCase())) {
|
||||
reasons.add("URL_BLACKLISTED: URL is in known malicious list");
|
||||
}
|
||||
|
||||
// ---- 检测5:SHA-1哈希黑名单比对 ----
|
||||
// 检查已知的恶意资源包哈希(可防止恶意资源包被重新分发)
|
||||
if (sha1 != null && !sha1.trim().isEmpty()) {
|
||||
if (sha1Blacklist.contains(sha1.trim().toLowerCase())) {
|
||||
reasons.add("SHA1_BLACKLISTED: resource pack SHA-1 is known malicious");
|
||||
}
|
||||
// SHA-1格式验证:必须是40个十六进制字符
|
||||
if (!sha1.trim().matches("^[0-9a-fA-F]{40}$")) {
|
||||
reasons.add("SHA1_INVALID_FORMAT: SHA-1 must be 40 hex characters");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 检测6:可疑文件扩展名 ----
|
||||
String path = uri.getPath();
|
||||
if (path != null) {
|
||||
String pathLower = path.toLowerCase();
|
||||
// 资源包应为.zip格式
|
||||
if (!pathLower.endsWith(".zip")) {
|
||||
for (String ext : DANGEROUS_EXTENSIONS) {
|
||||
if (pathLower.endsWith(ext)) {
|
||||
reasons.add("DANGEROUS_EXTENSION: " + ext + " is not a valid resource pack file");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 检测7:URL长度限制 ----
|
||||
// 超长的URL可能导致日志注入或缓冲区溢出
|
||||
if (url.length() > 2048) {
|
||||
reasons.add("URL_TOO_LONG: " + url.length() + " chars (max: 2048)");
|
||||
}
|
||||
|
||||
// ---- 检测8:URL编码绕过检测 ----
|
||||
// 检测URL编码中的内网IP(如 %31%32%37%2e%30%2e%30%2e%31 = 127.0.0.1)
|
||||
String decodedLower = url.trim().toLowerCase();
|
||||
if (decodedLower.contains("%31%32%37") || decodedLower.contains("127%2e")
|
||||
|| decodedLower.contains("%31%30%2e") || decodedLower.contains("10%2e")
|
||||
|| decodedLower.contains("%31%39%32%2e%31%36%38") || decodedLower.contains("192%2e168")
|
||||
|| decodedLower.contains("localhost")) {
|
||||
reasons.add("URL_ENCODED_INTERNAL: potentially obfuscated internal address");
|
||||
}
|
||||
|
||||
// 判定结果
|
||||
if (!reasons.isEmpty()) {
|
||||
blockedPacks.incrementAndGet();
|
||||
// 将恶意URL加入黑名单缓存
|
||||
urlCache.put(url.trim().toLowerCase(), new PackUrlInfo(Instant.now(), reasons));
|
||||
return PackCheckResult.blocked(reasons);
|
||||
}
|
||||
|
||||
// 缓存安全URL以避免重复检测
|
||||
urlCache.put(url.trim().toLowerCase(), new PackUrlInfo(Instant.now(), List.of()));
|
||||
|
||||
return PackCheckResult.clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将SHA-1哈希添加到黑名单。
|
||||
*/
|
||||
public void addToSha1Blacklist(String sha1) {
|
||||
if (sha1 != null && sha1.trim().matches("^[0-9a-fA-F]{40}$")) {
|
||||
sha1Blacklist.add(sha1.trim().toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将URL添加到黑名单。
|
||||
*/
|
||||
public void addToUrlBlacklist(String url) {
|
||||
if (url != null && !url.trim().isEmpty()) {
|
||||
urlBlacklist.add(url.trim().toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> getStatus() {
|
||||
Map<String, Object> s = new LinkedHashMap<>();
|
||||
s.put("totalPacksChecked", totalPacksChecked.get());
|
||||
s.put("blockedPacks", blockedPacks.get());
|
||||
s.put("sha1BlacklistSize", sha1Blacklist.size());
|
||||
s.put("urlBlacklistSize", urlBlacklist.size());
|
||||
s.put("cachedUrls", urlCache.size());
|
||||
|
||||
// 最近检查的资源包
|
||||
List<Map<String, Object>> recent = new ArrayList<>();
|
||||
for (Map.Entry<String, List<PackEvent>> e : packHistory.entrySet()) {
|
||||
List<PackEvent> events = e.getValue();
|
||||
if (!events.isEmpty()) {
|
||||
PackEvent last = events.get(events.size() - 1);
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("source", e.getKey());
|
||||
m.put("url", last.url.length() > 100 ? last.url.substring(0, 100) + "..." : last.url);
|
||||
m.put("sha1", last.sha1);
|
||||
m.put("time", last.time.toString());
|
||||
recent.add(m);
|
||||
}
|
||||
}
|
||||
recent.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString()));
|
||||
s.put("recentPacks", recent.subList(0, Math.min(recent.size(), 10)));
|
||||
|
||||
// URL黑名单(最多显示50条)
|
||||
s.put("urlBlacklist", new ArrayList<>(urlBlacklist).subList(0, Math.min(urlBlacklist.size(), 50)));
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化已知恶意SHA-1哈希列表。
|
||||
*/
|
||||
private void initSha1Blacklist() {
|
||||
// 这些是示例值 - 生产环境应从威胁情报源定期更新
|
||||
// 实际部署时可添加已知的恶意资源包哈希
|
||||
}
|
||||
|
||||
private void cleanupOldData() {
|
||||
Instant cutoff = Instant.now().minusMillis(URL_CACHE_TTL_MS);
|
||||
urlCache.entrySet().removeIf(e -> e.getValue().timestamp.isBefore(cutoff));
|
||||
|
||||
Instant eventCutoff = Instant.now().minusSeconds(7200);
|
||||
for (List<PackEvent> events : packHistory.values()) {
|
||||
events.removeIf(e -> e.time.isBefore(eventCutoff));
|
||||
}
|
||||
packHistory.entrySet().removeIf(e -> e.getValue().isEmpty());
|
||||
}
|
||||
|
||||
private static class PackEvent {
|
||||
final Instant time;
|
||||
final String url;
|
||||
final String sha1;
|
||||
final String sourcePlayer;
|
||||
final int packVersion;
|
||||
|
||||
PackEvent(Instant t, String u, String s, String sp, int pv) {
|
||||
this.time = t;
|
||||
this.url = u;
|
||||
this.sha1 = s;
|
||||
this.sourcePlayer = sp;
|
||||
this.packVersion = pv;
|
||||
}
|
||||
}
|
||||
|
||||
private static class PackUrlInfo {
|
||||
final Instant timestamp;
|
||||
final List<String> reasons;
|
||||
|
||||
PackUrlInfo(Instant t, List<String> r) {
|
||||
this.timestamp = t;
|
||||
this.reasons = r;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PackCheckResult {
|
||||
private final boolean blocked;
|
||||
private final List<String> reasons;
|
||||
|
||||
private PackCheckResult(boolean blocked, List<String> reasons) {
|
||||
this.blocked = blocked;
|
||||
this.reasons = reasons;
|
||||
}
|
||||
|
||||
public static PackCheckResult clean() {
|
||||
return new PackCheckResult(false, List.of());
|
||||
}
|
||||
|
||||
public static PackCheckResult blocked(List<String> reasons) {
|
||||
return new PackCheckResult(true, reasons);
|
||||
}
|
||||
|
||||
public boolean isBlocked() { return blocked; }
|
||||
public boolean isClean() { return !blocked; }
|
||||
public List<String> getReasons() { return reasons; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user