Tomcat文件包含漏洞深度解析:从原理到防御的实战指南
1. 项目概述:为什么我们要关注Tomcat文件包含漏洞?
如果你是一名Web应用开发者、安全研究员或者运维工程师,那么“Apache Tomcat文件包含漏洞”这个词组对你来说绝对不陌生。它不像那些惊天动地的远程代码执行漏洞那样引人注目,但在实际的渗透测试和红蓝对抗中,文件包含漏洞往往是打开内网大门的“第一把钥匙”。我见过太多案例,一个看似不起眼的文件包含点,最终演变成整个内网沦陷的起点。今天,我们就来深入拆解这个漏洞,不仅仅是复现,更要理解它的前世今生、触发条件、利用手法,以及最关键的——如何防御。
简单来说,文件包含漏洞允许攻击者通过Web应用的参数,动态包含并执行服务器上的任意文件。在Tomcat的语境下,这通常与特定的配置错误、老旧版本的缺陷,或者应用程序自身的逻辑问题紧密相关。复现这个漏洞,不仅能让你直观感受到攻击链是如何串联的,更能让你在开发或运维时,下意识地避开那些“坑”。网上教程很多,但大多只给步骤,不讲原理。我的目标是带你走一遍我踩过的路,把每个参数、每个报错背后的逻辑都讲清楚。
2. 漏洞原理深度解析:不仅仅是“包含”那么简单
2.1 文件包含漏洞的核心机制
要理解Tomcat下的文件包含,我们得先抛开Tomcat,看看文件包含漏洞的通用原理。它主要分为两种:本地文件包含(LFI)和远程文件包含(RFI)。LFI允许攻击者包含并读取服务器本地的文件,比如/etc/passwd、应用源码、配置文件等。而RFI则更危险,攻击者可以包含一个远程服务器上的恶意文件(如一个包含PHP代码的文本文件),并让目标服务器执行其中的代码。
在Java Web应用中,文件包含通常发生在使用RequestDispatcher.include()或RequestDispatcher.forward()方法时,如果开发者未经严格过滤就将用户输入(如请求参数)直接拼接到文件路径中,漏洞就产生了。例如,一个JSP页面有这样一段代码:
<% String page = request.getParameter("page"); if (page != null) { request.getRequestDispatcher("/pages/" + page + ".jsp").include(request, response); } %>攻击者只需要构造参数page=../../../WEB-INF/web.xml,就有可能穿越目录,读取到Web应用的敏感配置文件web.xml。
2.2 Tomcat环境下的特殊性
Tomcat作为Servlet容器,其自身的一些特性和历史配置会加剧文件包含的风险:
- 默认Servlet的配置:Tomcat的默认Servlet(
DefaultServlet)负责处理静态资源。在某些特定配置下(如readonly设置为false且未做严格路径限制),可能会被滥用来进行文件读取。 - JSP预编译与包含:JSP文件在首次被访问时会被Tomcat编译成Servlet。
<%@ include file=”...” %>这类静态包含指令,如果文件路径可控,就可能造成LFI。虽然动态包含(<jsp:include page=”...” />)更常见,但原理相似。 - 老旧版本与特定模块:历史上,Tomcat某些版本与第三方库(如某些版本的
Apache JServ Protocol连接器)结合时,存在可被利用的文件包含缺陷。此外,像CVE-2020-1938(Ghostcat)这类与AJP协议相关的漏洞,虽然本质是文件读取/包含,但利用方式完全不同。
我们本次复现聚焦于由应用逻辑缺陷引发的本地文件包含(LFI),这是开发中最容易不小心引入,也最具有普遍教育意义的类型。理解了这个,你就能举一反三。
注意:切勿在非授权环境中进行任何漏洞测试。所有实验必须在你自己完全控制的、隔离的虚拟机或实验环境中进行。
2.3 与网络热词中的其他漏洞对比
你可能会看到“永恒之蓝”、“Shiro反序列化”这些更“炫酷”的漏洞。文件包含漏洞通常被认为是“低危”的,因为它可能“只是”读取文件。但这是一个严重的误区。通过LFI,攻击者可以:
- 读取配置文件:获取数据库密码、API密钥、加密盐值。
- 读取源码:进行白盒审计,发现更深的逻辑漏洞。
- 结合其他漏洞:例如,在知道绝对路径后,配合文件上传漏洞,就能实现从LFI到RCE(远程代码执行)的质变。或者,在某些特定环境下(如
php://等包装器在特定配置下可用),LFI可以直接转化为代码执行。 因此,永远不要小看一个文件包含点。
3. 实验环境搭建与漏洞应用准备
3.1 靶机环境配置
为了原汁原味地复现,我们手动搭建一个存在漏洞的简单Web应用,而不是使用现成的漏洞靶场。这样你能更清楚地看到漏洞是如何被“编码”进去的。
1. 基础环境:
- 操作系统:Ubuntu 22.04 LTS 或 Windows 10/11。我推荐使用Linux,路径处理更清晰。这里以Ubuntu为例。
- Java环境:安装JDK 8或11。Tomcat对JDK版本有要求,建议使用LTS版本。
sudo apt update sudo apt install openjdk-11-jdk java -version # 验证安装 - Apache Tomcat:从 Apache Tomcat官网 下载Tomcat 9.x版本。选择
tar.gz包。wget https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.85/bin/apache-tomcat-9.0.85.tar.gz tar -xzf apache-tomcat-9.0.85.tar.gz mv apache-tomcat-9.0.85 ~/tomcat9 cd ~/tomcat9
2. 创建漏洞应用:在~/tomcat9/webapps/目录下,新建一个文件夹vuln-app。
mkdir ~/tomcat9/webapps/vuln-app mkdir ~/tomcat9/webapps/vuln-app/WEB-INF mkdir ~/tomcat9/webapps/vuln-app/WEB-INF/classes创建web.xml,这是应用的部署描述符。
nano ~/tomcat9/webapps/vuln-app/WEB-INF/web.xml输入以下内容:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <display-name>Vulnerable File Include App</display-name> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>现在,创建存在漏洞的JSP页面index.jsp:
nano ~/tomcat9/webapps/vuln-app/index.jsp输入以下漏洞代码:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>文件包含漏洞演示</title> </head> <body> <h2>文件包含演示页面</h2> <% // 漏洞点:直接获取用户输入并用于文件包含,未做任何过滤和路径校验 String file = request.getParameter("file"); if (file != null) { try { // 使用RequestDispatcher进行包含,这是Servlet标准API request.getRequestDispatcher(file).include(request, response); } catch (Exception e) { out.println("<p style='color:red;'>包含文件时出错: " + e.getMessage() + "</p>"); } } else { out.println("<p>请通过?file=参数指定要包含的文件。</p>"); } %> <hr> <p>示例(正常功能):<a href="?file=/header.jsp">?file=/header.jsp</a> (假设存在)</p> <p>示例(漏洞利用):<a href="?file=../../../../etc/passwd">?file=../../../../etc/passwd</a></p> </body> </html>再创建一个正常的被包含文件header.jsp,放在应用根目录:
nano ~/tomcat9/webapps/vuln-app/header.jsp<h3>这是正常的页眉文件</h3> <p>当前时间:<%= new java.util.Date() %></p>3.2 启动Tomcat并验证
进入Tomcat的bin目录,启动它:
cd ~/tomcat9/bin ./startup.sh # Windows下使用 startup.bat如果看到Tomcat started.类似的日志,说明启动成功。默认服务运行在8080端口。
打开浏览器,访问http://你的服务器IP:8080/vuln-app/。 你应该能看到我们的演示页面。先点击“?file=/header.jsp”链接,页面会正常显示页眉内容和时间。这说明包含功能在工作。
4. 漏洞复现与利用实战
环境就绪,现在让我们开始“攻击”自己搭建的这个脆弱应用。
4.1 基础利用:读取敏感系统文件
点击页面上给出的漏洞利用链接?file=../../../../etc/passwd,或者直接在地址栏手动构造。
http://localhost:8080/vuln-app/?file=../../../../etc/passwd发生了什么?
- 我们的应用在
/tomcat9/webapps/vuln-app/目录下。 - 参数
file的值为../../../../etc/passwd。 request.getRequestDispatcher(file)会基于当前请求的上下文路径(/vuln-app)来解析这个相对路径。../意味着向上级目录跳转。从vuln-app目录开始:- 第一个
../:跳到webapps - 第二个
../:跳到tomcat9 - 第三个
../:跳到tomcat9的父目录(通常是用户主目录) - 第四个
../:跳到根目录/ - 然后拼接
etc/passwd,最终尝试包含/etc/passwd文件。
- 第一个
如果服务器是Linux且Tomcat进程有读取权限,你将在网页上看到/etc/passwd文件的内容。这就是最经典的本地文件包含。
实操心得:路径穿越的“深度”需要多少个../?这取决于你的Web应用在服务器文件系统中的实际深度。我常用的方法是先尝试../../../,如果返回“文件未找到”或类似错误,再逐步增加../的数量。也可以尝试绝对路径,如file=/etc/passwd,但RequestDispatcher通常只处理相对上下文路径或绝对路径(以/开头,但仍在应用上下文内),直接根目录绝对路径可能被安全策略拦截或解析失败,但相对路径穿越更通用。
4.2 进阶利用:读取Web应用自身配置文件
对于攻击者而言,读取系统文件固然好,但读取应用自身的配置文件往往能获得更直接的收益。让我们尝试读取该Web应用的web.xml文件。
http://localhost:8080/vuln-app/?file=../WEB-INF/web.xml解析:
- 当前上下文是
/vuln-app。 ../WEB-INF/web.xml会跳转到vuln-app的上级目录webapps,然后寻找WEB-INF/web.xml?不对!- 这里有个关键点:
WEB-INF是一个受Tomcat保护的特殊目录。客户端无法直接通过URL访问其下的任何文件。但是,通过服务器端的请求分发(RequestDispatcher)是可以访问到的! - 正确的路径是:
/WEB-INF/web.xml(绝对路径,相对于应用上下文)。或者,因为我们的漏洞页面就在应用根目录,所以WEB-INF/web.xml(相对路径)也可以。让我们试试:
http://localhost:8080/vuln-app/?file=WEB-INF/web.xml你会发现,成功读取到了我们之前编写的web.xml的内容。如果这个文件里定义了数据库连接池参数,那么数据库用户名和密码就泄露了。
重要注意事项:
WEB-INF和META-INF目录是Java Web应用的安全边界。RequestDispatcher可以访问它们,但外部请求不能。这意味著,一个LFI漏洞赋予了攻击者“内部视角”,这是非常危险的。
4.3 利用漏洞获取源码(.java/.class)
假设我们的应用有一个Servlet,编译后的class文件位于WEB-INF/classes/com/example/MyServlet.class。攻击者虽然无法直接下载.class文件,但可以通过LFI让服务器将其内容作为文本或二进制流包含到响应中。由于class文件是二进制,直接包含可能会显示乱码,但通过一些技巧(如利用某些编码或错误页面的差异),可以推断信息。
更致命的是,如果服务器配置了JSP文件未预编译即暴露源码(某些老旧或错误配置),攻击者可能通过包含.jsp文件直接看到源码。例如,尝试包含自身:
http://localhost:8080/vuln-app/?file=index.jsp这通常会执行JSP,而不是显示源码。但在特定条件下(如请求.jsp文件时加上某些特殊参数,或在某些中间件配置下),可能会以源码形式返回。
4.4 从LFI到RCE的尝试
这是文件包含漏洞的“终极形态”。在PHP中,常结合php://input等包装器实现。在Java中,直接通过LFI执行代码比较困难,但并非不可能,通常需要结合其他漏洞:
- 结合文件上传:这是最常见的链。如果网站同时存在文件上传漏洞,允许上传JSP文件(或可被Tomcat解析的恶意文件),并且攻击者通过LFI知道了上传文件的绝对路径,那么就可以直接包含该上传文件,从而执行任意代码。
- 利用服务器特定文件:包含服务器上已有的、内容部分可控的文件。例如,包含Tomcat的日志文件(
../logs/localhost_access_log.*.txt),如果攻击者能将恶意代码(如JSP标签)注入到User-Agent或URL中(需要URL编码),那么日志里就会记录这些代码。再通过LFI包含这个日志文件,Tomcat可能会将其作为JSP解析执行。这种方法对Tomcat版本和配置非常敏感,成功率不高,但理论存在。 - 利用
/proc目录(Linux):在Linux系统上,/proc/self/environ文件包含了当前进程的环境变量。如果环境变量中有用户可控的部分(在某些CGI模式下可能),或许能注入代码。但这同样非常苛刻。
演示一个概念性尝试(通常失败,但用于理解思路):假设我们能让Tomcat将我们的请求参数记录到日志,并且日志文件可被包含。我们构造一个特殊的请求:
http://localhost:8080/vuln-app/?file=../logs/localhost_access_log.2024-12-01.txt&<%Runtime.getRuntime().exec("touch /tmp/pwned");%>然后访问这个URL。即使失败,这个过程也展示了攻击者的思路:寻找一切可能将代码写入服务器文件系统,再通过LFI去触发它。
5. 漏洞挖掘与排查技巧实录
在实际的安全评估中,你不太可能遇到一个如此明显的、把参数名直接叫file的漏洞。更多时候,你需要去挖掘和判断。
5.1 如何寻找文件包含点
- 参数分析:关注所有可能表示文件、页面、模板、模块的请求参数。常见参数名有:
file,page,path,template,module,include,load,document,view,f等。 - 功能推测:在网站中寻找“动态加载内容”的功能。比如,一个页面有多个标签页,切换时URL参数变化;或者有“下载”、“预览”功能,可能会涉及文件路径。
- 错误信息:尝试在参数中输入一些特殊值(如
../../../../),观察服务器的错误响应。如果错误信息中提到了“文件未找到”、“路径错误”等,而不是“参数非法”,那这里就可能存在路径遍历或文件包含。 - 模糊测试(Fuzzing):使用工具(如Burp Suite的Intruder)或自定义字典,对目标参数进行大量路径遍历payload的测试。字典应包含各种深度和不同操作系统的路径模式。
5.2 手工测试Payload库
以下是我积累的一些常用测试payload,你可以根据实际情况调整:
- 基础路径遍历:
../../../../etc/passwd....//....//....//....//etc/passwd(双重编码或特殊绕过)/etc/passwd(绝对路径)file:///etc/passwd(如果URL处理逻辑支持file协议)
- Web应用相关:
WEB-INF/web.xml../WEB-INF/web.xmlWEB-INF/classes/application.properties(Spring Boot配置)index.jsp(尝试读取源码)
- Windows系统:
..\..\..\..\windows\win.ini(使用反斜杠)../../../../windows/system32/drivers/etc/hosts
- 日志文件:
../logs/catalina.out../logs/localhost_access_log.%日期%.txt../../apache-tomcat-9.0.85/logs/localhost_access_log.2024-12-01.txt
5.3 常见拦截与绕过技巧
现代WAF(Web应用防火墙)和安全框架会对路径遍历进行过滤。以下是一些可能的绕过姿势:
- 编码绕过:
- URL编码:
..%2f..%2f..%2f..%2fetc%2fpasswd(将/编码为%2f) - 双重URL编码:
..%252f..%252f..%252f..%252fetc%252fpasswd - Unicode编码:在某些解析场景下可能有效。
- URL编码:
- 特殊字符绕过:
- 使用
....//或..\/等变体。 - 在路径末尾添加空字节
%00(空字节截断,在老旧Java版本或特定场景下可能有效,用于截断文件扩展名)。例如:../../../etc/passwd%00.jpg,如果代码是拼接.jpg后缀的话。
- 使用
- 路径标准化绕过:有些过滤器只检查
../,但不会递归检查。..././...//经过路径标准化后可能变成../。 - 协议包装器绕过(Java中较少见):如果应用逻辑直接使用
new FileInputStream(param)等低级API,且未对输入做协议检查,理论上可能使用file://、http://等。但在Servlet的RequestDispatcher中,通常不支持这些协议。
排查技巧实录:当你发现一个疑似包含点但被拦截时,不要轻易放弃。首先,用最简单的../../测试,看返回什么错误。是403、400,还是500?错误信息是否透露了后端技术(如Java堆栈跟踪)?然后,尝试将payload放在不同的参数位置,或者使用POST请求。有时,防护只针对GET请求。记录下所有不同的响应,这能帮你推测后端过滤逻辑的弱点。
6. 防御方案与安全开发实践
知道了怎么攻击,更重要的是知道如何防御。修复一个文件包含漏洞,通常比修复一个SQL注入要复杂一些,因为它涉及路径安全、输入验证和业务逻辑多个层面。
6.1 输入验证与白名单机制
最有效、最根本的防御措施。不要试图用黑名单过滤../、绝对路径等,总有绕过的方法。
- 方案:为
file、page这类参数建立一个严格的白名单。只允许包含预定义的、安全的文件。// 安全代码示例 String requestedPage = request.getParameter("page"); Map<String, String> allowedPages = new HashMap<>(); allowedPages.put("home", "/templates/home.jsp"); allowedPages.put("news", "/templates/news.jsp"); allowedPages.put("about", "/templates/about.jsp"); String safePagePath = allowedPages.get(requestedPage); if (safePagePath != null) { request.getRequestDispatcher(safePagePath).include(request, response); } else { // 记录非法访问日志,并返回404或错误页面 response.sendError(HttpServletResponse.SC_NOT_FOUND); } - 为什么有效:攻击者无法提供白名单之外的任何值,从根本上杜绝了路径遍历。
6.2 路径规范化与校验
如果业务上确实需要动态包含文件(比如一个CMS系统),白名单不现实,那么必须进行严格的路径校验。
- 方案:
- 获取规范路径:使用
java.io.File的getCanonicalPath()方法,获取参数对应的绝对、规范、唯一的文件路径。 - 定义安全基准目录:明确指定允许包含的文件所在的根目录(例如
/var/www/app/templates)。 - 校验是否在基准目录内:检查规范化的路径是否以基准目录的规范路径开头。
// 安全代码示例 String baseDir = "/var/www/app/templates"; // 允许访问的根目录 String userInput = request.getParameter("template"); File base = new File(baseDir); File requestedFile = new File(base, userInput); // 将用户输入视为相对于baseDir try { String canonicalBasePath = base.getCanonicalPath(); String canonicalRequestedPath = requestedFile.getCanonicalPath(); // 关键校验:请求的文件路径必须在基准目录之下 if (canonicalRequestedPath.startsWith(canonicalBasePath + File.separator)) { // 安全,可以包含 request.getRequestDispatcher(userInput).forward(request, response); } else { throw new SecurityException("路径遍历攻击尝试: " + userInput); } } catch (IOException e) { // 处理异常 response.sendError(HttpServletResponse.SC_BAD_REQUEST); } - 获取规范路径:使用
- 实操心得:一定要用
startsWith(canonicalBasePath + File.separator),而不是startsWith(canonicalBasePath)。因为如果canonicalBasePath是/var/www/app/templates,攻击者输入../../../etc/passwd,经过new File(base, input)和getCanonicalPath()后,得到的路径可能是/etc/passwd,它不以/var/www/app/templates开头,校验失败。但如果攻击者输入templates/../../../../etc/passwd,规范化后可能是/var/www/app/etc/passwd,它以/var/www/app/templates开头吗?不,它是以/var/www/app开头,所以仍然失败。但如果我们只用startsWith(canonicalBasePath),那么/var/www/app/templates_evil(如果存在)可能会被误判为合法。加上File.separator确保了子目录关系。
6.3 服务器与容器层加固
- 运行Tomcat的用户权限最小化:不要用
root用户运行Tomcat。创建一个专用的、低权限的用户(如tomcat),并确保它只能访问必要的目录(如webapps,logs,temp)。这样即使LFI成功,能读取的文件范围也大大受限。 - 设置Tomcat的
SecurityManager:启用Java SecurityManager,并配置严格的安全策略文件,限制代码对文件系统、网络等资源的访问。这对于生产环境是很好的实践,但配置较为复杂。 - 及时更新:保持Tomcat和JDK版本更新,及时修复已知的公开漏洞。虽然我们复现的是逻辑漏洞,但保持基础环境安全是底线。
- 审查第三方库:确保应用中使用的所有第三方库(如Apache Commons FileUpload, Spring框架等)都是最新安全版本,它们本身也可能存在路径遍历或相关漏洞。
6.4 安全开发生命周期(SDL)集成
将文件包含漏洞的防范意识融入开发流程:
- 安全编码规范:在团队规范中明确禁止未经校验的动态文件包含操作。
- 代码审计:在代码审查阶段,重点关注所有涉及文件路径拼接、动态加载资源的地方。
- 自动化扫描:在CI/CD流水线中集成静态应用安全测试(SAST)工具,如SonarQube、Checkmarx等,它们可以自动识别潜在的路径遍历漏洞模式。
- 渗透测试:定期对应用进行黑盒/白盒渗透测试,主动寻找此类漏洞。
文件包含漏洞就像系统的一道暗门,看起来不起眼,却可能通往核心地带。理解它的原理、掌握复现和利用的方法,最终是为了更好地关上这扇门。在安全的世界里,攻击者的视角是最好的防御教材。希望这篇详细的拆解,能让你下次在写代码时,对那个小小的request.getParameter多一份警惕。
