Apache Shiro CVE-2010-3863路径标准化漏洞解析与复现
2026/7/3 16:39:02 网站建设 项目流程

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的过滤器会开始工作:

  1. 获取请求路径:Shiro从HttpServletRequest对象中获取请求的URI,比如/admin/user/list
  2. 路径模式匹配:Shiro将这个获取到的路径,与配置中的所有规则键(如/admin/**)进行匹配。这里使用的通常是Ant风格的路径匹配器。
  3. 执行拦截逻辑:如果匹配到/admin/**,并且该规则关联了authc(认证)过滤器,那么Shiro会检查当前会话是否存在已登录的用户。如果没有,则中断请求处理,可能重定向到登录页或返回401。
  4. 放行请求:如果未匹配到任何需要权限的规则,或者权限检查通过,请求才会被传递给后续的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)上场了:

  1. Shiro拿到请求路径/./admin,用它去匹配规则/admin/**。Ant路径匹配器通常将/.视为一个字面字符,因此/./admin不匹配/admin/**。Shiro认为这个请求不需要认证,直接放行。
  2. 请求被放行后,继续传递到Web容器或应用框架(如Spring的DispatcherServlet)。容器在处理请求映射时,会先对路径进行标准化,将/./admin转换为/admin
  3. 应用内部的路由控制器(比如一个@RequestMapping(“/admin”)的Controller)接收到的是标准化后的/admin路径,并成功处理该请求。
  4. 攻击者就这样在没有登录凭证的情况下,直接访问到了后台管理功能。

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应用的麻烦。

你需要准备:

  1. 一台安装好DockerDocker Compose的Linux机器或虚拟机。我个人习惯用Ubuntu,但CentOS、Debian都可以。Windows用户建议使用WSL2。
  2. 基本的命令行操作知识。
  3. 一个浏览器,以及用于发送HTTP请求的工具,比如Burp SuitePostman或者命令行下的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

观察响应。如果漏洞存在,你可能会看到以下情况之一:

  1. 直接返回了/admin页面的内容(200 OK),这是最理想的证明。
  2. 返回了302重定向,但Location头指向的不是登录页,而是其他地址,这可能意味着绕过成功但触发了其他逻辑。
  3. 返回了404,这可能说明/admin这个路径本身不存在,或者Payload构造方式需要调整。

方法二:使用Burp Suite进行系统化测试手工在地址栏输入效率低,我们使用Burp的Intruder模块来批量测试多种Payload。

  1. 拦截请求:打开Burp,配置好浏览器代理。在浏览器中访问http://靶机IP:8080/admin,这个请求会被Burp Proxy拦截。

  2. 发送到Intruder:在Proxy的拦截历史中,右键点击这个请求,选择Send to Intruder

  3. 设置攻击位置:在Intruder标签页的Positions子标签里,Burp会自动标记一些参数。我们需要手动设置。清空所有自动标记(点击Clear §),然后选中URL路径中的/admin这部分。

  4. 添加Payload位置:选中/admin后,点击Add §,将其标记为Payload插入点。现在你的请求路径看起来应该是GET /§admin§ HTTP/1.1

  5. 选择Payload类型:切换到Payloads子标签。在Payload Sets里,选择Payload typeSimple list

  6. 填入测试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库中导入更全面的列表。

  7. 开始攻击:点击Start attack。Burp会创建一个新窗口,用每个Payload替换原位置,并发起请求。

  8. 分析结果:攻击完成后,重点关注Status(状态码)和Length(响应长度)这两列。寻找那些与原始请求(直接访问/admin,通常返回302或401)状态码和长度明显不同的条目。比如,如果出现了200 OK,并且响应长度很大,那很可能就是绕过成功了。双击该条目查看响应内容,确认是否包含了后台管理页面的数据。

4.2 绕过原理的逆向验证

仅仅看到绕过成功还不够,我们最好能从流量层面验证一下“路径解析不一致”这个原理。

  1. 在Burp中,分别拦截两个请求:
    • GET /admin HTTP/1.1
    • GET /./admin HTTP/1.1
  2. 将它们分别发送到Repeater模块。
  3. 在Repeater中发送这两个请求,仔细观察响应头。
    • 对于/admin请求,响应头里很可能有一个Location: /login.jsp这样的重定向字段。
    • 对于/./admin请求,如果漏洞利用成功,可能不会出现重定向到登录页的Location头,而是直接返回了200状态码和页面内容。
  4. 更进一步,如果你有权限查看靶场应用的日志(比如通过docker-compose logs),可以观察一下应用层面记录的两个请求的路径。很可能会发现,应用日志里记录的都是标准化后的/admin,这直观地证明了容器层做了归一化处理。

常见问题:为什么我测试的/./admin返回的是404? 这可能有几个原因:

  1. 应用路由不支持:靶场应用的后台控制器可能只映射了/admin,没有映射/admin/。当你请求/./admin时,容器标准化后可能仍然是/admin,但某些框架或静态资源处理器对路径的解析有细微差别。可以尝试/admin//./admin/
  2. Payload位置不对/./必须紧跟在上下文路径之后。如果你的应用部署在/myapp下,那么完整的URL应该是http://ip:port/myapp/./admin
  3. 环境问题:极少数情况下,某些旧版本容器对路径标准化的时机不同。可以尝试其他Payload,如//admin/admin/../admin
  4. 漏洞已修复:确认你启动的确实是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的路径是同一个版本。

修复方案的要点:

  1. 标准化时机前置:将路径标准化作为权限校验的第一步,确保用于匹配的路径是“干净的”。
  2. 使用容器API:优先使用request.getServletPath()request.getPathInfo()而非直接使用request.getRequestURI(),因为前者通常是容器已经处理过的、更准确的应用内路径。
  3. 移除上下文路径:确保匹配时使用的路径不包含Web应用的上下文路径(Context Path),避免因上下文路径的拼接问题导致匹配失败。

5.2 针对自身项目的修复与加固建议

如果你的项目还在使用Shiro 1.1.0之前的版本,最直接、最有效的修复方案就是升级Shiro到最新稳定版。在升级时,注意测试原有的权限配置是否依然工作正常,因为新版本的路径匹配逻辑可能略有变化。

除了升级,从这次漏洞中我们可以汲取更通用的安全开发经验:

  1. 权限校验的“黄金准则”——一致性:在任何安全校验点(过滤器、拦截器、AOP切面),用于决策的“资源标识符”(如URL路径、方法名、数据ID)必须与业务逻辑层最终使用的标识符完全一致。最好能从同一个经过权威处理的源头获取。
  2. 对用户输入进行规范化:对于URL路径、文件名这类用于资源定位的输入,在进入核心逻辑前,应主动进行一次标准化(Normalization)和规范化(Canonicalization)处理。在Java中,可以使用java.io.FilegetCanonicalPath()(注意文件系统操作)或org.springframework.util.StringUtils.cleanPath等工具方法。
  3. 采用白名单机制:如果可能,定义明确的、合法的URL路径模式白名单,拒绝任何不符合模式的请求。这比黑名单(拒绝已知的恶意模式)更有效。
  4. 在架构层面统一入口:设计一个统一的网关或前置过滤器,对所有入站请求的路径进行清洗和标准化,后续的所有组件都依赖这个清洗后的结果,避免各组件解析不一致。
  5. 进行专项安全测试:在渗透测试或代码审计中,将“路径标准化绕过”作为一项固定的测试用例。测试Payload库应包含:/./..///…/(多个点)、;(分号)、%2e%2f(编码形式)、\(反斜杠,Windows环境下)等。

5.3 现代框架中的类似问题与防护

虽然这是Shiro的老漏洞,但这类问题并未绝迹。在现代开发中,你需要注意:

  • Spring Security:它本身有较为完善的路径处理机制,但如果你自定义了FilterSecurityFilterChain的匹配规则,并且手动从HttpServletRequest中获取路径,同样可能踩坑。应使用Spring Security提供的RequestMatcher接口及其实现(如AntPathRequestMatcherMvcRequestMatcher),它们内部会处理路径解析问题。
  • 自定义拦截器:很多项目会写自己的权限拦截器。切记不要直接使用request.getRequestURI(),而应该使用框架提供的工具方法来获取“应用内部路径”,例如Spring的RequestContextUtils.getLookupPathForRequest或直接解析request.getServletPath()
  • API网关/反向代理(如Nginx):在多层架构中,请求可能经过Nginx再到达应用。要确保Nginx的proxy_pass指令传递的URL是规范的,同时应用服务器接收到的X-Forwarded-Prefix等头信息被正确解读,防止因为路径重写而引入新的绕过点。

6. 漏洞挖掘与测试技巧延伸

掌握了这个特定漏洞的复现方法后,我们可以把思路拓宽,看看在实战中如何主动发现这类问题。

6.1 黑盒测试方法论

当你面对一个陌生的Web应用,怀疑其存在权限校验逻辑时,可以遵循以下步骤进行路径标准化绕过的测试:

  1. 识别保护端点:首先通过爬虫、目录扫描或分析前端JS,找到那些需要权限的接口或页面,例如/admin/api/user/profile/dashboard
  2. 验证基础拦截:直接访问这些端点,确认其确实受到了保护(返回403、401或重定向到登录)。
  3. 构造测试用例:针对每个受保护的端点,系统性地尝试以下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/./..
  4. 观察差异:使用Burp Intruder或自己写的脚本批量发送请求,对比响应状态码、长度、内容以及重定向目标。任何与基准请求(直接访问被拒)不同的响应都值得深入分析。
  5. 上下文路径处理:如果应用部署在非根路径(如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, headers=headers, allow_redirects=False, timeout=5) 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命令清理,释放资源。安全研究最好都在隔离的虚拟环境或专用机器中进行,避免对生产或办公网络造成意外影响。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询