學習Sping-messaging遠程代碼執行(CVE-2018-1270)
01 漏洞描述
Spring Framework、5.0.5 之前的 5.0.x 版本和 4.3.16 之前的 4.3.x 版本以及不支持的舊版本允許應用程序通過spring-messaging模塊通過簡單的內存 STOMP 代理通過 WebSocket 端點公開 STOMP 。惡意用戶(或攻擊者)可以向代理發送可能導致遠程代碼執行攻擊的消息。
關于SockJS和STOMP
在Spring-messaging中是通過SockJS傳輸的STOPM內容,這個遠程代碼執行漏洞也是在對STOPM解析中出現的,本質上是一個Spring表達式注入。
SockJS
一些瀏覽器中缺少對WebSocket的支持,因此,回退選項是必要的,而Spring框架提供了基于SockJS協議的透明的回退選項。也可以理解為一個類似于http協議,更多的是定義在傳輸層的一個協議。
STOPM
STOMP(Simple (or Streaming) Text Orientated Messaging Protocol)一個簡單的面向文本/流的消息協議。STOMP提供了能夠協作的報文格式,以至于STOMP客戶端可以與任何STOMP消息代理(Brokers)進行通信,從而為多語言,多平臺和Brokers集群提供簡單且普遍的消息協作。
STOMP可用于任何可靠的雙向流網絡協議之上,如TCP和WebSocket。雖然STOMP是面向文本的協議,但消息有效負載可以是文本或二進制。
一個STOMP客戶端是一個可以以兩種模式運行的用戶代理,可能是同時運行兩種模式。
作為生產者,通過SEND框架將消息發送給服務器的某個服務
作為消費者,通過SUBSCRIBE制定一個目標服務,通過MESSAGE框架,從服務器接收消息。
COMMANDheader1:value1header2:value2 Body^@
常用的command
CONNECT STOMP客戶端通過初始化一個數據流或者TCP鏈接發送CONNECT幀到服務端CONNECTED 如果服務端接收了鏈接意圖,它回回復一個CONNECTED幀SEND 客戶端主動發送消息到服務器SUBSRIBE 客戶端注冊給定的目的地,被訂閱的目的地收到的任何消息將通過MESSAGE Frame發送給client。ACK 控制著確認模式。UNSUBSRIBE UNSUBSCRIBE用來移除一個已經存在訂閱,一旦一個訂閱被從連接中取消,那么客戶端就再也不會收到來自這個訂閱的消息
02 漏洞復現
CVE-2018-1270漏洞成功復現需要發送三個包才能成功。
1、CONNECT建立一個連接
2、SUBSRIBE添加一個訂閱,將payload放入服務端緩存存儲
3、SEND觸發payload
環境搭建
可以直接vulhub拉docker搭建也可以用idea拉寫好的代碼搭建gs-messaging-stomp-websocket用github搭建的時候需要注意下要把pom.xml中的spring版本改下不然拉的spring-messaging版本超過5.0.4了就修復了。
org.springframework.boot spring-boot-starter-parent 2.0.0.RELEASE
如果復現不成功一定看下spring-messaging版本是多少可以在idea里的lib里看看。

03 復現方法
1.修改頁面js文件
在發送訂閱的時候插入payload,在頁面上點connect,然后發兩個字符send就觸發了。
function connect() { var header = {"selector":"T(java.lang.Runtime).getRuntime().exec('touch /tmp/mi1k7ea')"}; 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); });}

2.burp抓包修改
可以在攔截器修改也可以全部放到重放里然后需要注意websocket ID訂閱和發送是否一樣,確保是在一個session中否則可能失敗。
["SUBSCRIBEid:sub-0destination:/topic/greetingsselector:T(java.lang.Runtime).getRuntime().exec('open /System/Applications/Calculator.app')\u0000"]




STOMP報文格式是和http很像的所以直接修改增加鍵值對就可以了,主要是selector這個字段,其他字段在這里修改后是沒影響的,只是在后臺會留下報錯的日志,正常情況下是不會留error日志的。

3.直接用exp打就可以了
直接看vulhub那個就能用了CVE-2018-1270_exploit.py,用的時候腳本里改下地址和需要執行的命令。
04 漏洞分析
首先看下補丁,主要修改了DefaultSubscriptionRegistry類中的一些描述,引入的包,EvaluationContext初始化方式和read方法的內容的改變,結合漏洞描述看最有可能的就是EvaluationContext初始化方式的變更最可能存在問題。

