CC注入Tomcat Upgrade/Executor/WebSocket内存马
godown WEB安全 1183浏览 · 2025-04-14 10:16

学习一下Tomcat中和组件内存马不一样的马。除了学习注入原理外,其payload还在一些缩短payload的场景有应用,比如shiro

CC注入Tomcat Upgrade/Executor/WebSocket内存马

漏洞所用环境及测试全部代码https://github.com/godownio/TomcatMemshell

漏洞路由为yourip:8080/TomcatMemshell_war_exploded/Vuln

Upgrade内存马

反向代理(Reverse Proxy)是一种服务器,它站在客户端和目标服务器之间代表客户端向目标服务器发起请求,然后把服务器的响应返回给客户端

你看不到背后的服务器,甚至都不知道它有多少台,或者地址是什么。

图片加载失败


图片加载失败


在有反向代理的情况下,植入需要新映射url的servlet、controller内存马因为没有在反向代理中配置的原因,会无法找到对应路径。又或者是在当前路径访问的Filter、Listener、valve可能因为原有Filter的原因注入失败。而且新加组件的动静非常大。通用回显严格说来不算内存马,每次都需要打一次payload,只能说是一种回显方式,并没有做持久化。blue0师傅发现在Processor中也存在一种可以利用的内存马

随便找个servlet项目在doGet or doPost打上断点

Http11Processor.service()内调用了isConnectionToken,判断header里有没有Connection:upgrade的请求头;

如果有的话取出Upgrade请求头对应的对象名,然后getUpgradeProtocol取出UpgradeProtocol。然后调用该对象的accept方法

图片加载失败


图片加载失败


什么意思呢?就是如下请求头,会调用protocol.getUpgradeProtocol取出test这个UpgradeProtocol

看到getUpgradeProtocol方法,从httpUpgradeProtocols中取出对象

图片加载失败


接下来寻找在哪初始化的httpUpgradeProtocols,如果在Tomcat启动时初始化,那么就可以进行修改,如果在一次请求中初始化,则理论上每次请求都不一样,就不能注入内存马

在configureUpgradeProtocol方法内向httpUpgradeProtocols内put了upgradeProtocol,且将其name作为键名

图片加载失败


通过查找用法configureUpgradeProtocol仅在AbstractHttp11Protocol.init内调用

图片加载失败


通过在该方法打上断点,发现在Tomcat初始化时通过如下调用栈调用:

图片加载失败


且初始upgradeProtocols为空,给我们注入留下了比较稳定的空间

图片加载失败


1恶意代码应该写在哪?

上文提到了,获取了UpgradeProtocol后,会去主动调用accept方法,且参数request是org.apache.coyote.Request。则写入一个恶意UpgradeProtocol,accept内为恶意代码

图片加载失败


1如何获取httpUpgradeProtocols?

通过request能获取到Http11NioProtocol,这是AbstractHttp11Protocol的子类,能获取到httpUpgradeProtocols

RequestFacade.request.connector.protocolHandler.httpUpgradeProtocols

图片加载失败








然后是payload:

如果是js环境推荐直接从request中获取

并不能从StandardContext中获取到request,StandardContext 是容器结构,而 Request 是运行时结构

需要用到通用回显的方式获取到request

给一个Latch1通过全局global获取request姿势拼接的Upgrade内存马:

注意执行命令需要带上Connection: UpgradeUpgrade请求头

图片加载失败


Executor内存马(不稳定,复现失败)

上面的Upgrade马实际上作用于下图connector内的Http11Processor,实际上还有更靠前的位置可以注入内存马。但是靠前的位置带给了这种内存马极其不稳定回显的局限性

还是可以学习一下,因为说不定就找到了稳定的回显,这种Executor马又具有常规组件查杀工具查杀不到的性质,未来可期呢。

图片加载失败


图上Endpoint属于Http11Protocol的一部分,实际Http11Protocol在Tomcat高版本处弃用状态,实际使用的是Http11NioProtocol

图片加载失败


Endpoint

