0x01 CVE-2023-33246
漏洞描述
RocketMQ 5.1.0及以下版本,在一定条件下,存在远程命令执行风险。RocketMQ的NameServer、Broker、Controller等多个组件外网泄露,缺乏权限验证,攻击者可以利用该漏洞利用更新配置功能以RocketMQ运行的系统用户身份执行命令。 此外,攻击者可以通过伪造 RocketMQ 协议内容来达到同样的效果。
影响版本
5.0.0 <= Apache RocketMQ < 5.1.1
4.0.0 <= Apache RocketMQ < 4.9.6
1.1 环境搭建
1 | vim bin/runserver.sh |
1.2 POC复现
1.2.1 POC
1 | import socket |
1.2.2 疑问POC
参考【1】中的POC,执行总是会报错connect to 127.0.0.1:10911 failed,Google反馈的大都是配置broker,但是实际测试仍然报错。
TODO: 分析原因
1 | import java.util.Base64; |
1.3 漏洞分析
总共涉及到3个线程
- netty server到 NettyRequestProcessor#processRequest(ChannelHandlerContext ctx, RemotingCommand cmd),其中cmd是网络传输Decode 所得
- AdminBrokerProcessor更新brokerController.brokerConfig,其中有个属性rocketmqHome
- 有个周期线程FilterServerManager,会用到rocketmqHome 拼接成命令
具体如下
1.3.1 Netty -> NettyRequestProcessor
调用栈如下:
1 | processRequestCommand:261, NettyRemotingAbstract (org.apache.rocketmq.remoting.netty) |
其逻辑如下
1. NettyServerHandler
NettyServerServer#NettyServerHandler 继承了Netty SimpleChannelInboundHandler 接口
1 | public class NettyServerHandler extends SimpleChannelInboundHandler<RemotingCommand> { |
具体逻辑在NettyRemotingAbstract.pprocessMessageReceived,NettyRemotingAbstract 有4个实现类
NettyRemotingClient
NettyRemotingServer
MultiProtocolRemotingServer,也是继承的NettyRemotingServer
NettyRemotingServer#SubRemotingServer
Tip: 泛型RemotingCommand 是如何实现的?具体实现是NettyDecoder
NettyRemotingServer
1
2
3
4
5
6
7
8
9
10
11
12protected ChannelPipeline configChannel(SocketChannel ch) {
return ch.pipeline()
.addLast(defaultEventExecutorGroup, HANDSHAKE_HANDLER_NAME, handshakeHandler)
.addLast(defaultEventExecutorGroup,
encoder,
new NettyDecoder(),
distributionHandler,
new IdleStateHandler(0, 0,
nettyServerConfig.getServerChannelMaxIdleTimeSeconds()),
connectionManageHandler,
serverHandler
);NettyDecoder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
ByteBuf frame = null;
Stopwatch timer = Stopwatch.createStarted();
try {
frame = (ByteBuf) super.decode(ctx, in);
if (null == frame) {
return null;
}
RemotingCommand cmd = RemotingCommand.decode(frame);
cmd.setProcessTimer(timer);
return cmd;
} catch (Exception e) {
log.error("decode exception, " + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), e);
RemotingHelper.closeChannel(ctx.channel());
} finally {
if (null != frame) {
frame.release();
}
}
return null;
}2. NettyRemotingAbstract#processMessageReceived,逻辑如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) {
if (msg != null) {
switch (msg.getType()) {
case REQUEST_COMMAND:
processRequestCommand(ctx, msg);
break;
case RESPONSE_COMMAND:
processResponseCommand(ctx, msg);
break;
default:
break;
}
}
}3. processRequestCommand
1
2
3
4
5
6
7
8
9
10
11public void processRequestCommand(final ChannelHandlerContext ctx, final RemotingCommand cmd) {
final Pair<NettyRequestProcessor, ExecutorService> matched = this.processorTable.get(cmd.getCode());
final Pair<NettyRequestProcessor, ExecutorService> pair = null == matched ? this.defaultRequestProcessorPair : matched;
final int opaque = cmd.getOpaque();
...
Runnable run = buildProcessRequestHandler(ctx, cmd, pair, opaque);
...
try {
final RequestTask requestTask = new RequestTask(run, ctx.channel(), cmd);
//async execute task, current thread return directly
pair.getObject2().submit(requestTask);- 最终调用ExecutorService.submit(requestTask) 实现进程派生
4. RequestTask#run
1 | public RequestTask(final Runnable runnable, final Channel channel, final RemotingCommand request) { |
- 所以逻辑是在buildProcessRequestHandler 内
5. NettyRemotingAbstract#buildProcessRequestHandler
1 | private Runnable buildProcessRequestHandler(ChannelHandlerContext ctx, RemotingCommand cmd, |
- 具体逻辑在NettyRequestProcessor#processRequest
6. NettyRequestProcessor#processRequest
NettyRequestProcessor有很多实现
- AdminBrokerProcessor
- AbstractSendMessageProcessor
- AckMessageProcessor
- AdminBrokerProcessor
- ChangeInvisibleTimeProcessor
- …
1.3.2 AdminBrokerProcessor更新brokerController.brokerConfig
1 | setRocketmqHome:776, BrokerConfig (org.apache.rocketmq.common) |
Invoke
注意这里的invoke MixAll
- properties2Object
实际上,也就这些属性能被update
1.3.3 周期任务FilterServerManager
1 | callShell:25, FilterServerUtil (org.apache.rocketmq.broker.filtersrv) |
注意FilterServerManager 的构造函数BrokerController 有final修饰
1 | public FilterServerManager(final BrokerController brokerController) { |
BrokerController
1 | public BrokerController( |
BrokerStartup
1 | public static BrokerController createBrokerController(String[] args) { |
大致的工作流程:
1 | BrokerStartup |
0x02 CVE-2023-37582
2.1 POC
参考【4】
2.2 漏洞分析
和命令执行类似,不过只有两层
- 第一层一样
- 第二层用的DefaultRequestProcessor,其UPDATE_NAMESRV_CONFIG 功能updateConfig 可以修改配置文件,其中文件名和文件内容都可自定义
2.2.1 Netty Server -> NettyRequestProcessor
与RCE漏洞类似
2.2.2 UPDATE_NAMESRV_CONFIG
调用栈如下
1 | update:202, Configuration (org.apache.rocketmq.remoting) |
其中 properties2Object 对Configuration#configObjectList 通过invoke方式进行覆盖,其中就有configStorePath,对应getStorePath的路径
1 | for (Object configObject : configObjectList) { |
Invoke
注意这里的invoke MixAll
- properties2Object
实际上,也就这些属性能被update
0x03 漏洞修复
3.1 patch1
https://github.com/apache/rocketmq/pull/6733/files
在NettyRequestProcessor 的实现类上新增了几个属性黑名单
- broker.processor.AdminBrokerProcessor#brokerConfigPath
- controller.processor.ControllerRequestProcessor#configStorePath
- namesrv.processor.DefaultRequestProcessor#kvConfigPath
- namesrv.processor.DefaultRequestProcessor#configStorePathName
3.2 patch2
https://github.com/apache/rocketmq/pull/6749/files
最主要的,把FilterServerManager给删了,彻底没命令执行的Sink了
3.3 patch3
在最新的代码中,不止在Processor 上引入了黑名单,在Config 上也引入了configblacklist
https://github.com/search?q=repo%3Aapache%2Frocketmq%20configblacklist&type=code
这中间应该还发生了一些故事
0x04 拓展思考
还有哪些Config 可以被控制
AdminBrokerProcessor.java
ControllerRequestProcessor.java
DefaultRequestProcessor.java
container.BrokerContainerProcessor.java
可以看到,比patch1 中的过滤多了BrokerContainerProcessor
但是后续BrokerContainerProcessor 也增加了过滤https://github.com/apache/rocketmq/pull/7587/files
4.1 Try Escape CVE-2023-37582
CVE-2023-33246 最终的sink 点整个被删了,没有操作空间
CVE-2023-37582 增加了以上的Patchs
核心逻辑有两个
- Processor 从Command 中获取属性,并更新Config
- UpdateConfig 只是其中的一种Command
- Configure.update() 调用persist 进行文件写入,文件名和文件内容都在Config 中
1. BrokerContainerProcessor
以BrokerContainerProcessor 为例
但是实际上,根源在update 中的properties2Object,而不只是update
2. Bypass 及影响
Bypass 逻辑:
1.addBorker,此时没有受各种黑名单限制
2.updateBrokerConfig:此时只限制了从net 而来的属性,原来的属性不受影响