EvaluationContext從最開始通過StandardEvaluationContext初始化到通過SimpleEvaluationContext初始化,然后熟悉spring表達式注入的話那么能夠知道使用SimpleEvaluationContext進行初始化是修復表達式注入常用方法之一,SimpleEvaluationContext會對執行的表達式做一些限制使其只能執行一些基礎的操作,不能調用java對象等。所以從補丁分析的起點就是在5.0.4版本的217行,此處是漏洞的觸發點。

在向上看了幾行后發現expression是從sub的getSelectorExpression方法獲取到的,然后context是從213行通過StandardEvaluationContext初始化的,都封裝在filterSubscriptions方法中。
目前存在兩個疑問:
1.filterSubscriptions在什么時候會觸發,如何才能觸發漏洞。
2.expression值是如何取到的,是如何傳入payload的,是在觸發過程中傳入的么?
filterSubscriptions如何觸發到
從污點追蹤來看,首先在本類上面一點185行,通過destintion獲取到了allMatches的值命名為result傳入到補丁修改的filterSubscriptions,然后在父類AbstractSubscriptionRegistry的filterSubscriptions單參數方法對message進行類型判斷和提取destination信息。直到SimpleBrokerMessageHandler類的sendMessageToSubscribers方法都是在對message的信息做提取和判斷。


在到到達了SimpleBrokerMessageHandler類的sendMessageToSubscribers方法后再上面一層是在判斷message的類型,對STOPM協議的不同的命令做處理,從字面意義看是在send命令會進入并觸發漏洞。
SimpleBrokerMessageHandler的handleMessageInternal方法是一個處理不同命令的工廠,這樣的話就還沒有到最外部通過SockJs獲取STOPM報文的點,但是目前到了這里暫時已經夠了能夠知道在什么情況下會觸發到漏洞,后面再去確定如何獲取到報文的。


現在已經找到了在send命令下會執行expression表達式,但是咋沒看到expression表達式從哪里來的呢?
expression值從哪里來
expression值從哪里來關系到payload從哪里來,首先sub是從info中獲取到的,然后subId是從allMatches中獲取到的,allMatches是在185行的this.destinationCache.getSubscriptions方法獲取到的的。就是個套娃,拆套娃肯定是從外面開始拆的.....

獲取allMatches
通過getSubscriptions方法獲取到的allMatches參數值,進去之后是從this.accessCache中獲取到的,然后這個參數是在哪里賦值的呢?

從下圖可以看到accessCache值都是在本類中進行賦值的有四個put,進去看看發現就是300行的那個put的調用鏈中初始化了一個表達式,然后表達式的值也是從message中獲取到的,再向上看發現是在SUBSCRIBE命令中進入到此處實現的中間經過了registerSubscription方法的一些判斷和處理,之后再次回到了熟悉的handleMessageInternal方法。

這里put到cache中的只有cachedDestination和subs,subs只有sessionid和subId并沒有放入表達式。




看到了這里初步估計表達式的值是在SUBSCRIBE命令的情況下傳入的,那又是那個參數導致的呢?
在addSubscriptionInternal方法中有一個getFirstNativeHeader方法將header為selector的鍵值取出作為表達式存儲在sub中了。

目前初步的判斷已經有了,在SUBSCRIBE命令下selector頭值會作為表達式存儲,之后在send命令下取出表達式值執行。但是目前還需要確定是根據什么去存儲和取出的表達式。
獲取info
info直接從Session中通過allmatches中的sesionID獲取到的,然后info是在addSubscriptionInternal中通過addSubscription方法放入的。


獲取sub
獲取就是從info中的sessionID獲取到的,然后在生成的時候就放入了表達式。


關于為什么能通過sessionid和subid獲取到對應的表達式,就是和sockjs接收websocket相關了,應該是在這個session會話中這個處理命令的方法是一直在內存中存活的所以才能取到對應的值。
05 如何挖掘
黑盒
在黑盒的時候可以根據包內容進行初步判斷,如果內容是明顯的STOMP協議格式則可能存在可以用payload嘗試下可能就存在對應版本的漏洞。
白盒
1.查看spring-messaging版本低于5.0.4是存在問題的。
2.查看是否有使用可以通過配置類和配置文件找。
06 修復方案
官方給出的修復方案是升級版本
Spring Framework 5.0到5.0.4升級到5.0.5版本
Spring Framework 4.3到4.3.14升級到4.3.15版本
官方通告里說spring-messaging有問題,而spring-messaging版本跟隨Spring Framework版本,所以重點關注spring-messaging的jar包即可
文章參考:
1.《WebSocket和Stomp協議》
2.《淺析Spring Messaging之CVE-2018-1270》
3.《spring-messaging 遠程代碼執行漏洞分析》
4.《spring源碼分析之spring-messaging模塊詳解》