環(huán)境搭建
先搭建一個(gè)SpringMVC項(xiàng)目,參考這篇文章,或者參考我以前的spring內(nèi)存馬分析那篇文章
https://blog.csdn.net/weixin_65287123/article/details/136648903
SpringMVC路由
簡(jiǎn)單寫(xiě)個(gè)servlet
package com.example.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.UnsupportedEncodingException;
import java.util.Base64;
@Controller
public class TestController {
@GetMapping("/")
public String Welcome(String type) throws UnsupportedEncodingException {
System.out.println(type);
if(!type.equals("")) {
return "hello";
}
return "index";
}
@ResponseBody
@RequestMapping("/readobject")
public String frontdoor(String payload) throws IOException, ClassNotFoundException {
byte[] base64decodedBytes = Base64.getDecoder().decode(payload);
ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
return "right";
}
}
這樣就是訪問(wèn)到index.jsp
路由解析流程主要就是Model和View以及最后Render。return處打個(gè)斷點(diǎn),看怎么處理的
先進(jìn)入invokeAndHandle
,調(diào)用invokeForRequest
方法,這個(gè)操作會(huì)獲取到我們傳進(jìn)去的視圖名稱
往下走,這個(gè)mavContainer就是一個(gè)ModelAndViewContainer容器對(duì)象
進(jìn)入handleReturnValue
方法
跟進(jìn)另一個(gè)handleReturnValue
略過(guò)dispatch的調(diào)試環(huán)節(jié),直接定位到render處
往下走進(jìn)入ThymeleafView的render方法,然后走到這
這個(gè)方法是重點(diǎn),后面會(huì)說(shuō)到,退出這個(gè)方法后,流程就結(jié)束了
Thymeleaf模板注入成因
其實(shí)就是上面的renderFragment
函數(shù)。這里直接講3.0.12版本后的方法,因?yàn)?.0.12后加了一層check,需要繞過(guò),之前版本的就是直接SPEL表達(dá)式就可以RCE,__${T%20(java.lang.Runtime).getRuntime().exec(%22calc%22)}__::.x
poc如上,接下來(lái)我們將一步步解釋為什么poc是上述形式,先改一下controller
@GetMapping("/")
public String Welcome(String type) throws UnsupportedEncodingException {
System.out.println(type);
if(!type.equals("")) {
return "hello/"+type+"/challenge";
}
return "index";
}
type傳入我們的payload,renderFragment
方法里獲取我們的payload
往下走,這里會(huì)判斷viewTemplateName
是否包含::
這里需要介紹一個(gè)東西
Thymeleaf 是與 java 配合使用的一款服務(wù)端模板引擎,也是 Spring 官方支持的一款服務(wù)端模板引擎。而 SSTI 最初是由 [James Kettle](https://portswigger.net/research/server-side-template-injection) 提出研究,[Emilio Pinna](https://github.com/epinna/tplmap) 對(duì)他的研究進(jìn)行了補(bǔ)充,不過(guò)這些作者都沒(méi)有對(duì) Thymeleaf 進(jìn)行 SSTI 相關(guān)的漏洞研究工作,后來(lái) Aleksei Tiurin 在 ACUNETIX 的官方博客上發(fā)表了關(guān)于 Thymeleaf SSTI 的[文章](https://www.acunetix.com/blog/web-security-zone/exploiting-ssti-in-thymeleaf/),因此 Thymeleaf SSTI 逐漸被安全研究者關(guān)注。
為了更方便讀者理解這個(gè) Bypass,因此在這里簡(jiǎn)單說(shuō)一遍一些基礎(chǔ)性的內(nèi)容,如果了解的,可以直接跳到 0x03 的內(nèi)容。
Thymeleaf 表達(dá)式可以有以下類型:
- ${...}:變量表達(dá)式 —— 通常在實(shí)際應(yīng)用,一般是OGNL表達(dá)式或者是 Spring EL,如果集成了Spring的話,可以在上下文變量(context variables )中執(zhí)行
- *{...}: 選擇表達(dá)式 —— 類似于變量表達(dá)式,區(qū)別在于選擇表達(dá)式是在當(dāng)前選擇的對(duì)象而不是整個(gè)上下文變量映射上執(zhí)行。
- #{...}: Message (i18n) 表達(dá)式 —— 允許從外部源(比如.properties文件)檢索特定于語(yǔ)言環(huán)境的消息
- @{...}: 鏈接 (URL) 表達(dá)式 —— 一般用在應(yīng)用程序中設(shè)置正確的 URL/路徑(URL重寫(xiě))。
- ~{...}:片段表達(dá)式 —— Thymeleaf 3.x 版本新增的內(nèi)容,分段段表達(dá)式是一種表示標(biāo)記片段并將其移動(dòng)到模板周圍的簡(jiǎn)單方法。 正是由于這些表達(dá)式,片段可以被復(fù)制,或者作為參數(shù)傳遞給其他模板等等
實(shí)際上,Thymeleaf 出現(xiàn) SSTI 問(wèn)題的主要原因也正是因?yàn)檫@個(gè)片段表達(dá)式,我們知道片段表達(dá)式語(yǔ)法如下:
1. ~{templatename::selector},會(huì)在/WEB-INF/templates/目錄下尋找名為templatename的模版中定義的fragment
重點(diǎn)是片段表達(dá)式。假如有一個(gè)html代碼
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body> <div th:fragment="banquan"> © 2021 ThreeDream yyds</div>
</body>
</html>
我們需要在另一個(gè)template模板文件引用上述的fragment
<div th:insert="~{footer :: banquan}"></div>
這就是片段表達(dá)式,片段表達(dá)式后面必須要有一個(gè)名字,這也對(duì)應(yīng)payload中的.x
,這個(gè).x
就是名稱,那個(gè).
也可以去掉改為任意的字符串.
繼續(xù)往下走,fragmentExpression
處進(jìn)行了一個(gè)拼接,剛好是片段表達(dá)式形式的拼接
跟進(jìn)parser.parseExpression
這個(gè)方法
繼續(xù)跟進(jìn),這里進(jìn)入preprocess
函數(shù)
注意上方的Pattern,Pattern.compile("\\_\\_(.*?)\\_\\_", 32);
這個(gè)剛好就能識(shí)別payload的形式,然后由于是片段表達(dá)式,所以有最后的.x
往下走進(jìn)入execute
方法解析匹配到的payload,解析過(guò)程就不說(shuō)了,就是正常的SPEL表達(dá)式解析,說(shuō)一下3.12
版本后的一個(gè)checker文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-849111.html
public static boolean containsSpELInstantiationOrStatic(final String expression) {
/*
* Checks whether the expression contains instantiation of objects ("new SomeClass") or makes use of
* static methods ("T(SomeClass)") as both are forbidden in certain contexts in restricted mode.
*/
final int explen = expression.length();
int n = explen;
int ni = 0; // index for computing position in the NEW_ARRAY
int si = -1;
char c;
while (n-- != 0) {
c = expression.charAt(n);
// When checking for the "new" keyword, we need to identify that it is not a part of a larger
// identifier, i.e. there is whitespace after it and no character that might be a part of an
// identifier before it.
if (ni < NEW_LEN
&& c == NEW_ARRAY[ni]
&& (ni > 0 || ((n + 1 < explen) && Character.isWhitespace(expression.charAt(n + 1))))) {
ni++;
if (ni == NEW_LEN && (n == 0 || !Character.isJavaIdentifierPart(expression.charAt(n - 1)))) {
return true; // we found an object instantiation
}
continue;
}
if (ni > 0) {
// We 'restart' the matching counter just in case we had a partial match
n += ni;
ni = 0;
if (si < n) {
// This has to be restarted too
si = -1;
}
continue;
}
ni = 0;
if (c == ')') {
si = n;
} else if (si > n && c == '('
&& ((n - 1 >= 0) && (expression.charAt(n - 1) == 'T'))
&& ((n - 1 == 0) || !Character.isJavaIdentifierPart(expression.charAt(n - 2)))) {
return true;
} else if (si > n && !(Character.isJavaIdentifierPart(c) || c == '.')) {
si = -1;
}
}
return false;
}
進(jìn)入這個(gè)方法,他會(huì)識(shí)別new關(guān)鍵字,不允許存在new
關(guān)鍵字,并且不允許存在T(.*)
這種形式的字符串,因此就得bypass了,而方法也很簡(jiǎn)單,fuzz一下就知道是T ()
加一個(gè)空格就行了。后續(xù)的一系列利用都是針對(duì)sepl表達(dá)式的研究了文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-849111.html
到了這里,關(guān)于Thymeleaf SSTI模板注入分析的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!