Spring Expression Language 是一种强大的表达式语言,在spring开发中经常用到。正是因为其功能足够完备,导致在某些情况下用户可能会从外部注入spel表达式,在服务器上执行非常规操作行为。
基本语法
SPEL主要有三种用法:在@Value注解中使用、在XML中配置、在代码块中使用Expression。在漏洞挖掘中一般关注在代码块中使用Expression的这种形式。前两种是在大部分情况下是不可控的。
常见的spel漏洞场景如下:
1 | //创建解析器 |
或者是使用虚拟容器的场景:
1 | StandardEvaluationContext ctx = new StandardEvaluationContext(); |
不论是哪一种场景都绕不开调用getValue或者setValue方法来触发spel表达式。就从这点来看的话,其实和ognl表达式注入漏洞很像,都是将解析表达式和执行表达式分开,先解析后执行。最后也是通过getValue或者setValue执行表达式。
1、界定符
SPEL使用#{.....}作为界定符,其中的内容被认定为是spel表达式。可以在表达式中引用其他对象、对象中的属性以及调用对象的方法。
1 | 引用其他对象:#{obj} |
2、赋值表达式
SPEl允许自定义变量,通过#符号实现变量声明,如:#aaa='2222',就在上下文环境中声明一个变量名为aaa的变量。
3、类相关表达式
在SPEL表达式中使用T(Type)方法来表示类的实例,这里的Type必须是类的全名,只有在java.lang包中的类可以直接声明。使用类相关的表达式可以调用类中的静态方法和静态属性。
1 | T(java.lang.Runtime) //引用java.lang.Runtime对象 |
除了使用T(Type)方法之外,SPEL还支持new关键字,也就是说可以在SPEL表达式中使用new关键字实例化一个对象。但是也有一个前提条件,除了基类和基础数据结构之外都必须使用完全限定的类名。
SPEL工作流程简述:
1.定义解析器ExpressionParser,在spring中默认是SpelExpressionParser。
2.使用ExpressionParser接口的parseExpression方法来解析表达式得到Expression对象。
3.定义上下文对象,SPEL使用EvaluationContext接口来表示上下文对象,这一步是可选的。
4.最后根据表达式进行一个求值的行为。
SPEL主要接口:
ExpressionParser:解析器接口
EvaluationContext:上下文环境接口
Expression:表达式对象接口
这里还需要提一下EvaluationContext接口,SPEL中提供两种EvaluationContext接口,分别是SimpleEvaluationContext和StandardEvaluationContext。二者的区别在于:
1 | SimpleEvaluationContext:针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别,公开SpEL语言特性和配置选项的子集。 |
简单的说就是SimpleEvaluationContext的功能远远少于StandardEvaluationContext,其不支持java类型引用、构造函数和bean的引用。可以将其视为一个自带的沙盒,正是因为其的纯粹性,就导致很多漏洞的修复方式就是将上下文环境接口改为SimpleEvaluationContext。
漏洞分析
从github上拉取gs-messaging-stomp-websocket项目进行调试分析。修改app.js文件中的connect函数如下:
1 | function connect() { |
启动服务点击Connect建立websocket连接。从代码层面我们可以看到调用到spring-messaging-5.0.4.RELEASE-sources.jar!/org/springframework/messaging/support/ExecutorSubscribableChannel.java文件中的run方法,并且获取到我们从前端传入的headers数据。

跟入this.messageHandler.handleMessage方法:

跟入this.subscriptionRegistry.registerSubscription方法:

继续跟入addSubscriptionInternal方法:

至于为什么要按照这种步骤走流程,其实也简单,就是因为我们可控参数在变量message中,那就自然需要跟踪message的走向。继续步入addSubscriptionInternal方法:

到这里就是重头戏了,在这个函数中很明显有解析SPEL表达式的行为,这里的解析器接口就是Spring默认的SpelExpressionParser。前提就是需要在传入的headers中存在selector变量才会将其视为SPEL表达式并解析,这也是我们先前修改app.js的原因。到这里已经完成了SPEL表达式注入漏洞挖掘的第一步:寻找SPEL表达式可控场景。但是如果仅仅只是表达式解析的话是远远不够的,我们还需要找到一个场景使得解析表达式生成的Expression对象调用getValue方法才能执行SPEL表达式。因此我们继续跟踪message变量,步入addSubscription方法:

继续跟入info.addSubscription方法:

步入subs.add方法:

终于!我们看到生成的Expression被赋值给this.selectorExpression属性。到这里貌似是不符合我们寻找getValue方法。但是我们在浏览器里发起的是websocket连接,这就表示这次存储的变量并不会立刻消失,并且很有可能在接下来的请求中用到。我们尝试从浏览器中随便发送一些字符到服务器端:

这里的调用栈特别长,前面主要是在实现send这一行为,我们直接定位到getMessageHandler方法。这里的message就是我们在建立websocket请求之后,再次请求时发送的一些信息,跟入this.messageHandler.handleMessage:

跟入handleMessageInternal方法:

继续跟踪message走向,步入sendMessageToSubscribers:

跟入this.subscriptionRegistry.findSubscriptions方法:

接下来连续步入findSubscriptionsInternal中的filterSubscriptions方法:

从这张图中就能看到存在一个SPEL表达式执行的利用点,也就是说如果expression可控,那么这里就存在漏洞。我们回到203行的info.getSubscription方法,通过这个方法获取刚才建立连接时的数据,这里就包括我们之前在app.js中修改的selector。继续向下走到207行的sub.getSelectorExpression方法,跟入:

在这里取出我们在建立websocket时所设置的selectorExpression,也就是SPEL表达式。再次回到filterSubscriptions方法,向下走:

这里就看得更加清晰,expression是我们传入的SPEL表达式,而context是标准的StandardEvaluationContext,实现全套SPEL语言功能。至此SPEL表达式注入漏洞产生!!
官方的修复方案是将上下文环境接口StandardEvaluationContext替换为SimpleEvaluationContext,保证其在一个相对安全的环境中实现SPEL的安全使用。
小结
通过这个漏洞分析使得我对于java审计思路这一块有了一些启发。我越来越觉得,在java中漏洞的发掘其本质并不是漏洞本身而是功能和场景的不当结合。我认为这些漏洞的本质都是合适的功能点在不合适的场景下的不当使用所产生的。回头看这个漏洞,设计初衷并没错,确实需要通过SPEL来解析selector,是一个合理的功能点。那么如何从一个合理的功能点入手寻找到不合理的利用方式,就是一个代码审计人员应该做的。
整个漏洞的挖掘难点就在于,如何能够找到这个selector的输入点呢?其实有两种方法,一方面通读组件源码,另一方面就是参考官方文档。前者成本太高,还是乖乖阅读文档来的轻松。这个漏洞的方法调用栈其实很深,如果单纯从代码的角度去发掘,难度极大。这时候找到一个合适的场景就显得尤为重要。这也侧面体现java审计的思路其实要从文档入手。