1. 项目概述与漏洞背景最近在整理一些经典的老漏洞发现Apache Shiro这个CVE-2010-3863虽然年份久远但其中涉及的路径标准化问题在今天很多自研的权限校验逻辑里依然能看到影子。这个漏洞的本质是Shiro在早期版本进行权限验证前没有对请求的URI进行标准化处理导致攻击者可以通过构造包含/.、/..、//等特殊序列的URL绕过配置的权限拦截规则直接访问到本应受保护的后台接口或页面。听起来是不是有点像我们小时候玩的那种“此路不通就绕个弯”的把戏但就是这种基础的逻辑缺陷往往能造成严重的越权访问。Apache Shiro本身是一个功能强大且应用广泛的安全框架很多Java Web项目尤其是Spring Boot出现之前的老系统都依赖它来做登录认证和权限控制。它的核心工作原理是通过一系列的过滤器链Filter Chain来拦截请求匹配配置的URL模式然后决定是放行、重定向到登录页还是直接拒绝。CVE-2010-3863就出在这个“匹配”环节上。当时Shiro的PathMatchingFilter在判断一个请求是否需要权限校验时直接使用了未经处理的原始请求路径去匹配开发者配置的Ant风格路径模式比如/admin/**。如果攻击者提交的路径是/./admin在Shiro看来它可能不匹配/admin/**但经过Web容器如Tomcat处理后的标准化路径却实实在在指向了/admin资源漏洞就这么产生了。复现这个漏洞不仅仅是为了“能攻击”更重要的是理解权限校验链条中“路径解析一致性”这个关键原则。无论是自己做安全开发还是做渗透测试搞清楚请求在框架层、容器层、应用层分别被如何解读是发现和防御这类逻辑漏洞的基本功。接下来我会带你从环境搭建、漏洞原理分析、手工复现到漏洞修复完整地走一遍这个过程过程中会穿插很多我实际测试时踩过的坑和总结的技巧。2. 漏洞原理深度剖析2.1 Shiro权限校验的核心流程要理解这个绕过漏洞我们得先看看Shiro在1.1.0版本之前一个受保护的请求大概经历了什么。假设我们在shiro.ini或相应的配置类里配置了这样一条规则/admin/** authc意思是所有以/admin开头的路径都需要认证authc。当一个请求比如GET /admin/user/list到达时Shiro的过滤器会开始工作获取请求路径Shiro从HttpServletRequest对象中获取请求的URI比如/admin/user/list。路径模式匹配Shiro将这个获取到的路径与配置中的所有规则键如/admin/**进行匹配。这里使用的通常是Ant风格的路径匹配器。执行拦截逻辑如果匹配到/admin/**并且该规则关联了authc认证过滤器那么Shiro会检查当前会话是否存在已登录的用户。如果没有则中断请求处理可能重定向到登录页或返回401。放行请求如果未匹配到任何需要权限的规则或者权限检查通过请求才会被传递给后续的Servlet或Spring MVC等控制器进行处理。问题就出在第一步和第二步之间。Shiro直接使用了request.getRequestURI()或类似方法返回的原始字符串进行匹配而这个字符串可能包含了容器尚未标准化的特殊字符。2.2 路径标准化差异导致的逻辑断层什么是路径标准化这是Web容器Tomcat, Jetty等提供的一项服务目的是将用户请求中可能包含的冗余、相对路径解析成一个规范化的、绝对上下文路径。举个例子原始请求URI/./admin容器标准化后/admin原始请求URI/xxx/../admin容器标准化后/admin原始请求URI//admin容器标准化后/admin多数容器会将双斜杠合并关键点在于标准化发生在Shiro的权限匹配之后。更准确地说Shiro在过滤器链里进行匹配时容器可能还没有对这个URI进行最终的标准化处理或者Shiro获取的是未经容器完全处理的路径。在某些部署方式或容器版本下request.getRequestURI()返回的就是浏览器发来的原始字符串。于是攻击者精心构造的Payload如/./admin上场了Shiro拿到请求路径/./admin用它去匹配规则/admin/**。Ant路径匹配器通常将/.视为一个字面字符因此/./admin不匹配/admin/**。Shiro认为这个请求不需要认证直接放行。请求被放行后继续传递到Web容器或应用框架如Spring的DispatcherServlet。容器在处理请求映射时会先对路径进行标准化将/./admin转换为/admin。应用内部的路由控制器比如一个RequestMapping(“/admin”)的Controller接收到的是标准化后的/admin路径并成功处理该请求。攻击者就这样在没有登录凭证的情况下直接访问到了后台管理功能。2.3 漏洞影响范围与利用条件这个漏洞的利用条件相对明确Shiro版本影响Apache Shiro 1.1.0之前的所有版本。具体来说是commitab8294940a19743583d91f0c7e29b405d197cc34之前的版本。这个commit修复了此问题。权限配置方式使用了Shiro的过滤器链来定义基于URL路径的拦截规则并且规则中包含了需要认证或鉴权的路径。部署环境与Web容器的具体实现有关但大多数常见容器Tomcat, Jetty, Resin等的默认行为都存在此风险。注意即使你的应用使用了Spring Security等其它安全框架但如果同时集成了Shiro并让其处理部分URL或者应用自身存在类似的、基于原始URI进行权限判断的逻辑同样可能受到此类“路径标准化不一致”问题的威胁。这是一种通用的逻辑缺陷模式。3. 漏洞复现环境搭建纸上得来终觉浅绝知此事要躬行。我们动手搭一个靶场把漏洞真实地跑起来看。3.1 环境准备与工具选择为了快速复现我们使用Vulhub这个优秀的漏洞靶场集成项目。它基于Docker能一键搭建起包含各种漏洞的完整环境省去了我们自己编译老版本Shiro、搭建Web应用的麻烦。你需要准备一台安装好Docker和Docker Compose的Linux机器或虚拟机。我个人习惯用Ubuntu但CentOS、Debian都可以。Windows用户建议使用WSL2。基本的命令行操作知识。一个浏览器以及用于发送HTTP请求的工具比如Burp Suite、Postman或者命令行下的curl。我强烈推荐Burp Suite因为它能方便地拦截、查看和重放请求是Web安全测试的瑞士军刀。首先我们从GitHub上拉取Vulhub的代码git clone https://github.com/vulhub/vulhub.git cd vulhub进入对应的漏洞目录cd shiro/CVE-2010-3863在这个目录下你会看到一个docker-compose.yml文件这就是定义整个靶场环境的配方。3.2 启动漏洞环境执行以下命令来构建并启动容器docker-compose up -d-d参数表示在后台运行。第一次执行时会从Docker Hub拉取镜像可能需要几分钟时间取决于你的网络速度。看到类似下面的输出就表示启动成功了Creating network “shiro-cve-2010-3863_default” with the default driver Creating shiro-cve-2010-3863_web_1 … done现在打开你的浏览器访问http://你的靶机IP:8080。如果看到Shiro示例应用的默认首页可能是一个简单的欢迎页面说明环境已经正常运行。实操心得有时候8080端口可能被占用。你可以修改docker-compose.yml文件将”8080:8080″左边的宿主端口改成别的比如”8088:8080″。修改后需要先docker-compose down停止旧容器再重新docker-compose up -d。3.3 靶场结构初探这个靶场通常模拟了一个简单的Web应用其中包含一个公开的首页/和一个需要认证才能访问的管理后台/admin。我们的目标就是在未登录的情况下通过路径绕过技巧访问到/admin页面。你可以先尝试直接访问http://你的靶机IP:8080/admin。正常情况下Shiro的authc过滤器会拦截这个请求并将你重定向到一个登录页面可能是/login.jsp或者返回一个401/403错误。记下这个正常被拦截的表现后面好做对比。4. 手工漏洞复现与验证环境好了我们开始真正的“绕过”测试。这里我会演示几种常见的绕过Payload和测试方法。4.1 基础绕过Payload测试最直接的测试就是使用/./、/../、//等序列。方法一使用浏览器或curl在浏览器地址栏直接输入http://你的靶机IP:8080/./admin或者使用curl命令curl -v http://你的靶机IP:8080/./admin观察响应。如果漏洞存在你可能会看到以下情况之一直接返回了/admin页面的内容200 OK这是最理想的证明。返回了302重定向但Location头指向的不是登录页而是其他地址这可能意味着绕过成功但触发了其他逻辑。返回了404这可能说明/admin这个路径本身不存在或者Payload构造方式需要调整。方法二使用Burp Suite进行系统化测试手工在地址栏输入效率低我们使用Burp的Intruder模块来批量测试多种Payload。拦截请求打开Burp配置好浏览器代理。在浏览器中访问http://靶机IP:8080/admin这个请求会被Burp Proxy拦截。发送到Intruder在Proxy的拦截历史中右键点击这个请求选择Send to Intruder。设置攻击位置在Intruder标签页的Positions子标签里Burp会自动标记一些参数。我们需要手动设置。清空所有自动标记点击Clear §然后选中URL路径中的/admin这部分。添加Payload位置选中/admin后点击Add §将其标记为Payload插入点。现在你的请求路径看起来应该是GET /§admin§ HTTP/1.1。选择Payload类型切换到Payloads子标签。在Payload Sets里选择Payload type为Simple list。填入测试Payload在下面的Payload Options [Simple list]框中输入我们想测试的各种路径变异字符串。这里有个技巧因为我们在/admin前后都加了位置标记所以Payload应该是能直接替换admin这个部分的。但更通用的方法是把整个路径作为Payload。我们可以换个思路在Positions里直接标记整个路径然后Payload里放各种变体。为了简单起见我们直接列出常见Payload/admin /./admin /admin/ /admin/. /admin/.. /admin/../ //admin /admin// /xxx/../admin /admin;/ (分号绕过针对某些容器可一并测试) /admin../ /admin..;/你也可以从SecLists等Payload库中导入更全面的列表。开始攻击点击Start attack。Burp会创建一个新窗口用每个Payload替换原位置并发起请求。分析结果攻击完成后重点关注Status状态码和Length响应长度这两列。寻找那些与原始请求直接访问/admin通常返回302或401状态码和长度明显不同的条目。比如如果出现了200 OK并且响应长度很大那很可能就是绕过成功了。双击该条目查看响应内容确认是否包含了后台管理页面的数据。4.2 绕过原理的逆向验证仅仅看到绕过成功还不够我们最好能从流量层面验证一下“路径解析不一致”这个原理。在Burp中分别拦截两个请求GET /admin HTTP/1.1GET /./admin HTTP/1.1将它们分别发送到Repeater模块。在Repeater中发送这两个请求仔细观察响应头。对于/admin请求响应头里很可能有一个Location: /login.jsp这样的重定向字段。对于/./admin请求如果漏洞利用成功可能不会出现重定向到登录页的Location头而是直接返回了200状态码和页面内容。更进一步如果你有权限查看靶场应用的日志比如通过docker-compose logs可以观察一下应用层面记录的两个请求的路径。很可能会发现应用日志里记录的都是标准化后的/admin这直观地证明了容器层做了归一化处理。常见问题为什么我测试的/./admin返回的是404 这可能有几个原因应用路由不支持靶场应用的后台控制器可能只映射了/admin没有映射/admin/。当你请求/./admin时容器标准化后可能仍然是/admin但某些框架或静态资源处理器对路径的解析有细微差别。可以尝试/admin/或/./admin/。Payload位置不对/./必须紧跟在上下文路径之后。如果你的应用部署在/myapp下那么完整的URL应该是http://ip:port/myapp/./admin。环境问题极少数情况下某些旧版本容器对路径标准化的时机不同。可以尝试其他Payload如//admin、/admin/../admin。漏洞已修复确认你启动的确实是Shiro 1.0.0版本的环境。检查docker-compose.yml中使用的镜像标签。5. 漏洞修复方案与安全启示5.1 官方修复方案解读Apache Shiro在1.1.0版本中修复了此漏洞。修复的核心思想是在Shiro进行路径匹配之前先对请求的URI进行一次与Web容器行为一致的标准化处理。我们可以看一下修复的commit (ab8294940) 中的关键代码。修复主要发生在PathMatchingFilter及其相关工具类中。Shiro引入了PathMatchingFilterChainResolver等类在解析和匹配路径时会调用WebUtils.*getPathWithinApplication*(request)来获取路径而这个方法内部会使用HttpServletRequest.*getServletPath*()和getPathInfo()并对其进行合并与标准化确保Shiro用于匹配的路径与最终到达Servlet的路径是同一个版本。修复方案的要点标准化时机前置将路径标准化作为权限校验的第一步确保用于匹配的路径是“干净的”。使用容器API优先使用request.getServletPath()和request.getPathInfo()而非直接使用request.getRequestURI()因为前者通常是容器已经处理过的、更准确的应用内路径。移除上下文路径确保匹配时使用的路径不包含Web应用的上下文路径Context Path避免因上下文路径的拼接问题导致匹配失败。5.2 针对自身项目的修复与加固建议如果你的项目还在使用Shiro 1.1.0之前的版本最直接、最有效的修复方案就是升级Shiro到最新稳定版。在升级时注意测试原有的权限配置是否依然工作正常因为新版本的路径匹配逻辑可能略有变化。除了升级从这次漏洞中我们可以汲取更通用的安全开发经验权限校验的“黄金准则”——一致性在任何安全校验点过滤器、拦截器、AOP切面用于决策的“资源标识符”如URL路径、方法名、数据ID必须与业务逻辑层最终使用的标识符完全一致。最好能从同一个经过权威处理的源头获取。对用户输入进行规范化对于URL路径、文件名这类用于资源定位的输入在进入核心逻辑前应主动进行一次标准化Normalization和规范化Canonicalization处理。在Java中可以使用java.io.File的getCanonicalPath()注意文件系统操作或org.springframework.util.StringUtils.cleanPath等工具方法。采用白名单机制如果可能定义明确的、合法的URL路径模式白名单拒绝任何不符合模式的请求。这比黑名单拒绝已知的恶意模式更有效。在架构层面统一入口设计一个统一的网关或前置过滤器对所有入站请求的路径进行清洗和标准化后续的所有组件都依赖这个清洗后的结果避免各组件解析不一致。进行专项安全测试在渗透测试或代码审计中将“路径标准化绕过”作为一项固定的测试用例。测试Payload库应包含/.、/..、//、/…/多个点、;分号、%2e、%2f编码形式、\反斜杠Windows环境下等。5.3 现代框架中的类似问题与防护虽然这是Shiro的老漏洞但这类问题并未绝迹。在现代开发中你需要注意Spring Security它本身有较为完善的路径处理机制但如果你自定义了Filter或SecurityFilterChain的匹配规则并且手动从HttpServletRequest中获取路径同样可能踩坑。应使用Spring Security提供的RequestMatcher接口及其实现如AntPathRequestMatcher、MvcRequestMatcher它们内部会处理路径解析问题。自定义拦截器很多项目会写自己的权限拦截器。切记不要直接使用request.getRequestURI()而应该使用框架提供的工具方法来获取“应用内部路径”例如Spring的RequestContextUtils.getLookupPathForRequest或直接解析request.getServletPath()。API网关/反向代理如Nginx在多层架构中请求可能经过Nginx再到达应用。要确保Nginx的proxy_pass指令传递的URL是规范的同时应用服务器接收到的X-Forwarded-Prefix等头信息被正确解读防止因为路径重写而引入新的绕过点。6. 漏洞挖掘与测试技巧延伸掌握了这个特定漏洞的复现方法后我们可以把思路拓宽看看在实战中如何主动发现这类问题。6.1 黑盒测试方法论当你面对一个陌生的Web应用怀疑其存在权限校验逻辑时可以遵循以下步骤进行路径标准化绕过的测试识别保护端点首先通过爬虫、目录扫描或分析前端JS找到那些需要权限的接口或页面例如/admin、/api/user/profile、/dashboard。验证基础拦截直接访问这些端点确认其确实受到了保护返回403、401或重定向到登录。构造测试用例针对每个受保护的端点系统性地尝试以下Payload变体假设目标端点为/protected目录遍历式/./protected/xxx/../protected/protected/../protected多余分隔符//protected/protected/////protected编码混淆/%2e/protected(.的URL编码)/%2f/protected(/的URL编码有时容器解码顺序不同)\protected(Windows路径分隔符在某些解析场景下可能被错误处理)。后缀截断/protected;/protected../protected%00(空字节需看环境)。混合拼接/./%2f/protected/protected/./..观察差异使用Burp Intruder或自己写的脚本批量发送请求对比响应状态码、长度、内容以及重定向目标。任何与基准请求直接访问被拒不同的响应都值得深入分析。上下文路径处理如果应用部署在非根路径如http://host/app/protected测试时Payload需要放在上下文路径之后/app/./protected。有时还需要测试对上下文路径本身的绕过。6.2 白盒代码审计关注点如果你是开发人员或进行代码审计可以在源码中搜索以下风险点关键词搜索在Java项目中搜索getRequestURI()、getRequestURL()、getServletPath()、getPathInfo()等方法的使用。检查这些方法返回的路径是否被直接用于权限判断、路由匹配或文件操作。权限校验逻辑找到自定义的过滤器、拦截器或AOP切面看它们如何获取请求路径。重点检查路径匹配前是否有标准化操作。框架配置检查Shiro、Spring Security等安全框架的配置文件或配置类确认其使用的路径匹配器是否是最新版本或者是否有自定义的路径解析逻辑。文件操作搜索new File(userInputPath)、Paths.get(userInput)等代码这里可能存在路径遍历漏洞其原理与本次讨论的URL绕过类似都是由于未规范化输入导致的。6.3 自动化测试脚本示例为了提高效率可以写一个简单的Python脚本进行批量测试。这里提供一个使用requests库的基础示例import requests import sys def test_path_bypass(target_url, protected_path): 测试给定路径的绕过可能性 headers {User-Agent: Mozilla/5.0 Security Test} # 基础Payload列表 payloads [ protected_path, # 原始路径作为基准 /. protected_path, protected_path /., /.. protected_path, protected_path /.., // protected_path.lstrip(/), protected_path.rstrip(/) //, /xxx/../ protected_path.lstrip(/), protected_path ;, protected_path .., protected_path %00, ] print(f[*] Testing {target_url} with path: {protected_path}) print(- * 60) baseline_response None for payload in payloads: test_url target_url.rstrip(/) payload try: resp requests.get(test_url, headersheaders, allow_redirectsFalse, timeout5) status resp.status_code length len(resp.content) location resp.headers.get(Location, ) # 第一个payload是基准 if payload protected_path: baseline_response (status, length, location) print(f[BASE] {payload:30s} - Status: {status}, Length: {length}, Redirect: {location[:50]}) else: # 与基准对比 base_status, base_length, base_loc baseline_response if status ! base_status or length ! base_length: # 响应有显著差异可能绕过成功 print(f[!] POSSIBLE BYPASS: {payload:30s} - Status: {status}, Length: {length}, Redirect: {location[:50]}) else: print(f[ ] {payload:30s} - Status: {status}, Length: {length}) except requests.exceptions.RequestException as e: print(f[x] Error testing {payload}: {e}) if __name__ __main__: if len(sys.argv) ! 3: print(Usage: python shiro_bypass_test.py base_url protected_path) print(Example: python shiro_bypass_test.py http://192.168.1.100:8080 /admin) sys.exit(1) base_url sys.argv[1] path sys.argv[2] test_path_bypass(base_url, path)这个脚本会逐个尝试Payload并比较响应与原始请求的差异。将差异显著的请求标记出来供人工进一步验证。你可以根据需要扩展Payload列表和对比逻辑比如比较响应正文的哈希值。7. 总结与反思CVE-2010-3863是一个典型的“解析差异”漏洞。它不涉及复杂的加密算法或内存破坏而是源于安全组件与应用容器之间对同一数据理解的不一致。这种漏洞看似简单却非常隐蔽因为开发者在单一视角下要么在Shiro配置里要么在Controller里看到的逻辑都是正确的。复现这个老漏洞给我的启示是安全是一个整体链条任何一个环节的“想当然”都可能成为突破口。在设计权限系统时我们必须追问“我用来做决策的信息和最终执行业务逻辑时使用的信息是百分之百同一份吗它们经过的解析、转换流程完全一致吗”对于防御者而言升级到已修复的版本是必须的但更重要的是建立一种“怀疑输入”和“统一解析”的安全开发意识。对于攻击者而言这个案例展示了黑盒测试中一种有效的思路寻找同一数据在不同处理阶段被差异化解析的机会这种思路同样适用于参数解析、头处理、会话管理等多个方面。最后漏洞复现的环境在实验结束后记得用docker-compose down命令清理释放资源。安全研究最好都在隔离的虚拟环境或专用机器中进行避免对生产或办公网络造成意外影响。