前言

最近看到某平台上有一篇关于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">
      © 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. 使用扩展名
    http://localhost:8080/employees/nego/Jack.xml
    返回结果为XML
    http://localhost:8080/employees/nego/Jack.json
    返回结果为JSON
    http://localhost:8080/employees/nego/Jack
    使用默认view呈现,比如JSP

  2. HTTP Request Header中的Accept,Accept 分别是 text/jsp, text/pdf, text/xml, text/json, 无Accept 请求头

  3. 使用参数
    http://localhost:8080/employees/nego/Jack?format=xml
    返回结果为XML
    http://localhost:8080/employees/nego/Jack?format=json
    返回结果为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的方式无用。

参考

点击收藏 | 2 关注 | 1 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