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.
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:
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:
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:
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:
# 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
#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:
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:
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:
* 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
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: