CVE-2020-16875: ejecución remota de código en Microsoft Exchange

En septiembre de 2020 se publicó un parche para CVE-2020-16875 que afecta a Microsoft Exchange 2016 y 2019. La vulnerabilidad permite ejecución remota de código (RCE) a través de los cmdlets proporcionados por el endpoint HTTPS /ecp/DLPPolicy de un servidor Exchange.

La vulnerabilidad y la investigación originales fueron reportadas por Steven Seeley de Source Incite (@steventseeley):

https://srcincite.io/advisories/src-2020-0019/ (Aviso)
https://srcincite.io/pocs/cve-2020-16875.py.txt (PoC)

Para solucionar dicha vulnerabilidad se publicó el siguiente parche que filtraba los cmdlets correspondientes:

https://support.microsoft.com/en-us/help/4577352/security-update-for-exchange-server-2019-and-2016

Pero, como ya ha pasado otra veces, el análisis del parche condujo al bypass del mismo, y buena cuenta de ello dio el equipo de X41 D-Sec, que volvieron a conseguir inyectar cmdlets en un servidor remoto Exchange. Eso sí, recordar que se requiere un usuario válido con permisos para administrar las políticas de DLP.

El código del parche que previene la explotación es el siguiente:

internal static void ValidateCmdletParameters(string cmdlet,
    IEnumerable<KeyValuePair<string, string>> requiredParameters)
{
    if (string.IsNullOrWhiteSpace(cmdlet))
    {
        return;
    }
    Collection<PSParseError> collection2;
    Collection<PSToken> collection = PSParser.Tokenize(cmdlet,
        out collection2);
    if (collection2 != null && collection2.Count > 0)
    {
        throw new DlpPolicyParsingException(
            Strings.DlpPolicyNotSupportedCmdlet(cmdlet));
    }
    if (collection != null)
    {
        // #1 CHECKS IF THERE IS MORE THAN ONE COMMAND, BUT DOES NOT
        // RECOGNIZE .NET FUNCTIONS SUCH AS [Int32]::Parse("12")
        if ((from token in collection
        where token.Type == PSTokenType.Command 
        select token).ToList<PSToken>().Count > 1)
        {
            throw new DlpPolicyParsingException(
                Strings.DlpPolicyMultipleCommandsNotSupported(cmdlet));
        }
    }
    bool flag = false;
    foreach (KeyValuePair<string, string> keyValuePair in requiredParameters)
    {
        // #2 CHECKS IF THE cmdlet STRING(!!) STARTS WITH AN ALLOWED KEY
        if (cmdlet.StartsWith(keyValuePair.Key,
            StringComparison.InvariantCultureIgnoreCase))
        {
            // #3 CHECKS IF THE THE VALUES / PARAMETERS MATCH A CERTAIN
            // REGEX
            if (!Regex.IsMatch(cmdlet, keyValuePair.Value,
                RegexOptions.IgnoreCase))
            {
                throw new DlpPolicyParsingException(
                    Strings.DlpPolicyMissingRequiredParameter(cmdlet,
                        keyValuePair.Value));
            }
            flag = true;
        }
    }
    if (!flag)
    {
        throw new DlpPolicyParsingException(Strings.DlpPolicyNotSupportedCmdlet(
                                                                        cmdlet));
    }
}
Se intenta validar el cmdlet con dos objetivos principales:

1. Evitar varios tokens de comandos de Powershell por cmdlet.
2. Permitir sólo comandos incluidos en la lista blanca con ciertos parámetros.

Para ello, se introdujeron tres controles para filtrar intentos de explotación pero desafortunadamente, estos controles no son suficientes...

Bypass Check #1

El primer check del parche es analizar y tokenizar la cadena de cmdlet y comprobar si hay presente más de un token de tipo PSTokenType.Command. Esto bloquea el payload que tiene varios comandos como por ejemplo:
New-object System.Diagnostics.ProcessStartInfo;$i.UseShellExecute=$true;
$i.FileName="cmd";$i.Arguments="/c %s";
$r=New-Object System.Diagnostics.Process;$r.StartInfo=$i;$r.Start()

También se evitan los tokens de comando dentro de las declaraciones `$()` como la siguiente:

new-transportrule -Name $(Diagnostics.Process.Start(....))

Sin embargo, las llamadas directas a .NET a través de la sintaxis de corchetes siguen siendo posible, ya que PSParser no los considera como tokens de "Comando":
new-transportrule -Name $([Diagnostics.Process]::start("cmd.exe", "/C ...."))
Así que con el comando anterior se consigue bypassear el primer check.

Bypass Check #2

La segunda comprobación se puede omitir fácilmente porque la verificación se realiza en el string en raw del cmdlet y solo usa la función `.StartsWith ()` para comprobar el comienzo del cmdlet. Para bypassearlo solo suministramos una string de comando que esté contenida en las claves válidas dadas a través de requiredParameters:
new-transportruleSOMETHINGELSE....

Bypass Check #3

El tercer check está aplicando una expresión regular al cmdlet para asegurarse de que solo se proporcionan parámetros y valores válidos. Desafortunadamente la expresión regular se queda corta en al menos dos formas:

1. Por defecto, Regex.IsMatch() no matchea varias líneas.
2. La expresión regular aplicada no coincide desde el principio hasta el final del string del cmdlet, pero en su lugar también matchea con subcadenas.

Para omitir la verificación, el cmdlet de Powershell solo necesita contener uno de los parámetros esperados como por ejemplo `DlpPolicy`.

PoC

El siguiente payload puede omitir las tres comprobaciones mencionadas anteriormente:

<![CDATA[ new-transportrule
   -Name $([Diagnostics.Process]::start("cmd.exe / C <run-as-SYSTEM>"))
   -DlpPolicy "%%DlpPolicyName%%"
]]>

Al final, el impacto es idéntico a la vulnerabilidad anterior presente en las instalaciones de Exchange sin parchear.

Versiones afectadas
  •     Microsoft Exchange Server 2016 Cumulative Update 16
  •     Microsoft Exchange Server 2016 Cumulative Update 17
  •     Microsoft Exchange Server 2019 Cumulative Update 5
  •     Microsoft Exchange Server 2019 Cumulative Update 6
Contramedidas

El parche se publicó durante el Patch Tuesday de diciembre. En cualquier caso, es recomendable crear un interfaz más estricto para el acceso a la API del DLP de Exchange. El uso de cmdlets es intrínsecamente peligroso debido a la complejidad del lenguaje de scripting de Powershell. En su lugar, se recomienda una interfaz dedicado que solo acepte parámetros individuales incluidos en lista blanca.

Fuentes:
- https://x41-dsec.de/security/advisory/exploit/research/2020/12/21/x41-microsoft-exchange-rce-dlp-bypass/
- https://support.microsoft.com/en-us/help/4577352/security-update-for-exchange-server-2019-and-2016
- https://msrc.microsoft.com/update-guide/en-us/vulnerability/CVE-2020-16875
- https://media.cert.europa.eu/static/SecurityAdvisories/2020/CERT-EU-SA2020-044.pdf
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-16875

Comentarios