Java Web路径穿越漏洞实战:从WEB-INF泄露到安全防御
1. 项目概述:一次典型的Java Web路径穿越漏洞实战复盘
最近在整理过去的CTF解题笔记,翻到了这道来自RoarCTF 2019的“Easy Java”。这道题在当年算是一个经典的Java Web路径穿越漏洞案例,它巧妙地利用了Java Web应用对文件下载功能的不安全实现,结合对WEB-INF目录结构的理解,来获取敏感的配置文件。很多刚接触Java Web安全的同学,可能对WEB-INF这个目录的“神圣不可侵犯”性有误解,觉得它被容器保护得很好,这道题就是一个很好的“祛魅”过程。它不涉及复杂的框架漏洞或反序列化,核心就是最基础的路径遍历(Path Traversal)和对Web应用标准目录结构的认知。通过复现这道题,我们能清晰地看到,一个看似简单的文件下载接口,如果缺乏有效的输入校验和路径控制,会带来多么严重的信息泄露风险。无论你是正在打CTF的新手,还是想巩固Web安全基础的开发者,这个案例都值得深入琢磨一下。
2. 漏洞原理与场景深度拆解
2.1 核心漏洞:不受控的文件路径参数
这道题目的场景非常明确:一个提供了文件下载功能的Java Web应用。通常,前端会有一个列表或链接,点击后触发类似/download?filename=xxx.pdf的请求,后端则根据这个filename参数去服务器特定目录(比如/files)读取文件并返回。
漏洞的根源就在于,这个filename参数完全由用户控制,并且后端程序在拼接文件路径时,没有进行任何规范化(Canonicalization)和校验。攻击者可以注入包含路径遍历序列(如../)的字符串。
例如,假设后端代码是这样写的(概念性代码):
String basePath = "/var/www/app/uploads/"; String filename = request.getParameter("filename"); File file = new File(basePath + filename); // ... 读取文件并输出给用户如果用户传入filename=../../../etc/passwd,那么最终拼接的路径就变成了/var/www/app/uploads/../../../etc/passwd,经过系统路径解析后,就等价于/etc/passwd。这就实现了跨越应用目录,读取服务器上任意文件的目的。
注意:在Unix-like系统和Windows系统上,路径遍历的表示方法略有不同(
../vs..\),但原理一致。Java的File类会处理这些序列。
2.2 关键目标:WEB-INF目录与web.xml
在标准的Java Web应用(遵循Servlet规范)中,WEB-INF是一个位于应用根目录下的特殊目录。它的特殊性在于:
- 客户端不可直接访问:Servlet容器(如Tomcat, Jetty)会阻止任何直接来自客户端的对
WEB-INF和META-INF目录下资源的请求。你无法通过http://target.com/app/WEB-INF/web.xml直接访问到它。 - 存放核心配置与代码:
WEB-INF目录下通常包含:web.xml:Web应用部署描述文件,是核心配置文件,定义了Servlet、Filter、Listener等。classes/:存放编译后的Java类文件(.class)。lib/:存放应用依赖的JAR包。
正因为客户端无法直接访问,一些开发者会误以为其中的文件是绝对安全的。然而,如果存在上述的路径穿越漏洞,并且应用本身有权限读取这些文件,那么WEB-INF的“保护”就形同虚设了。
web.xml文件是本题的“Flag”所在。它里面可能包含数据库连接信息、敏感接口路径、甚至是后端的逻辑密码(在这道CTF题中,flag很可能就以注释或某个参数值的形式藏在里面)。通过路径穿越读取web.xml,是Java Web安全信息收集中非常经典的一步。
2.3 场景还原:题目可能的界面与交互
根据“Easy Java”这个名称和常见出题套路,我们可以推测题目环境可能提供了一个非常简单的Web界面。也许是一个“帮助”页面,里面提了一下有个文件下载功能;或者更直接,页面上就有一个输入框,写着“输入文件名下载”,旁边放个“Download”按钮。
初始尝试可能是下载一个已知的、正常的文件,比如help.pdf或readme.txt,以确认下载功能正常工作。然后,攻击的思路便转向:我能否利用这个功能,去读取下载目录之外的文件?特别是那个受保护的WEB-INF/web.xml文件。
这里就引出一个关键问题:我们知道目标文件是WEB-INF/web.xml,但我们从哪个起点开始穿越呢?我们需要知道文件下载功能设置的基准目录(basePath)在哪里。是应用根目录?还是某个子目录?这通常需要一些探测或猜测。
3. 解题步骤与实操过程详解
3.1 信息收集与功能探测
第一步永远是观察。访问目标网址,查看页面源码、JavaScript文件、以及可能的注释。题目可能在前端给出提示,比如注释里写着“下载功能仅供下载/doc目录下的文件”。
更重要的步骤是直接测试下载接口。通过浏览器开发者工具的Network面板,观察点击下载按钮时发出的请求。假设我们捕获到的请求是:
GET /download?filename=help.doc HTTP/1.1这就确认了接口路径和参数名。
手动修改参数进行测试:
- 测试基础遍历:尝试
filename=../../../。观察响应。如果是目录列表泄露,可能会返回错误信息或403/500状态码。如果服务器配置了默认索引页,可能返回200但内容是索引页。最理想的情况是返回一个包含WEB-INF的目录列表。 - 测试绝对路径:有些情况下,如果后端直接使用
filename参数创建File对象,甚至可能支持绝对路径(如filename=/etc/passwd),但这道Java题大概率不支持。
3.2 构造路径穿越Payload
这是最核心的一步。我们需要找到从下载功能的基准目录到WEB-INF/web.xml的相对路径。
在Java Web应用中,一个Servlet的当前工作目录(ServletContext的根路径)通常是Web应用的根目录。如果下载Servlet没有特意设置basePath,而是直接使用filename,那么new File(filename)就会相对于应用根目录(或者说是Servlet容器的当前工作目录,但通常是应用根目录)来寻找文件。
一个经典的Payload是:filename=WEB-INF/web.xml如果下载Servlet的路径解析是相对于应用根目录的,那么这个请求就会直接尝试读取应用根目录下的WEB-INF/web.xml文件。
但更常见的情况是,下载功能被限制在某个子目录,比如/files或/downloads。这时,我们就需要向上回退。
假设基准目录是/var/www/tomcat/webapps/ctfapp/downloads/,而web.xml在/var/www/tomcat/webapps/ctfapp/WEB-INF/web.xml。 那么,从downloads目录回到应用根目录,需要../。再从根目录进入WEB-INF,所以完整的相对路径是:../WEB-INF/web.xml。
因此,Payload尝试顺序通常是:
WEB-INF/web.xml(直接读取)../WEB-INF/web.xml(回退一层)../../WEB-INF/web.xml(回退两层)../../../WEB-INF/web.xml(回退三层)
在实战或CTF中,可能需要多次尝试。你可以通过不断添加../来向上遍历,直到读到文件或返回错误。
3.3 利用漏洞读取web.xml
当我们构造出正确的Payload,比如filename=../../../WEB-INF/web.xml(假设需要回退三层),并向/download接口发起请求时,如果漏洞存在且路径正确,服务器就不会返回一个文件下载,而是直接将web.xml的内容输出到HTTP响应体中。
实操记录: 使用curl工具或浏览器直接访问构造的URL:
curl 'http://target-ctf-server:port/download?filename=../../../WEB-INF/web.xml'或者使用Burp Suite的Repeater模块,手动修改并重放请求。
成功的响应特征:
- 状态码:通常是200 OK。
- 响应头:
Content-Type可能是application/xml、text/xml或者甚至是application/octet-stream(如果后端没有正确设置MIME类型)。 - 响应体:直接就是
web.xml文件的XML格式内容。
这时,你需要仔细查看这个XML文件的内容。Flag可能以以下几种形式存在:
- 作为注释:
<!-- flag{this_is_the_flag} --> - 作为某个初始化参数的值:
<param-value>flag{config_password_here}</param-value> - 作为某个Servlet或Filter的名称:虽然不常见。
- 藏在文件末尾或某个不起眼的配置项里。
3.4 扩展利用:读取Class文件与源码泄露
拿到web.xml后,解题可能就结束了。但作为一次完整的学习,我们可以思考更深层次的利用。web.xml里定义了Servlet和其对应的处理类。例如:
<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.ctfapp.servlet.LoginServlet</servlet-class> </servlet>我们知道了处理登录的类文件是com.ctfapp.servlet.LoginServlet。这个类文件编译后位于WEB-INF/classes/目录下,对应的路径是WEB-INF/classes/com/ctfapp/servlet/LoginServlet.class。
既然我们已经有了路径穿越漏洞,我们完全可以尝试去读取这个.class文件:filename=../../../WEB-INF/classes/com/ctfapp/servlet/LoginServlet.class
.class文件是字节码,我们可以使用反编译工具(如JD-GUI、CFR、FernFlower)将其还原成Java源代码。在真实的渗透测试或更复杂的CTF题中,这可能会泄露关键的业务逻辑、加密算法、硬编码的密钥等,为进一步攻击(如逻辑漏洞、反序列化)铺平道路。
4. 漏洞挖掘与防御的深层思考
4.1 为什么这种漏洞会发生?
从开发角度,原因无非以下几点:
- 安全意识不足:开发者认为文件名参数是前端可控的,或者只会在预设列表中选择,忽视了用户可直接修改HTTP请求。
- 对Java File API的误解:认为
new File()只会在当前目录下操作,或者不了解路径遍历序列的威力。 - 缺乏输入校验:没有对用户输入的
filename参数进行“白名单”校验(只允许特定文件名)或“规范化+校验”处理。 - 框架误用:可能使用了某些框架的便捷方法,但没有仔细阅读文档,不知道这些方法可能存在安全风险。
4.2 如何防御路径穿越漏洞?
防御的核心原则是:永远不要信任用户输入,对文件路径进行严格的白名单控制。
白名单校验:这是最有效的方法。如果下载功能只允许下载少数几个已知文件,那么直接维护一个允许的文件名列表(Map),将用户输入的参数与列表比对,只返回匹配的文件。
Map<String, String> allowedFiles = new HashMap<>(); allowedFiles.put("guide", "/secure/path/guide.pdf"); allowedFiles.put("help", "/secure/path/help.doc"); String fileKey = request.getParameter("key"); // 不用filename了,用key String realPath = allowedFiles.get(fileKey); if (realPath == null) { // 返回错误,文件不存在 return; } File file = new File(realPath);路径规范化与校验:如果必须支持一定动态性,则:
- 规范化路径:使用
File.getCanonicalPath()或Path.normalize().toAbsolutePath()来获取规范化的绝对路径。 - 校验路径前缀:确保规范化后的路径,是以你允许的基准目录(
BASE_DIR)开头的。
String userInput = request.getParameter("filename"); // 定义允许的基准目录 File baseDir = new File("/var/www/app/safe_download_area"); // 构造用户请求的文件 File requestedFile = new File(baseDir, userInput); // 获取规范路径 String canonicalPath = requestedFile.getCanonicalPath(); // 检查规范路径是否以基准目录的规范路径开头 if (!canonicalPath.startsWith(baseDir.getCanonicalPath() + File.separator)) { // 路径穿越尝试!拒绝请求。 throw new IllegalArgumentException("Invalid file path."); } // 安全,可以读取文件- 规范化路径:使用
使用资源ID而非路径:在数据库中存储文件,前端通过文件ID(如UUID)来请求下载,后端根据ID从数据库或安全存储中获取文件流。
Web服务器配置:在Nginx或Apache层面,可以配置规则阻止请求中包含
../的URL。
4.3 CTF中的变体与进阶思考
在更复杂的题目中,出题人可能会设置一些障碍:
- 过滤
../:后端代码可能用replaceAll("\\.\\./", "")或replace("\\.\\./", "")来过滤../。但可能存在双写绕过(....//过滤一次后变成../)或使用URL编码(%2e%2e%2f)绕过。 - 强制添加后缀:后端可能自动为输入添加
.pdf后缀。这时需要利用%00(空字节截断)或路径参数(filename=../../../WEB-INF/web.xml%00),但在高版本JDK和Servlet容器中,空字节截断通常已失效。另一种思路是考虑目录遍历后,目标文件本身是否需要后缀。 - 读取其他敏感文件:除了
web.xml,还可以尝试读取WEB-INF/classes/下的.class文件、/etc/passwd、/proc/self/environ(Linux环境变量)、应用日志文件等,进行信息收集。
复现这道“Easy Java”题目,绝不仅仅是为了得到一个Flag。它像一把钥匙,打开了Java Web应用安全中“访问控制”和“输入校验”这两扇最基础也最重要的大门。理解了路径穿越,你就能举一反三,在遇到文件上传、文件包含、模板注入等其他漏洞时,拥有更敏锐的嗅觉。下次当你看到任何一个由用户输入控制的文件路径参数时,心里都应该立刻响起警报:这里,会不会是下一个“Easy Java”?
