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审计的思路其实要从文档入手。