CVE-2018-1270 RCE with spring-messaging分析

 

作者: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

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()

 

Websocket

由于HTTP具有单向通信的特点,于是造成了Server向Client推送消息变得很难,需要使用轮询的方式,于是有了WebSocket,他支持双向通信,类似于聊天室模式,在这个会话里,Server和Clinet都能发送数据,相互通信,所以WebSocket是一种在一个TCP连接上能够全双工,双向通信的协议

 

Stomp

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 的消息功能是基于消息代理构建的

demo

stomp的架构图

图中各个组件介绍:

  • 生产者型客户端(左上组件): 发送SEND命令到某个目的地址(destination)的客户端。
  • 消费者型客户端(左下组件): 订阅某个目的地址(destination), 并接收此目的地址所推送过来的消息的客户端。
  • request channel: 一组用来接收生产者型客户端所推送过来的消息的线程池。
  • response channel: 一组用来推送消息给消费者型客户端的线程池。
  • broker: 消息队列管理者,也可以成为消息代理。它有自己的地址(例如“/topic”),客户端可以向其发送订阅指令,它会记录哪些订阅了这个目的地址(destination)。
  • 应用目的地址(图中的”/app”): 发送到这类目的地址的消息在到达broker之前,会先路由到由应用写的某个方法。相当于对进入broker的消息进行一次拦截,目的是针对消息做一些业务处理。
  • 非应用目的地址(图中的”/topic”,也是消息代理地址): 发送到这类目的地址的消息会直接转到broker。不会被应用拦截。
  • SimpAnnotatonMethod: 发送到应用目的地址的消息在到达broker之前, 先路由到的方法. 这部分代码是由应用控制的。
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时

 

漏洞分析

SUBSCRIBE

先在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变量

然后在缓存中更新订阅

Send

发送的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。

 

Reference

(完)