Java Web安全实战:从路径穿越漏洞到WEB-INF敏感文件读取
1. 从一道CTF题看Java Web安全基础最近在复盘一些经典的CTF题目发现很多朋友对Java Web相关的安全挑战感到头疼尤其是那些看起来“简单”的题目往往藏着不少细节。今天我们就来深度拆解一道名为“Easy Java”的题目它来自RoarCTF 2019。别被名字骗了“Easy”可能只是出题人的调侃这道题涉及的知识点非常经典是理解Java Web安全特别是文件读取和路径穿越漏洞的绝佳案例。通过这道题我们不仅能学会如何解题更能深入理解Java Web应用中一些常见的安全配置误区、请求处理机制以及如何利用这些机制进行渗透测试。无论你是CTF新手还是想巩固Java安全基础的开发者这篇文章都会带你走一遍完整的思考、测试和利用过程。2. 题目环境初探与信息收集拿到一个Web题目第一步永远是信息收集。对于Java Web应用我们首先得搞清楚它用的是什么框架、部署在什么容器上、有哪些明显的接口。2.1 访问基础页面与功能点分析启动题目提供的Docker环境或访问目标地址后我们通常首先看到一个登录页面。很多新手会直接去尝试SQL注入或者弱口令但这道题的入口可能不在这里。一个关键的习惯是查看网页源代码和检查所有的前端请求。在登录页面我们可能会发现一些注释掉的提示或者引用了不常见的JS文件、CSS文件。更重要的是要查看robots.txt、crossdomain.xml等文件或者尝试访问一些常见的路径比如/admin、/manage、/api等。对于Java应用一个特别有用的信息收集点是WEB-INF目录。这个目录通常位于Web应用的根目录下里面存放了Java Web应用的核心配置文件web.xml、编译后的类文件.class以及库文件lib/。正常情况下WEB-INF目录是被服务器保护的客户端无法直接通过HTTP请求访问其中的内容。但是如果存在某些配置不当或特定的漏洞攻击者就有可能读取到其中的敏感文件从而获取源码、数据库配置甚至逻辑漏洞。2.2 利用报错信息探测应用结构在测试过程中故意触发一些错误是常用的手段。例如尝试访问一个不存在的页面/xxx或者向已知接口传递非法参数。Java应用尤其是开启了调试模式或使用了默认错误页的可能会返回包含堆栈跟踪Stack Trace的详细错误信息。这些信息极其宝贵可能会泄露使用的框架如Spring MVC, Struts2的版本信息。绝对路径错误信息中可能包含服务器上文件系统的绝对路径如/usr/local/tomcat/webapps/ROOT/...。类名和方法名暴露了后端代码的结构。在这道“Easy Java”题中一个经典的突破口就是通过构造特殊的请求让应用返回错误进而发现一个可以用于文件读取的接口或参数。很多解题Writeup会提到一个/Download接口这就是通过信息收集或测试发现的。3. 核心漏洞任意文件读取与路径穿越这道题的核心漏洞点通常是一个文件下载或文件读取功能该功能没有对用户输入的文件路径进行严格的过滤和校验导致了任意文件读取Arbitrary File Read和目录穿越Directory Traversal。3.1 漏洞原理与请求构造假设我们发现了这样一个接口http://target.com/Download?filenameexample.jpg。它的功能是根据filename参数读取服务器上的某个文件如图片并返回给用户。安全的实现应该将filename参数限制在某个特定的、安全的目录内如/static/images/并且禁止使用../这样的路径穿越符。不安全的实现可能是这样的伪代码String filename request.getParameter(filename); File file new File(/webapp/static/ filename); // ... 读取文件并输出如果攻击者将filename参数设置为../../../etc/passwd那么拼接后的路径就变成了/webapp/static/../../../etc/passwd经过操作系统路径解析后就指向了系统的/etc/passwd文件从而实现了对敏感系统文件的读取。这就是目录穿越攻击。在Java中路径分隔符是正斜杠/在Windows上也可以是反斜杠\。因此常见的Payload包括../../../etc/passwd....//....//....//etc/passwd(双重编码或特殊绕过)WEB-INF/web.xml(如果当前目录就是Web根目录的上级或同级)3.2 针对WEB-INF目录的利用对于Java Web应用最诱人的目标就是WEB-INF/web.xml文件。这个文件是应用的部署描述符包含了Servlet、Filter、Listener等配置。读取到它我们就能知道应用有哪些关键的处理类进而可能去读取这些类的.class文件。但是直接请求/WEB-INF/web.xml是会被容器如Tomcat拦截的。我们需要找到一个“通道”让应用服务器以“数据”的形式把这个文件的内容吐给我们而不是去“执行”它。上面提到的存在路径穿越漏洞的文件下载接口就是这样一个通道。关键步骤通常如下定位漏洞点通过信息收集找到可能存在文件读取功能的端点例如/Download、/file、/read等。测试路径穿越尝试使用../../来跳出当前目录。例如/Download?filename../../WEB-INF/web.xml。确定Web根目录有时需要多次尝试../的个数。一个技巧是先读取一个已知的、可通过正常URL访问的文件比如/favicon.ico通过报错或已知路径来推算Web应用的绝对路径从而确定需要多少层../才能回到Web根目录。假设Web应用部署在/usr/local/tomcat/webapps/ROOT下载Servlet的基目录是/usr/local/tomcat/webapps/ROOT/download/。那么要读取/usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml就需要这样的路径../../WEB-INF/web.xml从download目录向上退两层。4. 从web.xml到Flag的完整攻击链成功读取WEB-INF/web.xml只是第一步我们需要从中提取进一步利用的信息。4.1 分析web.xml获取关键类名打开读取到的web.xml我们会看到类似这样的配置servlet servlet-nameDownloadServlet/servlet-name servlet-classcom.roarctf.web.DownloadServlet/servlet-class /servlet servlet-mapping servlet-nameDownloadServlet/servlet-name url-pattern/Download/url-pattern /servlet-mapping servlet servlet-nameFlagController/servlet-name servlet-classcom.roarctf.web.FlagController/servlet-class /servlet servlet-mapping servlet-nameFlagController/servlet-name url-pattern/flag/url-pattern /servlet-mapping这里我们发现了两个关键信息我们正在利用的/Download接口对应的处理类是com.roarctf.web.DownloadServlet。存在一个名为FlagController的类映射到了/flag路径。这很可能就是获取flag的关键入口。4.2 利用漏洞读取Java类文件知道了类的全限定名Fully Qualified Name我们就可以尝试去读取这个类的字节码文件.class。在Java Web应用中编译后的类文件通常位于WEB-INF/classes/目录下其路径由包名决定。例如类com.roarctf.web.FlagController对应的.class文件路径就是WEB-INF/classes/com/roarctf/web/FlagController.class。我们可以再次利用那个存在路径穿越的文件下载接口来读取它/Download?filename../../WEB-INF/classes/com/roarctf/web/FlagController.class服务器会返回这个二进制.class文件。我们无法直接阅读二进制内容需要下一步操作。4.3 反编译.class文件分析源码拿到.class文件后我们需要使用Java反编译工具将其还原成可读的Java源代码。常用的工具有JD-GUI、CFR、FernFlower等。这里以JD-GUI为例它是一个带有图形界面的工具使用非常方便。将下载下来的FlagController.class文件用JD-GUI打开我们可能会看到类似如下的代码package com.roarctf.web; import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class FlagController extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String flag ROARCTF{This_Is_A_Fake_Flag}; // 真实的flag可能从数据库、环境变量或特定文件中读取 // String realFlag System.getenv(FLAG); // 或者 // String realFlag readFromFile(/flag.txt); response.getWriter().println(flag); } }从反编译的代码中我们直接看到了一个硬编码的假Flag。显然真正的Flag不会这么简单。我们需要仔细分析代码逻辑Flag是否从数据库查询可能需要SQL注入Flag是否从服务器上的某个特定文件读取可能需要利用刚才的任意文件读漏洞去读另一个文件Flag是否来自请求的某个特定参数或头可能需要构造特殊的请求代码中是否有条件判断比如需要特定的Referer、Cookie或者POST请求假设我们分析源码后发现真正的Flag是从一个名为/flag的系统文件注意这里是服务器操作系统根目录下的文件不是Web路径中读取的并且FlagController的doGet方法会读取它并返回。但是直接访问/flag接口可能还需要一个特定的密钥key参数。4.4 构造最终Payload获取Flag结合所有信息我们最终的利用链可能是这样的第一步信息收集。发现/Download接口。第二步漏洞利用。利用/Download接口的路径穿越漏洞读取WEB-INF/web.xml发现FlagController类。第三步源码审计。利用同一漏洞读取FlagController.class反编译后分析逻辑。发现需要访问/flag接口并且需要传递一个名为key的参数其值被硬编码在DownloadServlet类中。第四步获取密钥。再次利用任意文件读读取WEB-INF/classes/com/roarctf/web/DownloadServlet.class反编译找到硬编码的key值例如secret_key_123。第五步访问Flag接口。构造请求GET /flag?keysecret_key_123。最终服务器返回真正的FlagROARCTF{Real_Flag_Here}。5. 漏洞的深层成因与修复方案这道题虽然“解”开了但作为开发者我们更应该思考漏洞产生的根本原因以及如何修复。5.1 漏洞根因分析未验证的用户输入DownloadServlet直接信任了来自客户端的filename参数并将其拼接进文件路径这是最主要的原因。路径规范化缺失在拼接路径后没有对最终路径进行“规范化”canonicalize处理。规范化会解析掉.当前目录和..上级目录符号并检查最终路径是否在允许的目录范围内。Java中可以使用File.getCanonicalPath()方法。白名单机制缺失对于文件下载功能最安全的做法是维护一个允许下载的文件名白名单如id到filename的映射或者只允许访问某个严格限制的子目录并通过白名单校验文件后缀。5.2 安全的修复代码示例以下是一个修复后的DownloadServlet的doGet方法示例protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String fileId request.getParameter(id); // 改用id而非文件名 if (fileId null || fileId.isEmpty()) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, Missing file id); return; } // 1. 白名单映射将安全的id映射到实际安全的文件名 MapString, String allowedFiles new HashMap(); allowedFiles.put(1, welcome.pdf); allowedFiles.put(2, guide.jpg); // ... 从数据库或配置中加载 String safeFileName allowedFiles.get(fileId); if (safeFileName null) { response.sendError(HttpServletResponse.SC_NOT_FOUND, File not found); return; } // 2. 定义安全的基目录最好配置在外部如properties文件 String baseDir getServletContext().getRealPath(/) downloads/; File base new File(baseDir); // 3. 构造目标文件对象 File targetFile new File(base, safeFileName); // 4. 关键步骤获取规范路径并检查是否逃逸出了基目录 String canonicalBasePath base.getCanonicalPath(); String canonicalTargetPath targetFile.getCanonicalPath(); if (!canonicalTargetPath.startsWith(canonicalBasePath File.separator)) { // 路径穿越攻击目标文件不在基目录下。 response.sendError(HttpServletResponse.SC_FORBIDDEN, Access denied); return; } // 5. 检查文件是否存在且是普通文件非目录、符号链接等 if (!targetFile.isFile()) { response.sendError(HttpServletResponse.SC_NOT_FOUND, File not found); return; } // 6. 安全的文件传输设置Content-Type, Content-Disposition等 // ... 省略具体传输代码 }修复要点解析参数名变更将filename改为id避免直接传递路径。白名单机制使用Map建立安全ID与安全文件名的映射从根本上杜绝任意文件名输入。规范路径检查使用getCanonicalPath()获取文件和基目录的绝对规范路径并强制检查目标文件路径是否以基目录路径开头。这是防御路径穿越最核心的一步。注意这里在比较时加上了File.separator是为了防止目录名巧合造成的绕过例如基目录是/app/static攻击者试图访问/app/static_secret文件。文件存在性检查使用isFile()确保目标是一个普通文件而不是目录或符号链接符号链接也可能用于穿越。5.3 其他防护建议最小权限原则运行Java应用的服务账号不应有对系统敏感目录如/etc,/root的读取权限。安全配置在web.xml或框架配置中确保没有将WEB-INF、META-INF等目录暴露为静态资源目录。输入验证对所有用户输入进行严格的验证和过滤不仅限于文件名还包括所有来自请求的参数、头、Cookie等。错误信息处理在生产环境中应配置自定义错误页面避免将详细的堆栈跟踪和系统路径信息泄露给客户端。6. 举一反三Java Web中的其他常见安全问题通过“Easy Java”这道题我们掌握了任意文件读取和路径穿越。在真实的Java Web安全评估中还有更多需要注意的点6.1 不安全的反序列化如果应用接收并反序列化来自客户端的对象例如通过RMI、HTTP请求中的序列化数据并且类路径中包含有危险利用链的库如Apache Commons Collections, Groovy, Spring等攻击者可以构造恶意序列化数据在服务器上执行任意代码。防御方法包括升级依赖库版本、使用白名单限制可反序列化的类、使用安全的替代方案如JSON。6.2 表达式语言注入EL Injection在JSP页面中如果未对用户输入进行过滤就直接放入${}表达式中执行可能导致表达式语言注入。例如c:out value${param.userInput}/是安全的但如果开发者错误地使用了${param.userInput}直接执行而userInput是.class.forName(java.lang.Runtime).getRuntime().exec(calc)就会造成命令执行。防御的关键是避免使用JspContext.findValue()等动态解析不可信输入的方法并对所有渲染到页面的数据进行正确的编码或过滤。6.3 不安全的XML解析XXE如果应用使用DocumentBuilderFactory、SAXParserFactory等解析外部传入的XML数据且未禁用外部实体External Entity引用就可能存在XXE漏洞。攻击者可以通过构造恶意XML读取服务器上的任意文件、发起内部网络请求甚至导致拒绝服务。修复方法是在解析XML前显式配置工厂属性禁用DTD和外部实体。DocumentBuilderFactory dbf DocumentBuilderFactory.newInstance(); dbf.setFeature(http://apache.org/xml/features/disallow-doctype-decl, true); dbf.setFeature(http://xml.org/sax/features/external-general-entities, false); dbf.setFeature(http://xml.org/sax/features/external-parameter-entities, false);6.4 日志注入与敏感信息泄露在记录日志时如果直接将用户输入如用户名、请求参数写入日志文件而未进行适当的清理可能会造成日志注入Log Injection或敏感信息泄露。例如用户输入中包含换行符\n可能伪造新的日志条目或者将密码等敏感信息误记入日志。应确保日志内容经过适当的格式化或过滤并对日志文件的访问权限进行严格控制。7. 实战中的工具与排查技巧在实战渗透测试或CTF比赛中除了手动测试熟练使用工具能极大提高效率。7.1 信息收集与目录扫描浏览器开发者工具F12永远是第一线工具查看网络请求、源代码、Cookie、本地存储。Burp Suite / OWASP ZAP代理工具用于拦截、查看、重放和修改所有HTTP/HTTPS请求是手工测试的核心。dirsearch / gobuster / ffuf目录和文件暴力破解工具用于发现隐藏的接口、备份文件如.git,.bak,.swp、配置文件等。WhatWeb / WappalyzerWeb应用指纹识别工具快速识别网站使用的技术栈如Java, Tomcat 8.5, Spring Boot。7.2 针对Java应用的专项测试寻找WEB-INF可以尝试的路径包括/WEB-INF/web.xml、/WEB-INF/classes/、/WEB-INF/lib/。虽然直接访问通常被禁止但通过像本题这样的间接漏洞可能读取到。测试已知漏洞根据识别出的框架版本如Spring Boot 1.x, Struts 2.3.x使用公开的EXP或扫描器如nmap脚本 metasploit模块进行测试。反编译工具JD-GUI图形化方便、CFR命令行反编译能力强、FernFlowerIntelliJ IDEA内置引擎效果优秀。拿到.class或.jar文件后多尝试几个工具因为不同工具对混淆代码的处理能力不同。7.3 漏洞利用的思维模式由点到面发现一个可疑点如一个报错、一个非常规参数不要轻易放过思考它可能关联的整个功能模块。参数污染对每一个看到的参数都尝试进行边界测试空值、超长字符串、特殊字符../,..\,%00,,、数组参数param[]a、JSON/XML格式注入等。关注“副作用”一个功能点可能主要目的安全但其“副作用”或错误处理流程存在漏洞。例如文件上传功能可能检查了后缀但上传失败时的错误信息却泄露了绝对路径。代码与配置审计思维即使黑盒测试也要在脑子里“白盒化”。看到/Download?filexxx就想象后端可能是new File(BASE_DIR filename)。这种思维能帮你更快地构造出有效的Payload。回过头看“Easy Java”这道题它就像一把钥匙打开了Java Web安全基础的大门。它串联起了信息收集、漏洞探测、路径穿越、源码获取、代码审计这一套完整的渗透测试流程。在真实环境中漏洞可能更隐蔽防护措施可能更多层但基本的原理和思考方式是相通的。理解每一层防御为何存在、如何被绕过才是从“解题”到“懂安全”的关键。下次遇到Java Web的题目或实际应用不妨先从这些基础点入手或许会有意想不到的发现。

相关新闻