1. 项目概述一次对经典漏洞的深度剖析最近在整理一些历史漏洞的实战笔记翻到了2017年那个经典的Tomcat任意文件上传漏洞CVE-2017-12615。这个漏洞的官方描述听起来挺简单在特定配置下攻击者可以通过HTTP PUT方法上传一个JSP文件到服务器进而执行任意代码。但真正有意思的地方在于当这个漏洞发生在Windows服务器上时情况就变得微妙起来。官方补丁出来后安全社区很快就发现了多种绕过补丁限制的方法而这些方法的核心恰恰是巧妙利用了Windows操作系统本身的一些“特性”。这让我觉得单纯复现一个漏洞的POC概念验证意义有限真正有价值的是理解攻击者是如何“见招拆招”的以及我们作为防御方又该如何构建更深层次的防御。今天我就结合自己当年分析这个漏洞和后续绕过手法的经验来详细拆解一下在Windows环境下针对CVE-2017-12615的三种典型绕过姿势。这不仅仅是漏洞复现更是一次对Windows文件系统特性、Web容器安全机制和攻防对抗思维的深度探索。2. 漏洞原理与核心配置解析2.1 CVE-2017-12615的根源PUT方法与静态资源处理要理解绕过必须先吃透漏洞本身。Apache Tomcat在7.0.0到7.0.81版本中默认配置存在一个安全隐患。简单来说Tomcat提供了一个叫做DefaultServlet的组件它负责处理静态资源比如HTML、图片文件的请求。这个Servlet支持HTTP的多种方法其中就包括PUT用于上传资源和DELETE用于删除资源。在conf/web.xml配置文件中关于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 ... /servlet servlet-mapping servlet-namedefault/servlet-name url-pattern//url-pattern /servlet-mapping注意readonly这个初始化参数默认值是true。当readonly为true时DefaultServlet会拒绝处理PUT和DELETE请求这是安全的行为。漏洞产生的第一个必要条件就是有人手动或通过某些部署脚本、管理界面将这个readonly参数设置为了false。这使得Tomcat允许通过PUT方法上传文件。但光允许上传还不够为什么能上传JSP文件并执行呢这里涉及到Tomcat的请求处理流程。当一个请求例如PUT /test.jsp进来时Tomcat会先根据URL路径寻找匹配的Servlet。对于.jsp或.jspx结尾的请求默认会由JspServlet来处理它的映射模式通常是*.jsp和*.jspx。JspServlet的设计是处理动态内容它本身并不支持PUT方法。按照设计一个.jsp文件的PUT请求应该被JspServlet拒绝。漏洞的核心逻辑缺陷就出现在这里在受影响的Tomcat版本中存在一个请求分发逻辑的瑕疵。在某些情况下特别是当请求路径经过规范化处理后对于PUT /test.jsp这样的请求Tomcat错误地将其路由给了支持PUT方法的DefaultServlet而不是本应处理它的JspServlet。于是DefaultServlet便忠实地将请求体内容作为文件内容写入到了服务器的对应路径如webapps/ROOT/test.jsp。一旦这个JSP文件被成功写入后续任何用户通过GET或POST请求访问这个文件时它就会被JspServlet正常解析并执行其中的Java代码从而造成远程代码执行RCE。注意这个漏洞的触发有严格的版本和配置限制。首先Tomcat版本需在7.0.0至7.0.81之间或8.5.0至8.5.20对应CVE-2017-12617。其次必须显式配置readonly为false。默认安装的Tomcat是安全的。但在实际生产环境中一些运维人员或应用程序为了支持RESTful API的文件上传功能可能会修改此配置从而引入风险。2.2 官方补丁与防御思路拦截与校验Apache官方在修复CVE-2017-12615时采取的策略简单直接在DefaultServlet处理PUT和DELETE请求的逻辑中加入了对请求路径的校验。补丁代码的核心是增加了一个isPutAllowed或类似的检查函数它会判断请求的目标资源是否是一个JSP或JSPX文件通过检查文件名后缀。如果是则直接拒绝该PUT请求。补丁的伪代码逻辑类似于protected void doPut(HttpServletRequest request, HttpServletResponse response) { String path getRelativePath(request); // 新增校验如果请求路径以“.jsp”、“.jspx”、“.jspf”等结尾则返回403禁止 if (isJspFile(path)) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } // 原有的文件写入逻辑... }这个修复思路属于“黑名单”机制旨在从入口处阻断攻击者直接上传JSP文件。然而安全攻防的常态就是“道高一尺魔高一丈”。攻击者很快发现在Windows系统上可以利用其文件系统路径处理的多个特性来巧妙地绕过这个后缀名检查。3. Windows特性利用三种绕过姿势详解Windows和Linux/Unix在文件系统路径处理上存在诸多差异这些差异在平时开发中可能无关紧要但在安全攻防的上下文中就成了关键的突破口。下面我们逐一拆解三种经典的绕过方法。3.1 绕过姿势一利用Windows文件名尾随空格与点号这是最广为人知的一种绕过方式直接利用了Windows文件系统的一个古老特性自动去除文件名末尾的点和空格。原理剖析 在Windows的NTFS文件系统中为了兼容古老的MS-DOS和8.3文件名格式当创建一个新文件时系统会自动去除文件名末尾的点和空格。例如你尝试创建一个名为test.jsp.末尾带点或test.jsp末尾带空格的文件实际在磁盘上创建的文件名就是test.jsp。这个特性在图形界面和命令行中都被保留了下来。攻击利用 攻击者构造一个特殊的PUT请求其路径不是简单的/shell.jsp而是/shell.jsp.末尾加点或/shell.jsp末尾加空格需要URL编码为%20。PUT /shell.jsp.%20 HTTP/1.1 Host: vulnerable-host:8080 Content-Type: application/x-www-form-urlencoded Content-Length: 100 % out.println(Hello, Hacked by CVE-2017-12615); %请求阶段Tomcat的DefaultServlet接收到路径/shell.jsp.。它执行补丁中的校验函数isJspFile(path)。这个函数通常使用String.endsWith(.jsp)之类的逻辑。由于路径以.jsp.结尾并不以.jsp结尾因此校验通过文件写入阶段DefaultServlet尝试将请求体内容写入到文件shell.jsp.。它调用Java的File相关API。在Windows系统上当Java运行时环境JRE通过本地接口调用操作系统API创建文件时Windows内核的文件系统驱动会执行“规范化”操作自动去掉末尾的点和空格。于是磁盘上实际创建的文件名是shell.jsp。访问执行阶段攻击者再访问GET /shell.jsp。此时该请求由JspServlet处理因为它匹配*.jsp模式。JspServlet找到磁盘上的shell.jsp文件编译并执行其中的JSP代码攻击成功。实操心得在测试这种绕过时Burp Suite等工具是必备的。需要注意空格在HTTP请求中需要编码为%20而点号.则可以直接使用。另外这种绕过仅对Windows系统有效。在Linux上shell.jsp.就是一个合法的、以点结尾的文件名不会被截断因此后续无法通过/shell.jsp访问到。3.2 绕过姿势二利用Windows路径分隔符转换第二种方法更为巧妙利用了Java路径处理与Windows系统特性之间的一个细微差异。原理剖析 在URL和Unix-like系统中路径分隔符是正斜杠/。而在Windows系统中路径分隔符是反斜杠\。Java作为跨平台语言其File类和相关API会处理这个差异。当在Windows上使用一个包含/的路径创建File对象时Java内部可能会进行转换。关键在于Tomcat补丁中的路径校验和实际文件创建可能发生在处理流程的不同阶段并且对路径字符串的处理方式可能不一致。攻击利用 攻击者构造一个包含/的请求路径例如/shell.jsp/。PUT /shell.jsp/ HTTP/1.1 Host: vulnerable-host:8080 ...请求与校验阶段Tomcat接收到路径/shell.jsp/。校验函数isJspFile(path)判断路径是否以.jsp结尾。由于路径以/结尾校验不通过/shell.jsp/.endsWith(“.jsp”) false因此请求被放行。路径处理与文件创建阶段DefaultServlet需要将URL路径转换为服务器上的文件系统路径。它可能使用类似new File(baseDir, requestPath)的代码。在Windows上当这个包含/的路径被传递给底层的Windows文件系统API时会发生一些有趣的事情。在某些版本的Tomcat或特定的JDK/Windows组合下路径末尾的/可能被解释或处理导致最终创建的文件名仍然是shell.jsp。一种常见的情况是/shell.jsp/被当作一个目录路径但在某些逻辑下写入文件内容时系统实际上在shell.jsp作为一个文件中写入了数据。另一种情况与下面的“::$DATA”流结合使用。更深层的利用结合NTFS数据流在Windows NTFS文件系统中有一个特性叫“交替数据流”Alternate Data Streams, ADS。文件的主数据流没有名字但可以通过filename:streamname的格式指定其他数据流。其中::$DATA表示默认的、未命名的数据流。 攻击者可以构造这样的请求PUT /shell.jsp::\$DATA HTTP/1.1$需要URL编码为%24。PUT /shell.jsp%3a%3a%24DATA HTTP/1.1 Host: vulnerable-host:8080 ...这里:被编码为%3a。Tomcat的校验逻辑看到的是shell.jsp%3a%3a%24DATA不以.jsp结尾。但当Java在Windows上创建文件时:是NTFS流分隔符。shell.jsp::$DATA指示系统将数据写入shell.jsp文件的默认数据流。最终磁盘上生成的就是shell.jsp文件。注意事项这种绕过方式对环境依赖较强成功率不如第一种。它高度依赖于Tomcat版本、JDK版本以及Windows系统如何处理这些特殊路径。在测试时需要多次尝试不同的变体如/shell.jsp/./、/shell.jsp\反斜杠需编码为%5c等。ADS流的方式在一些配置严格的服务器上可能因为权限或策略问题失败。3.3 绕过姿势三利用Windows短文件名8.3格式特性这是一种相对“古老”但依然可能有效的技巧利用了NTFS文件系统为长文件名自动生成的短文件名8.3格式兼容特性。原理剖析 NTFS为了兼容旧的16位应用程序会为长文件名自动生成一个符合8.3格式最多8个字符的主文件名点号最多3个字符的扩展名的短文件名。例如文件longfilename.jsp可能对应的短文件名为LONGFI~1.JSP。这个短文件名可以通过dir /x命令在命令行查看。攻击利用 假设攻击者已经通过某种方式可能是其他漏洞或者服务器上已存在知道web根目录下存在一个名为upload.jsp的文件。那么这个文件很可能有一个短文件名比如UPLOAD~1.JSP。 攻击者可以尝试PUT这个短文件名PUT /UPLOAD~1.JSP HTTP/1.1 Host: vulnerable-host:8080 ...校验阶段Tomcat的校验函数检查路径/UPLOAD~1.JSP。它是否以.jsp结尾是的.JSP不区分大小写匹配。那么这个请求会被补丁拦截吗这取决于补丁的实现。如果补丁只是简单地进行后缀名字符串匹配那么.JSP是匹配的请求会被拒绝。但是如果补丁的实现存在缺陷例如在比较前进行了小写转换但短文件名的大小写格式特殊或者攻击者利用其他方式先让校验逻辑失效那么有可能绕过。更常见的利用场景短文件名绕过往往不是直接用于上传而是用于覆盖已存在的文件。如果服务器上已经有一个合法的image.jpg文件其短文件名可能是IMAGE~1.JPG。攻击者构造PUT /IMAGE~1.JPG请求可能会绕过一些基于长文件名的内容类型检查或后缀检查如果检查逻辑不严谨从而覆盖掉原来的图片文件注入恶意内容。虽然JPG文件不会被当作JSP执行但可能造成破坏或用于其他攻击链如钓鱼。短文件名探测 在利用前攻击者通常需要先探测存在的短文件名。这可以通过发送一系列带有波浪号~的请求并观察服务器响应差异来实现例如访问/WEB-INF~1/、/UPLOAD~1.JSP等如果文件或目录存在响应码可能是404Not Found或403Forbidden而不存在的短文件名可能返回400Bad Request或其他错误。这是一种信息收集手段。避坑技巧对于防御方来说彻底禁用Windows服务器的8.3短文件名功能是一个有效的加固措施。可以通过在命令行需管理员权限执行fsutil behavior set disable8dot3 1来全局禁用或在特定卷上执行fsutil 8dot3name set D: 1禁用D盘。禁用后需要重启服务器。这不仅能提高一点性能还能消除此类攻击面。4. 漏洞复现与深度测试环境搭建理解了原理和绕过手法我们还需要一个环境来亲手验证。这里我分享一个使用Docker快速搭建靶场的方法它比直接在物理机安装旧版Tomcat更安全、更便捷。4.1 基于Docker的靶场搭建我们使用一个包含漏洞版本Tomcat和故意错误配置的Docker镜像。# 1. 搜索可用的漏洞环境镜像以Linux为例Windows需安装Docker Desktop docker search cve-2017-12615 # 通常会找到类似 vulhub/tomcat:7.0.81 的镜像 # 2. 拉取镜像 docker pull vulhub/tomcat:7.0.81 # 3. 运行容器将容器的8080端口映射到本地的8080端口 docker run -d -p 8080:8080 --name tomcat-cve vulhub/tomcat:7.0.81 # 4. 查看容器日志确认启动成功 docker logs tomcat-cve这个镜像通常已经将readonly设置为false并部署在Windows容器环境不这里有个关键点上述绕过手法严重依赖Windows特性。而大多数公开的Docker漏洞镜像如vulhub是基于Linux的。在Linux容器里姿势一末尾空格/点和姿势二路径分隔符/ADS流是无效的因为Linux文件系统行为不同。因此为了完整测试Windows绕过我们需要一个Windows Server Core的Docker环境并在其中手动安装配置有漏洞的Tomcat。步骤更复杂准备Windows Docker主机你需要一台运行Windows 10/11专业版/企业版或Windows Server 2016及以上版本的机器并安装Docker Desktop切换到Windows容器模式或Docker Engine。获取Tomcat安装包下载Tomcat 7.0.81的Windows安装包zip版apache-tomcat-7.0.81-windows-x64.zip。编写Dockerfile# 使用Windows Server Core基础镜像 FROM mcr.microsoft.com/windows/servercore:ltsc2019 # 设置工作目录 WORKDIR C:\\app # 将Tomcat ZIP包复制到镜像中需提前放在Dockerfile同目录 COPY apache-tomcat-7.0.81-windows-x64.zip . # 安装Java这里以OpenJDK 8为例需自行下载msi或使用其他方式 # 假设已下载OpenJDK的zip包并复制到镜像 COPY openjdk-8uXXX-windows-x64.zip . RUN powershell -Command Expand-Archive -Path openjdk-8uXXX-windows-x64.zip -DestinationPath C:\\java RUN setx /M JAVA_HOME C:\\java\\jdk-XXX RUN setx /M PATH %PATH%;%JAVA_HOME%\\bin # 解压Tomcat RUN powershell -Command Expand-Archive -Path apache-tomcat-7.0.81-windows-x64.zip -DestinationPath C:\\tomcat # 修改Tomcat配置启用PUT方法 # 找到 C:\tomcat\conf\web.xml修改DefaultServlet的readonly参数为false RUN powershell -Command ((Get-Content C:\\tomcat\\conf\\web.xml) -replace param-namereadonly/param-name\s*param-valuetrue/param-value, param-namereadonly/param-nameparam-valuefalse/param-value) | Set-Content C:\\tomcat\\conf\\web.xml # 暴露端口 EXPOSE 8080 # 启动Tomcat CMD [C:\\tomcat\\bin\\catalina.bat, run]构建并运行镜像docker build -t vulnerable-tomcat-win . docker run -d -p 8080:8080 --name my-vuln-tomcat vulnerable-tomcat-win重要提示在真实测试中手动构建Windows镜像过程繁琐。更常见的做法是直接在一台Windows虚拟机如Windows Server 2012 R2上安装Tomcat 7.0.81并进行配置修改这样更接近真实攻击环境。4.2 使用Burp Suite进行漏洞验证与绕过测试环境就绪后我们使用Burp Suite作为攻击工具。基础漏洞验证配置Burp代理浏览器访问http://your-target:8080。拦截任意请求发送到Repeater模块。将请求方法改为PUT路径设为/test.jsp body中写入简单的JSP代码% out.println(Vulnerable!); %发送请求。如果返回201 Created或204 No Content说明上传成功前提是readonlyfalse已配置。然后访问GET /test.jsp如果页面显示“Vulnerable!”则基础漏洞存在。绕过测试1尾随点在Repeater中修改PUT请求路径为/bypass1.jsp.。发送请求观察状态码。成功的话是201。访问GET /bypass1.jsp不带点。如果执行成功说明绕过生效。绕过测试2尾随空格修改PUT请求路径为/bypass2.jsp%20注意空格编码。发送请求。访问GET /bypass2.jsp。绕过测试3NTFS数据流修改PUT请求路径为/bypass3.jsp%3a%3a%24DATA::$DATA的URL编码。发送请求。注意观察响应有时可能返回403如果补丁拦截了包含特殊字符的路径或500错误。尝试访问GET /bypass3.jsp。测试结果分析表测试用例请求路径预期结果WindowsLinux环境结果说明基础漏洞/test.jsp被补丁拦截 (403)被补丁拦截 (403)直接上传JSP被补丁阻止绕过1-尾随点/shell.jsp.上传成功 (201) - 访问/shell.jsp可执行上传成功但文件名为shell.jsp.访问/shell.jsp404Windows特性利用成功绕过1-尾随空格/shell.jsp%20上传成功 (201) - 访问/shell.jsp可执行上传成功文件名为shell.jsp访问需带空格Windows特性利用成功绕过2-路径斜杠/shell.jsp/可能成功 (201) 或 被作为目录处理通常被创建为目录shell.jsp/上传失败成功率依赖环境绕过2-NTFS流/shell.jsp::$DATA可能成功但易被WAF或补丁拦截特殊字符无效Linux不支持ADS依赖具体防护规则绕过3-短文件名/EXISTI~1.JSP可能覆盖已存在文件或绕过校验无效Linux无此特性需先探测短文件名5. 防御加固与实战排查指南作为防御方了解攻击手法是为了更好地防护。针对CVE-2017-12615及其绕过我们需要采取多层次、纵深防御的策略。5.1 根本性解决方案升级与安全配置立即升级Tomcat这是最彻底的方法。将Tomcat升级到官方修复后的版本7.0.82及以上8.5.22及以上9.0.0.M21及以上。升级前务必做好备份和测试。检查并修正配置审查所有Tomcat实例的conf/web.xml文件确保所有DefaultServlet的readonly初始化参数值为true。这是默认值任何将其改为false的行为都必须经过严格的安全评审。init-param param-namereadonly/param-name param-valuetrue/param-value !-- 确保这里是true -- /init-param禁用不必要的HTTP方法在conf/web.xml中可以显式地限制DefaultServlet只允许GET、HEAD、POST等安全方法。servlet-mapping servlet-namedefault/servlet-name url-pattern//url-pattern /servlet-mapping !-- 添加安全约束 -- security-constraint web-resource-collection web-resource-nameRestricted Methods/web-resource-name url-pattern//url-pattern http-methodPUT/http-method http-methodDELETE/http-method !-- 可以添加其他危险方法如TRACE -- /web-resource-collection auth-constraint / /security-constraint这样配置后即使readonly被误设为falsePUT和DELETE请求也会在更早的阶段被容器拒绝返回403。5.2 针对Windows环境的强化措施禁用8.3短文件名在Windows服务器上如前所述使用fsutil命令全局或在特定磁盘上禁用短文件名生成。这是一项重要的系统加固措施。应用程序层过滤如果业务确实需要文件上传功能不应依赖DefaultServlet。应该开发专门的、安全的文件上传Servlet或使用成熟框架的组件。在上传逻辑中必须进行严格的检查白名单校验文件扩展名只允许上传图片、文档等静态资源后缀如.jpg, .png, .pdf绝对禁止.jsp, .jspx, .php, .asp等可执行脚本后缀。重命名文件使用随机字符串如UUID重命名上传的文件避免用户控制文件名。限制上传目录将文件上传到Web根目录以外的路径并通过程序动态读取返回防止直接通过URL访问执行。检查文件内容对于图片等文件可以进行二次渲染或头信息检查防止文件头伪装。部署Web应用防火墙WAF在Tomcat前端部署WAF可以配置规则拦截包含::$DATA、尾随点和空格、波浪号等可疑特征的PUT请求。规则示例检测请求路径是否匹配正则表达式.*\.jsp[\s\.].*尾随点或空格。检测请求路径是否包含:或|等Windows特殊字符需URL解码后检测。严格限制PUT方法的使用范围到特定的、安全的URL路径。5.3 安全监控与应急响应日志审计开启Tomcat的访问日志access_log并定期审计所有PUT请求。重点关注返回状态码为201Created的PUT请求检查其上传的文件路径和文件类型。文件系统监控在Web应用目录如webapps/ROOT部署文件完整性监控FIM工具。任何新增的.jsp、.jspx文件都应触发高优先级告警。入侵检测在服务器层面部署HIDS主机入侵检测系统监控catalina.home目录下JSP文件的创建、修改行为。应急响应流程一旦发现可疑文件上传立即隔离服务器或暂停Tomcat服务。备份可疑文件、相关日志和系统内存镜像用于后续分析。删除恶意文件。根据日志追溯攻击源IP、攻击时间、利用手法。全面检查服务器是否已被植入后门、存在其他漏洞。修复漏洞升级或修改配置并验证后再恢复服务。6. 从漏洞看安全开发与运维的思考CVE-2017-12615及其绕过案例给我们上了一堂生动的安全课。它暴露的不仅仅是Tomcat的一个代码缺陷更是多层防御理念缺失的问题。安全开发层面默认安全Tomcat将readonly默认设为true是正确的。任何偏离默认安全配置的行为都必须有充分的理由和严格的控制。深度防御官方的第一次补丁只做了简单的后缀名黑名单校验这属于单点防御很容易被绕过。安全的校验应该在多个层次进行URL路由层、文件系统操作层、甚至操作系统调用层。例如在最终调用FileOutputStream写入之前再次校验最终的文件路径名。跨平台兼容性陷阱开发跨平台应用时必须充分考虑不同操作系统Windows/Linux/macOS在文件系统、路径处理、大小写敏感度等方面的差异。这些差异往往是安全漏洞的温床。处理文件路径时应使用规范的API如Path.normalize()并明确制定命名规范。安全运维层面最小权限原则运行Tomcat的账户应遵循最小权限原则仅拥有其所需目录的读写权限避免使用SYSTEM或Administrator等高权限账户。定期漏洞扫描与更新建立规范的漏洞扫描和补丁管理流程。对于Apache Tomcat这类核心中间件应订阅其安全公告并及时评估、测试、部署安全更新。配置即代码与审计将服务器配置如web.xml纳入版本管理。任何变更都需要经过申请、评审、审计的流程。定期进行配置合规性检查确保没有偏离安全基线的配置存在。这个漏洞虽然已过去多年但其中蕴含的攻防思想——利用系统特性绕过表层防御——至今仍在不断上演。作为技术人员我们研究历史漏洞不是为了攻击而是为了构建更坚固的防御。每一次对漏洞的深入分析都是对我们自身系统安全设计和运维能力的一次审视和提升。在实战中永远不要假设攻击者只会按照说明书来攻击他们总会寻找那些在规范之外、却在系统特性之中的“捷径”。