EndpointProtocolHandler 中用来监听端口、接收连接、分发 socket 给 Processor 处理的组件。

NioEndpoint:基于 Java NIO 的非阻塞实现

AprEndpoint:基于 APR 的 native 实现

JIoEndpoint:老的基于传统 BIO 的实现

你可以把它当作一个“Socket Server”,它主要负责:

图片加载失败


Processor

Processor才是真正处理请求的逻辑,比如 Http11Processor

解析 HTTP 请求

封装成 Request / Response

调用 servlet 容器(Engine → Host → Context → Wrapper)

HTTP请求到来,Tomcat处理的简化流程如下:

1 Connector 初始化并创建 ProtocolHandler

2 ProtocolHandler 持有一个 Endpoint(如 NioEndpoint

3 NioEndpoint 启动并监听端口

4来一个连接后:

NioEndpoint 分配线程处理连接

调用 Processor(如 Http11Processor)来解析和响应请求

上面的Upgrade内存马,实际上也可以被称作Processor内存马

下面我们详细解释一下上面提到的Endpoint

Endpoint

Acceptor

Acceptor属于Endpoint的一部分

Tomcat 启动后,每个 Connector 会对应一个 EndpointEndpoint 会启动一个 Acceptor 线程

调用 ServerSocketChannel.accept() 等方法接受客户端连接

将 Socket 通知给 Poller / Worker 线程(由线程池处理请求)

图片加载失败


NIO 模型下,不是每个连接对应一个线程,而是将连接注册到 Selector 上。

Poller 就是维护 Selector 的线程,不断轮询:“哪个连接有数据要读?”

Poller的主要工作如下,很好理解:

注册 SocketChannelSelector

Selector.select() 时发现连接有数据可读

将“有数据可读的连接”放到工作队列中,交给 Worker 来处理

Executor

似乎Endpoint有Acceptor就足够分发请求到Processor了,那Executor在整个处理过程中有什么作用呢?

默认每个 ConnectorEndpoint 会自己创建一个线程池来处理 Socket 连接;如果有多个 Connector(比如 HTTP + AJP),那就是多个线程池,不容易统一配置和管理;引入 Executor 后,就可以在多个 Connector 之间共享线程池。

如果配置了Executor,Endpoint(比如 NioEndpoint)的线程池不再自己 new,而是用这个共享的 Executor

从一个请求到达Tomcat的栈图可以验证以上理论

图片加载失败


其中Poller不属于Executor,worker属于Executor

图片加载失败


那么调用到Executor的地方一定是在Poller中,我们来调试一下

调试分析

断点先打在NioEndpoint$Poller.Poller(),Poller构造函数新建了WindowsSelectorImpl作为selector

图片加载失败


请求到来时,断点打在NioEndpoint$Poller.run(),调用了processKey()

图片加载失败


processKey内调用了processSocket。该方法用于处理 SelectionKey 的读写事件,优先处理读事件,若失败则关闭连接;接着处理写事件,若失败也关闭连接。若存在发送文件数据,则直接处理文件发送。

图片加载失败


跟进到processSocket,调用了getExecutor获取executor,然后调用了对应的execute方法

图片加载失败


没有Executor配置的情况下,默认是取出ThreadPoolExecutor

图片加载失败


org.apache.tomcat.util.threads.ThreadPoolExecutor实现自java.util.concurrent.ThreadPoolExecutor,那自定义的executor也实现这个类下的接口Executor,然后重写execute为回显代码

图片加载失败


图片加载失败


注入点

在哪注入Executor?

调用到getExecutor的栈如下:

图片加载失败


getExecutor方法属于AbstractEndpoint,很明显getExecutor可以通过NioEndpoint去触发,变量也是存在NioEndpoint中

图片加载失败


能通过NioEndpoint去调用setExecutor,可以看到设置了Executor并把内置的executor置为空,这样就不会调用到上述的默认ThreadPoolExecutor了

图片加载失败


现在的目标是找到NioEndpoint

用java-object-searcher可以找到

https://godownio.github.io/2025/04/09/li-yong-java-object-searcher-gou-jian-tomcat-xian-cheng-hui-xian/

搜索的结果如下

链子肯定是选下面这条

注意,并不是每次都在[14]的位置存在NioEndpoint$Poller,可以确定的是Thread名包含ClientPoller的肯定会存在NioEndpoint$Poller。为了避免歧义,一定要固定好ClientPoller-0或者ClientPoller-1

图片加载失败


目前的整体框架:

回显

重写的execute参数只有command,没有以往组件内存马自带的request作为参数,如何回显?

图片加载失败


executor可以打通用回显吗?

答案是不能,因为请求到达Executor时,如上文所说,还没有进行Processor处理,取到了request,但是request里没东西

下图是我打入内存马后用?cmd=ipconfig测试,结果request为R(null)的例子

图片加载失败


executor内存马中,就不能想着去取request进行回显了

这个内存马的发现者肯定找到了办法进行回显的

网上很火的一种方式是找Channel,消息没有读取出来,肯定是在channel里

按照如下变量栈,可以找到请求:

command.socketWrapper.socket.appReadBufHandler.byteBuffer.hb

不过获取的请求是byte形式,转为String后也无法直接作为request,不过可以利用固定的分割作为匹配

图片加载失败


图片加载失败


比如使用自定义的Header,并将显著标志作为字符串结尾,如cmd:whoami\r

问题1

但是我们来分析一下:

最开始执行到ThreadPoolExecutor.executor内appReadBufHandler是空的,注意!此时已经到Executor内了,意味着我们自定义的Executor也会和这个一样appReadBufHandler为空。

图片加载失败


Executor发给Worker处理后,初始化Http11InputBuffer才会调用到setAppReadBufHandler进行处理!所以以上读取requestString的方式是错的

图片加载失败


图片加载失败


如果有appReadBufHandler,那是上一次请求的缓存。比如先打一个注册Executor内存马,那想执行到execute时,appReadBufHandler中不是带命令的request,而是上一次注册Executor的请求,如果你在注册Executor请求中就带上cmd:xxx\r,那就能执行命令,这也是众多文章说此方法不稳定的原因

问题2

如果制作的类如下,修改了executor会导致后面的诸多正常请求无法完成分发,导致Tomcat出现错误

图片加载失败


正常的业务极有可能被直接打崩

当然,如果继承ThreadPoolExecutor正常业务就能顺利运行,但是就无法继承AbstractTranslet了,应用面会比较窄,所以作者给出的都是JSP的代码

回显构造(不稳定)

我的思路是自定义defineClass去加载恶意Executor,但是触发方式依旧是极不稳定

作者给出的第二版代码https://xz.aliyun.com/news/11059

甚至触发稳定性不如第一版,-.-

给一个第二版代码,definClass后是能成功触发到executor的getRequest的,只是取request大概率为空导致复现失败。加载字节码的思路是没错的

注意!defineClass加载字节码所用的ClassLoader必须是Tomcat的WebappClassLoader,只有这个类加载器加载的字节码才能使用Tomcat的类,比如org.apache.tomcat.util.threads.ThreadPoolExecutor,否则会报java.lang.NoClassDefFoundError: org/apache/tomcat/util/threads/ThreadPoolExecutor

恶意Executor:生成Base64放到代码2

ExecutorMemShell:

所以我放弃这个马,我认为这个马有很多的局限性,当然作者思路还是很NB的,绝大部分问题在于我的技术不足

WebSocket内存马

Tomcat从7.0.2版本开始支持WebSocket。Tomcat 从 7.0.47 版本开始支持 JSR 356,废弃了之前的自定义 WebSocket API。引入了全新的API,包括:

注解驱动的 WebSocket(@ServerEndpoint

标准的 SessionMessageHandlerEndpoint 等类接口

与 Servlet 容器集成

tomcat-catalina 是 Tomcat 的核心模块之一,但 它并不包含 WebSocket 支持本身。要启用 WebSocket 功能,还需要额外添加 WebSocket 相关模块

Spring Boot 2.0 及以上版本,就已经 内置支持 WebSocket(JSR-356 标准)

WebSocket 是 全双工 的,客户端和服务器可以同时发送消息。

通信双方 都是 Endpoint,只是角色不同:

客户端ClientEndpoint

服务端ServerEndpoint

要注入WebSocket内存马,先看看建立一个WebSocket通道具体怎么实现的,可分为注解实现和手动实现两种

注解实现

服务端

利用@ServerEndpoint注解创建WebSocket。服务端在接收到握手请求后,为每个连接自动创建一个新的 ServerEndpoint 实例。

如下:

客户端

用WebSocketContainer.connectToServer进行连接

其中@OnOpen,@OnMessage,@OnClose,@OnError的功能相信大家都能理解

手动实现

可以手动实现@ServerEndpoint的功能,那么该注解的功能是什么呢?

GPT问一下非springboot项目如何调试@ServerEndpoint注册过程,知道在WsSci.onStartup()中开始注册被@ServerEndpoint注解装饰的类

图片加载失败


关于整个SCI的过程,其实得从SPI说起。详情:https://godownio.github.io/2024/10/28/snakeyaml/#SPI-%E6%9C%BA%E5%88%B6

简单来说,SPI就是在运行时从 META-INF/services/ 目录下指定的配置文件中加载实现类。

Tomcat启动时,会使用SPI扫描所有JAR

ServletContainerInitializer就满足SPI的条件,作为配置文件,指定初始化时加载WsSci



图片加载失败


看到WsSci,@HandlesTypes({ServerEndpoint.class, ServerApplicationConfig.class, Endpoint.class}) 用于标注一个 ServletContainerInitializer(SCI) 实现类,告诉 Servlet 容器:

“启动时请帮我收集所有继承了 ServerEndpointServerApplicationConfigEndpoint 的类,我需要用它们来完成 WebSocket 的初始化配置。”

图片加载失败


@HandlesTypesServlet 3.0 中引入的注解,标记在实现了 ServletContainerInitializer 的类上,容器会根据你指定的类型自动收集相关类,并作为参数传入 onStartup() 方法。

一句话说来,SCI就是 Servlet 3.0 引入的一种机制,容器在部署时会扫描 META-INF/services/ServletContainerInitializer,加载并执行其中定义类的 onStartup() 方法,实现动态组件注册。

从代码上来说,Tomcat启动时,LifecycleBase.start会调用startInternal()

图片加载失败


startInternal方法会循环调用initializers(也就是META-INF/services/ServletContainerInitializer)内类的onStartup方法

图片加载失败


WsSci#OnStartup方法上打上断点

首先调用了init方法

图片加载失败


初始化WsServerContainer并返回

图片加载失败


回到OnStartup,对SCI收集到所有继承了 ServerEndpointServerApplicationConfigEndpoint 的类作为参数clazzes,对这个参数进行一个遍历

图片加载失败


里面就有自定义的HelloWebSocket

图片加载失败


循环的主要功能是遍历传入的类集合 clazzes,并对每个类进行分类和过滤。下图后面三个循环分别是:

如果类实现了 ServerApplicationConfig 接口,则实例化该类并添加到 serverApplicationConfigs 集合中。

如果类继承了 Endpoint 类,则将其添加到 scannedEndpointClazzes 集合中。

如果类标注了 @ServerEndpoint 注解,则将其添加到 scannedPojoEndpoints 集合中。

图片加载失败


循环结束后,if块调用了把scannedPojoEndpoints添加到filteredPojoEndpoints,else块是针对上面类继承Endpoint的处理。

图片加载失败


什么情况会走到else块呢?

如下利用实现ServerApplicationConfig,自定义决定注册哪些Endpoint

getEndpointConfigs(...):用于注册程序化配置的 Endpoint(继承自 jakarta.websocket.Endpoint)。

getAnnotatedEndpointClasses(...):用于筛选哪些带有 @ServerEndpoint 注解的类要被注册。

WsSci也指定了收集该类的子类

图片加载失败


这样就会在else块内就会调用getEndpointConfigs和getAnnotatedEndpointClasses去做对应的注册。相当于一个WebSocket筛选器

图片加载失败


之后循环对filteredPojoEndpoint调用WsServerContainer.addEndpoint,跟进该方法

图片加载失败


图片加载失败


WsServerContainer.addEndpoint方法内首先是检查传入的类是否有ServerEndpoint注解,所以我们手动注册WebSocket内存马肯定是不能直接调用WsServerContainer.addEndpoint的

图片加载失败


接着调用了ServerEndpointConfig.Builder.create().build(),可以看到参数是HelloWebSocket和value写的映射路径/hello-websocket

图片加载失败


至于后面跟的decoders,encoders,subprotocols,configurator,是因为ServerEndpoint还有一些扩展使用,如下。但是这一块我们无需关注

@ServerEndpoint(value = "/ws/{userId}", encoders = {MessageEncoder.class}, decoders = {MessageDecoder.class}, configurator = MyServerConfigurator.class)

在调用完create().build()后,调用了另一个参数类型的WsServerContainer.addEndpoint(ServerEndpointConfig sec, boolean fromAnnotatedPojo)

图片加载失败


WsServerContainer共有四个addEndpoint,注意参数的区别

图片加载失败


该addEndpoint内没有了对注解的判断,所以是能直接用的

图片加载失败


由上面的分析可知,理论上,假如是通过继承Endpoint实现的ServerEndpoint,则必须通过经过serverApplicationConfigs的筛选才能注册进filteredEndpointConfigs

图片加载失败


但是直接调用最后的addEndpoint,则完全不用管serverApplicationConfigs

内存马实现

因为是手动调用WsServerContainer.addEndpoint(ServerEndpointConfig sec, boolean fromAnnotatedPojo),如何获取WsServerContainer呢?

还记得WsSci.init吗,初始化WsServerContainer后调用StandardContext.setAttribute装到到键名为javax.websocket.server.ServerContainer

图片加载失败


用getAttribute能从StandardContext取出,java中获取StandardContext的方法见:https://godownio.github.io/2025/02/26/tomcat-xia-huo-qu-standardcontext-de-fang-fa-jsp-zhuan-java-nei-cun-ma/

当然也可以通过StandardContext->ApplicationContext->attributes反射取出

图片加载失败


java-object-searcher从线程中也是从StandardContext中找到的WsServerContainer:

图片加载失败


好像只有这一种方式取到WsServerContainer了

WebSocket内存马的实现过程如下,依旧通过defineClass加载字节码:

1获取StandardContext,进而获取WsServerContainer

2用ServerEndpointConfig.Builder.create().build()创建恶意ServerWebSocket,用addEndpint添加进WsServerContainer

3需要一个相适配的ClientWebSocket去连接

注意继承Endpoint实现的恶意ServerWebSocket,重写onMessage需要实现MessageHandler.Whole<String>,至于为什么是重写onMessage各位肯定知道

先制作一个恶意的EvilServerWebSocket:

然后用TemplatesImpl去注册上面的EvilServerWebSocket,注意这里测试,在上面的代码生成完base64码后注释掉,不然会报 loader (instance of org/apache/catalina/loader/ParallelWebappClassLoader): attempted duplicate class definition重复加载类

然后用自己做的客户端WebSocketClient去连接

演示一波功能:

图片加载失败


图片加载失败


大佬已经做了一整套webSocket内存马利用开源工具了:

https://github.com/veo/wsMemShell/tree/main

这种内存马很明显有个好处,就是新开ws TCP通道,很多防护设备可能检测不到该通道的流量,不过坏处也在于此,新开通道很容易被监测。

REF:

https://mp.weixin.qq.com/s/RuP8cfjUXnLVJezBBBqsYw

https://xz.aliyun.com/news/11012

https://wileysec.github.io/fee784a451d7.html

0 条评论
某人
表情
可输入 255

没有评论