作者:Hu3sky@360CERT
Spring Messaging 属于Spring Framework项目,其定义了Enterprise Integration Patterns典型实现的接口及相关的支持(注解,接口的简单默认实现等)
https://pivotal.io/security/cve-2018-1270
Spring versions 5.0.x - 5.0.5 versions 4.3.x - 4.3.16 SpringBoot 2.0.0及以下
Spring Framework允许应用程序通过spring-messaging模块通过简单的内存STOMP代理通过WebSocket端点公开STOMP。恶意用户(或攻击者)可以向代理发送消息,这可能导致远程执行代码攻击。
复现环境
https://repo.spring.io/release/org/springframework/spring/5.0.0.RELEASE/
spel是Spring Expression Language 即,spring表达式语言,是一个支持查询和操作运行时对象导航图功能的强大的表达式语言.支持以下功能
文字表达式 布尔和关系运算符 正则表达式 类表达式 访问 properties, arrays, lists, maps 方法调用 关系运算符 参数 调用构造函数 Bean引用 构造Array 内嵌lists 内嵌maps 三元运算符 变量 用户定义的函数 集合投影 集合筛选 模板表达式
一个hello world
import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; public class Spel { public static void main(String[] args) { ExpressionParser parser = new SpelExpressionParser(); String var1 = (String)parser.parseExpression("'Hello World'").getValue(); int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue(); System.out.println(maxValue); System.out.println(var1); } }
弹calc
代码
String calc = (String)parser.parseExpression("T(Runtime).getRuntime().exec('open /Applications/Calculator.app/')").getValue();
这里的T是类型操作符,可以从类路径加载指定类名称(全限定名)所对应的 Class 的实例,格式为:T(全限定类名),效果等同于 ClassLoader#loadClass()
由于HTTP具有单向通信的特点,于是造成了Server向Client推送消息变得很难,需要使用轮询的方式,于是有了WebSocket,他支持双向通信,类似于聊天室模式,在这个会话里,Server和Clinet都能发送数据,相互通信,所以WebSocket是一种在一个TCP连接上能够全双工,双向通信的协议
STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,一个非常简单和容易实现的协议,提供了可互操作的连接格式,易于开发并应用广泛。这个协议可以有多种载体,可以通过HTTP,也可以通过WebSocket。在Spring-Message中使用的是STOMP Over WebSocket。
STOMP是一种基于帧的协议, STOMP是基于Text的,但也允许传输二进制数据。 它的默认编码是UTF-8,但它的消息体也支持其他编码方式,比如压缩编码。
一个STOMP帧由三部分组成:命令,Header(头信息),Body(消息体)
COMMAND header1:value1 header2:value2 Body^@
一个实际帧结构
SEND destination:/broker/roomId/1 content-length:57 {“type":"ENTER","content":"o7jD64gNifq-wq-C13Q5CRisJx5E"}
spring 的消息功能是基于消息代理构建的
stomp的架构图
图中各个组件介绍:
git clone https://github.com/spring-guides/gs-messaging-stomp-websocket cd gs-messaging-stomp-websocket //回到spring-boot 2.0.1之前 git checkout 6958af0b02bf05282673826b73cd7a85e84c12d3
几个重要的文件
GreetingController.java(相当于图中的SimpAnnotatonMethod)
package hello; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; @Controller public class GreetingController { //使用MessageMapping注解来标识所有发送到“/hello”这个destination的消息,都会被路由到这个方法进行处理 @MessageMapping("/hello") //使用SendTo注解来标识这个方法返回的结果,都会被发送到它指定的destination,“/topic/greetings”. @SendTo("/topic/greetings") public Greeting greeting(HelloMessage message) throws Exception { Thread.sleep(1000); // simulated delay return new Greeting("Hello, " + message.getName() + "!"); } }
尤其注意,这个处理器方法有一个返回值,这个返回值并不是返回给客户端的,而是转发给消息代理的,如果客户端想要这个返回值的话,只能从消息代理订阅
WebSocketConfig.java
package hello; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; //使用Configuration注解标识这是一个Springboot的配置类. @Configuration //使用此注解来标识使能WebSocket的broker.即使用broker来处理消息 @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { //应用程序以 /app 为前缀,而 代理目的地以 /topic 为前缀 config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/gs-guide-websocket").withSockJS(); } }
运行后
在static/app.js
添加header,需要指定为selector,在文档里有说明,是一个选择器
https://stomp.github.io/stomp-specification-1.0.html
随便发送一条消息即可触发
可以抓个包看到websocket发送的STOMP帧
点击connect时
点击send时
先在ExecutorSubscribableChannel.class的run函数下断,可以发现先来的第一条命令是CONNECT
然后是SUBSCRIBE命令,此时的message如下
然后从header取出selector并判断是否为null
不为null后,调用expressionParser.parseExpression处理selector,返回给expression变量,
然后调用subscriptionRegistry.addSubscription方法添加订阅
传入sessionId,subsId,destination和expression,首先从sessionid中获得info,如果没有就注册订阅,存储着session里的东西,返回DefaultSubscriptionRegistry的实例info
然后获取value,这里为null
DefaultSubscriptionRegistry.SessionSubscriptionInfo value = (DefaultSubscriptionRegistry.SessionSubscriptionInfo)this.sessions.putIfAbsent(sessionId, info);
往下走,添加订阅
把destination put 进destinationLookup
然后调用Subscription初始化id和selector值,最后add进subs变量
然后在缓存中更新订阅
发送的message为
前面的调用很多,我们之间从处理的地方开始跟进
在这之前的调用栈,通过反射来调到我们请求的方法
inovke处理完后,会返回我们的控制器里的值
往下执行,跟入handleReturnValue
调用getReturnValueHandler来获得handler变量,用于处理return数据,有默认的defaultDestinationPrefix
继续跟进
提取headers等信息
然后get Sessionid 为c01pvxeq
获取destination
进入for循环后,首先调用createHeaders,把sessionid传入,主要是设置session和header
然后跟到convertAndSend
调用doConvert函数得到message,然后调用send,开始发送message
调用sendInternal
message
由于timeout为-1,所以调用send传入message参数
跟入sendInternal
调用SimpleBrokerMessageHandler的handleMessageInternal
获取到信息后调用updateSessionReadTime更新时间
检测是否是perfix开头,这里发往的订阅目的地是/topic/greetings,所以checkDestinationPrefix为true
调用sendMessageToSubscribers发送message到订阅地址
跟入findSubscriptions查找订阅,检测destination,没有error则进入findSubscriptionsInternal函数
这里的destinationCache变量是
还记得一开始处理订阅的时候么,我们有一些put操作,相当于存入缓存了,这里就是一个提取的操作
调用DefaultSubscriptionRegistry的getSubscriptions获取订阅信息,
从整个订阅里获取全部的信息然后保存到一个迭代器里,并且赋值给info
最后根据sessionID add result里
最后更新缓存
然后跟进filterSubscriptions
首先判断selector是否存在,存在则进入判断
result变量传入后形参是allMatches
提取出expression,判断不为空
然后在getValue执行spel表达式
调用链很长
https://github.com/spring-projects/spring-framework/commit/e0de9126ed8cf25cf141d3e66420da94e350708a#diff-ca84ec52e20ebb2a3732c6c15f37d37aL217
更改为要重用的静态计算上下文,SimpleEvaluationContext 对于权限的限制更为严格,能够进行的操作更少。只支持一些简单的Map结构。
来看看修改为SimpleEvaluationContext如下代码的执行情况
import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.SimpleEvaluationContext; public class Spel { public static void main(String[] args) { ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); Expression calc = parser.parseExpression("T(Runtime).getRuntime().exec('open /Applications/Calculator.app/')"); calc.getValue(context); } }
不再弹出calc。