过程
在3月4日早上10点,突然接到问题反馈,预发环境网关返回接口500。
于是登录预发环境网关服务查看,发现网关上报错如下:
java.lang.IllegalArgumentException:a header value must not end with '\r' or '\n':uid=0(root) gid=0(root) groups=0(root)
以为是普通的服务异常,是返回了异常的ResponseHeader所致,询问了前端表示没有做响应头相关的变更。
因为当时有功能需要在预发环境下验收测试,为了不耽误进度,选择直接重启网关服务,服务重启之后,恢复正常。
由于并没有发现问题的关键所在,而此时gateway正常启动,没有了错误日志。我决定再观察一段时间。
这里有个疑点: 这个异常提示Header value非法的提示内容非常奇怪,经过搜索与测试之后发现header value竟然是一条linux命令的结果。
这个时候我还没有联想到是安全方面的问题。只是留了一个心眼。
时间到了当天下午三点同样的问题在预发环境下再次出现,这次我在登录应用日志之后,发现了新的报错:
查了一下这个ip地址是美国。突然意识到可能受到了网络攻击。
再查看堆栈,发现异常来源于org.springframework.expression.spel包.
上网查询了一下发现SPEL是Spring提供的表达式语言,可以在代码中运行表达式,甚至是调用Java的类库,这里就是调用了Runtime.exec()
方法来执行系统命令。于是上午的异常堆栈也解释通了。
那么攻击者究竟是如何通过spel来实现远程代码执行的呢?
在网络上搜索 spel + spring cloud gateway + 执行漏洞
得到了一个报告。发现我们使用的gateway版本3.0.3正好在范围之内。
于是按照报告的建议,关闭了预发环境的SpringCloudGateway的Actuator模块。重启服务之后,不再出现异常。
原理
在后续的问题溯源过程中发现了文章
记录如下:
首先:StandardEvaluationContext类可以任意执行#{}
之内的spel表达式。
代码执行的漏洞的执行点找到之后,是什么行为导致这个方法被执行呢?
查看方法调用栈可以看到是由于路由刷新的导致的。
当CachingRouteLocator
收到RefreshRoutesEvent
(路由刷新事件)之后,
- 就会调用
RouteDefinitionRouteLocator.getRoutes()
方法获取RouteDefinition
(路由定义)列表。 - 进而遍历每个
RouteDefinition
,调用convertToRoute(RouteDefinition)
,将RouteDefinition
对象转换成Route
对象,并交给CachineRouteLocator
。在转换的过程中,需要将路由定义过程中的filters拿出来构建真正的过滤器。 - 在
RouteDefinitionRouteLocator.loadGatewayFilters()
方法中的this.configurationService.with(factory)
会构建出ConfigurableBuilder
这里使用了链式调用,name()方法用来记录filter定义名称,properties()记录定义过滤器是传入的自定义参数。bind()方法就是解析路由定义的过滤器中的”args”的方法。
在调用ConfigurableBuilder.normalizeProperties()
方法时,就会调用shortcutType().normalize()
方法来将可能存在的Spel表达式一般化,最终导致了spel表达式被调用。
总结
总结一下,在调用Actuator接口的refresh方式后,gateway的CashingRouteLocator类在接收到刷新事件之后,会重新加载所有缓存的路由定义,重新生成route对象,并将其重新缓存起来达到刷新路由的效果。
其中从RouteDefinition对象解析到Route对象的过程中,系统会将路由定义中可能的spel表达式进行一般化,这就给了代码的远程执行提供了可能。
看看spring cloud gateway v3.1.0是怎么解决这个漏洞的:将原本的StandardEvaluationContext。替换成了GatewayEvaluationContext。
教训
1、Spring Cloud Actuator模块是一个用来监控和管理springboot应用的模块,在加载Actuator模块后,子模块可以通过自定义端点 实现监控和管理(使用Actuator的端点注解将自己的管理与监控节点加载到/actuator路径之下)。 特别说明,spring cloud gateway使用Actuator自定义端点实现了路由的新增和刷新, 所以就算没有本次漏铜,直接将一个拥有这样权限的路径暴露在公网中,本身就是十分危险的。 准确的来说,将任何一个不需要身份验证的服务直接暴露在公网上都是危险的行为。
2、本来Spring Boot提供了spring-security组件来提供安全服务,所以一旦我们的服务需要使用Actuator来达到管理与监控的目的,我们必须也要加载spring-security模块来保证应用安全,除非这个服务是公网不可达的。