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?filenamexxx.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.xmlWeb应用部署描述文件是核心配置文件定义了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?filenamehelp.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是filenameWEB-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工具或浏览器直接访问构造的URLcurl 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-valueflag{config_password_here}/param-value作为某个Servlet或Filter的名称虽然不常见。藏在文件末尾或某个不起眼的配置项里。3.4 扩展利用读取Class文件与源码泄露拿到web.xml后解题可能就结束了。但作为一次完整的学习我们可以思考更深层次的利用。web.xml里定义了Servlet和其对应的处理类。例如servlet servlet-nameLoginServlet/servlet-name servlet-classcom.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将用户输入的参数与列表比对只返回匹配的文件。MapString, 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/environLinux环境变量、应用日志文件等进行信息收集。复现这道“Easy Java”题目绝不仅仅是为了得到一个Flag。它像一把钥匙打开了Java Web应用安全中“访问控制”和“输入校验”这两扇最基础也最重要的大门。理解了路径穿越你就能举一反三在遇到文件上传、文件包含、模板注入等其他漏洞时拥有更敏锐的嗅觉。下次当你看到任何一个由用户输入控制的文件路径参数时心里都应该立刻响起警报这里会不会是下一个“Easy Java”